1package telemetry23import (4 "encoding/hex"5 "encoding/json"6 "fmt"7 "log/slog"8 "strconv"9 "time"10)1112// The OTLP/HTTP JSON encoding of logs, traces and metrics: the protobuf13// messages of opentelemetry-proto under the protobuf JSON mapping, so14// field names are lowerCamelCase, 64-bit integers are decimal strings,15// enums are their numbers and trace and span ids are hex. Only the16// fields 012 sends are declared.1718// i64 is a 64-bit integer, written as a JSON string.19type i64 int642021func (n i64) MarshalJSON() ([]byte, error) {22 return strconv.AppendQuote(nil, strconv.FormatInt(int64(n), 10)), nil23}2425// u64 is an unsigned 64-bit integer, written as a JSON string.26type u64 uint642728func (n u64) MarshalJSON() ([]byte, error) {29 return strconv.AppendQuote(nil, strconv.FormatUint(uint64(n), 10)), nil30}3132func nanos(t time.Time) u64 { return u64(t.UnixNano()) }3334type traceID [16]byte35type spanID [8]byte3637func (id traceID) MarshalJSON() ([]byte, error) { return json.Marshal(hex.EncodeToString(id[:])) }38func (id spanID) MarshalJSON() ([]byte, error) { return json.Marshal(hex.EncodeToString(id[:])) }3940func (id traceID) IsZero() bool { return id == traceID{} }41func (id spanID) IsZero() bool { return id == spanID{} }4243// anyValue is an AnyValue holding one scalar.44type anyValue struct {45 kind byte // 's', 'i', 'd' or 'b'46 s string47 i int6448 d float6449 b bool50}5152func str(s string) anyValue { return anyValue{kind: 's', s: s} }5354func (v anyValue) MarshalJSON() ([]byte, error) {55 switch v.kind {56 case 'i':57 return fmt.Appendf(nil, `{"intValue":"%d"}`, v.i), nil58 case 'd':59 b, err := json.Marshal(v.d)60 return append(append([]byte(`{"doubleValue":`), b...), '}'), err61 case 'b':62 return fmt.Appendf(nil, `{"boolValue":%t}`, v.b), nil63 }64 b, err := json.Marshal(v.s)65 return append(append([]byte(`{"stringValue":`), b...), '}'), err66}6768type keyValue struct {69 Key string `json:"key"`70 Value anyValue `json:"value"`71}7273// appendAttr adds a to kvs, flattening groups into dotted keys.74func appendAttr(kvs []keyValue, prefix string, a slog.Attr) []keyValue {75 a.Value = a.Value.Resolve()76 if a.Equal(slog.Attr{}) {77 return kvs78 }79 if a.Value.Kind() == slog.KindGroup {80 p := prefix81 if a.Key != "" {82 p += a.Key + "."83 }84 for _, g := range a.Value.Group() {85 kvs = appendAttr(kvs, p, g)86 }87 return kvs88 }89 return append(kvs, keyValue{Key: prefix + a.Key, Value: valueOf(a.Value)})90}9192func valueOf(v slog.Value) anyValue {93 switch v.Kind() {94 case slog.KindString:95 return str(v.String())96 case slog.KindInt64:97 return anyValue{kind: 'i', i: v.Int64()}98 case slog.KindUint64:99 return anyValue{kind: 'i', i: int64(v.Uint64())}100 case slog.KindFloat64:101 return anyValue{kind: 'd', d: v.Float64()}102 case slog.KindBool:103 return anyValue{kind: 'b', b: v.Bool()}104 case slog.KindDuration:105 return anyValue{kind: 'd', d: ms(v.Duration())}106 case slog.KindTime:107 return str(v.Time().Format(time.RFC3339Nano))108 }109 return str(v.String())110}111112func attrsOf(as []slog.Attr) []keyValue {113 var kvs []keyValue114 for _, a := range as {115 kvs = appendAttr(kvs, "", a)116 }117 return kvs118}119120type resource struct {121 Attributes []keyValue `json:"attributes"`122}123124type scope struct {125 Name string `json:"name"`126 Version string `json:"version,omitempty"`127}128129// Logs.130131type logsRequest struct {132 ResourceLogs []resourceLogs `json:"resourceLogs"`133}134135type resourceLogs struct {136 Resource resource `json:"resource"`137 ScopeLogs []scopeLogs `json:"scopeLogs"`138}139140type scopeLogs struct {141 Scope scope `json:"scope"`142 LogRecords []logRecord `json:"logRecords"`143}144145type logRecord struct {146 TimeUnixNano u64 `json:"timeUnixNano"`147 ObservedTimeUnixNano u64 `json:"observedTimeUnixNano"`148 SeverityNumber int `json:"severityNumber"`149 SeverityText string `json:"severityText"`150 Body anyValue `json:"body"`151 Attributes []keyValue `json:"attributes,omitempty"`152 TraceID traceID `json:"traceId,omitzero"`153 SpanID spanID `json:"spanId,omitzero"`154}155156// severity maps a slog level onto an OTLP SeverityNumber: slog's levels157// are spaced so that Debug, Info, Warn and Error land on DEBUG (5), INFO158// (9), WARN (13) and ERROR (17).159func severity(l slog.Level) int { return min(max(int(l)+9, 1), 24) }160161// Traces.162163type tracesRequest struct {164 ResourceSpans []resourceSpans `json:"resourceSpans"`165}166167type resourceSpans struct {168 Resource resource `json:"resource"`169 ScopeSpans []scopeSpans `json:"scopeSpans"`170}171172type scopeSpans struct {173 Scope scope `json:"scope"`174 Spans []spanData `json:"spans"`175}176177const (178 spanKindInternal = 1179 statusOK = 1180 statusError = 2181)182183type spanData struct {184 TraceID traceID `json:"traceId"`185 SpanID spanID `json:"spanId"`186 ParentSpanID spanID `json:"parentSpanId,omitzero"`187 Name string `json:"name"`188 Kind int `json:"kind"`189 StartTimeUnixNano u64 `json:"startTimeUnixNano"`190 EndTimeUnixNano u64 `json:"endTimeUnixNano"`191 Attributes []keyValue `json:"attributes,omitempty"`192 Status status `json:"status"`193}194195type status struct {196 Code int `json:"code,omitempty"`197 Message string `json:"message,omitempty"`198}199200// Metrics.201202type metricsRequest struct {203 ResourceMetrics []resourceMetrics `json:"resourceMetrics"`204}205206type resourceMetrics struct {207 Resource resource `json:"resource"`208 ScopeMetrics []scopeMetrics `json:"scopeMetrics"`209}210211type scopeMetrics struct {212 Scope scope `json:"scope"`213 Metrics []metric `json:"metrics"`214}215216const temporalityCumulative = 2217218type metric struct {219 Name string `json:"name"`220 Description string `json:"description,omitempty"`221 Unit string `json:"unit,omitempty"`222 Gauge *gauge `json:"gauge,omitempty"`223 Sum *sum `json:"sum,omitempty"`224 Histogram *histogram `json:"histogram,omitempty"`225 Summary *summary `json:"summary,omitempty"`226}227228type gauge struct {229 DataPoints []numberPoint `json:"dataPoints"`230}231232type sum struct {233 DataPoints []numberPoint `json:"dataPoints"`234 AggregationTemporality int `json:"aggregationTemporality"`235 IsMonotonic bool `json:"isMonotonic"`236}237238type numberPoint struct {239 Attributes []keyValue `json:"attributes,omitempty"`240 StartTimeUnixNano u64 `json:"startTimeUnixNano,omitempty"`241 TimeUnixNano u64 `json:"timeUnixNano"`242 AsInt *i64 `json:"asInt,omitempty"`243 AsDouble *float64 `json:"asDouble,omitempty"`244}245246func intPoint(start, t time.Time, v int64, attrs ...keyValue) numberPoint {247 n := i64(v)248 p := numberPoint{Attributes: attrs, TimeUnixNano: nanos(t), AsInt: &n}249 if !start.IsZero() {250 p.StartTimeUnixNano = nanos(start)251 }252 return p253}254255type histogram struct {256 DataPoints []histogramPoint `json:"dataPoints"`257 AggregationTemporality int `json:"aggregationTemporality"`258}259260type histogramPoint struct {261 Attributes []keyValue `json:"attributes,omitempty"`262 StartTimeUnixNano u64 `json:"startTimeUnixNano"`263 TimeUnixNano u64 `json:"timeUnixNano"`264 Count u64 `json:"count"`265 Sum float64 `json:"sum"`266 BucketCounts []u64 `json:"bucketCounts"`267 ExplicitBounds []float64 `json:"explicitBounds"`268 Min float64 `json:"min"`269 Max float64 `json:"max"`270}271272type summary struct {273 DataPoints []summaryPoint `json:"dataPoints"`274}275276type summaryPoint struct {277 StartTimeUnixNano u64 `json:"startTimeUnixNano"`278 TimeUnixNano u64 `json:"timeUnixNano"`279 Count u64 `json:"count"`280 Sum float64 `json:"sum"`281 QuantileValues []quantileValue `json:"quantileValues"`282}283284type quantileValue struct {285 Quantile float64 `json:"quantile"`286 Value float64 `json:"value"`287}