telemetry
packageAPI reference for the telemetry
package.
Imports
(12)OTLPExporter
OTLPExporter sends spans and metrics to an OTLP-compatible endpoint over HTTP/JSON.
type OTLPExporter struct
Methods
Parameters
func (*OTLPExporter) ExportSpan(span otlpSpan)
{
e.batchMu.Lock()
e.spans = append(e.spans, span)
shouldFlush := len(e.spans) >= e.maxBatch
e.batchMu.Unlock()
if shouldFlush {
e.Flush(context.Background())
}
}
Parameters
func (*OTLPExporter) ExportMetric(m otlpMetric)
{
e.batchMu.Lock()
e.metrics = append(e.metrics, m)
shouldFlush := len(e.metrics) >= e.maxBatch
e.batchMu.Unlock()
if shouldFlush {
e.Flush(context.Background())
}
}
Parameters
Returns
func (*OTLPExporter) Flush(ctx context.Context) error
{
e.batchMu.Lock()
spans := e.spans
metrics := e.metrics
e.spans = nil
e.metrics = nil
e.batchMu.Unlock()
if len(spans) == 0 && len(metrics) == 0 {
return nil
}
payload := map[string]any{
"resourceSpans": []map[string]any{
{
"scopeSpans": []map[string]any{
{"spans": spans},
},
},
},
"resourceMetrics": []map[string]any{
{
"scopeMetrics": []map[string]any{
{"metrics": metrics},
},
},
},
}
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("otlp marshal: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", e.endpoint, bytes.NewReader(data))
if err != nil {
return fmt.Errorf("otlp request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := e.client.Do(req)
if err != nil {
return fmt.Errorf("otlp send: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("otlp status %d: %s", resp.StatusCode, string(body))
}
return nil
}
func (*OTLPExporter) loop()
{
ticker := time.NewTicker(e.flushMs)
defer ticker.Stop()
for {
select {
case <-ticker.C:
e.Flush(context.Background())
case <-e.stopCh:
e.Flush(context.Background())
return
}
}
}
func (*OTLPExporter) Close()
{
close(e.stopCh)
}
Fields
| Name | Type | Description |
|---|---|---|
| endpoint | string | |
| client | *http.Client | |
| batchMu | sync.Mutex | |
| spans | []otlpSpan | |
| metrics | []otlpMetric | |
| maxBatch | int | |
| flushMs | time.Duration | |
| stopCh | chan struct{} |
otlpSpan
type otlpSpan struct
Fields
| Name | Type | Description |
|---|---|---|
| TraceID | string | json:"traceId" |
| SpanID | string | json:"spanId" |
| Name | string | json:"name" |
| StartTime | string | json:"startTimeUnixNano" |
| EndTime | string | json:"endTimeUnixNano" |
| Attrs | map[string]any | json:"attributes,omitempty" |
otlpMetric
type otlpMetric struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | json:"name" |
| Kind | string | json:"kind" |
| Value | float64 | json:"value" |
OTLPOption
OTLPOption configures an OTLPExporter.
type OTLPOption func(*OTLPExporter)
WithOTLPEndpoint
WithOTLPEndpoint sets the OTLP receiver endpoint URL.
Parameters
Returns
func WithOTLPEndpoint(url string) OTLPOption
{
return func(e *OTLPExporter) { e.endpoint = url }
}
WithOTLPBatchSize
WithOTLPBatchSize sets the maximum batch size before flushing.
Parameters
Returns
func WithOTLPBatchSize(n int) OTLPOption
{
return func(e *OTLPExporter) { e.maxBatch = n }
}
NewOTLPExporter
NewOTLPExporter creates a new OTLPExporter with the given options.
Parameters
Returns
func NewOTLPExporter(opts ...OTLPOption) *OTLPExporter
{
e := &OTLPExporter{
endpoint: "http://localhost:4318/v1/traces",
client: &http.Client{Timeout: 5 * time.Second},
maxBatch: 100,
flushMs: 5000 * time.Millisecond,
stopCh: make(chan struct{}),
}
for _, opt := range opts {
opt(e)
}
go e.loop()
return e
}
PrometheusExporter
type PrometheusExporter struct
Methods
Parameters
func (*PrometheusExporter) IncCounter(name string, delta int64)
{
p.mu.Lock()
defer p.mu.Unlock()
p.counters[name] += delta
}
Parameters
func (*PrometheusExporter) SetGauge(name string, value float64)
{
p.mu.Lock()
defer p.mu.Unlock()
p.gauges[name] = value
}
Parameters
func (*PrometheusExporter) ObserveHistogram(name string, value float64, buckets []float64)
{
p.mu.Lock()
defer p.mu.Unlock()
h, ok := p.histos[name]
if !ok {
h = &promHistogram{buckets: make(map[float64]int64), sum: 0, count: 0}
for _, b := range buckets {
h.buckets[b] = 0
}
p.histos[name] = h
}
h.count++
h.sum += value
for _, b := range buckets {
if value <= b {
h.buckets[b]++
}
}
}
Parameters
func (*PrometheusExporter) WriteText(w io.Writer)
{
p.mu.RLock()
defer p.mu.RUnlock()
for name, val := range p.counters {
fmt.Fprintf(w, "# TYPE %s counter\n%s %d\n", name, name, val)
}
for name, val := range p.gauges {
fmt.Fprintf(w, "# TYPE %s gauge\n%s %g\n", name, name, val)
}
for name, h := range p.histos {
fmt.Fprintf(w, "# TYPE %s histogram\n", name)
for _, b := range sortedKeys(h.buckets) {
fmt.Fprintf(w, "%s_bucket{le=\"%g\"} %d\n", name, b, h.buckets[b])
}
fmt.Fprintf(w, "%s_bucket{le=\"+Inf\"} %d\n", name, h.count)
fmt.Fprintf(w, "%s_sum %g\n", name, h.sum)
fmt.Fprintf(w, "%s_count %d\n", name, h.count)
}
}
Fields
| Name | Type | Description |
|---|---|---|
| mu | sync.RWMutex | |
| counters | map[string]int64 | |
| gauges | map[string]float64 | |
| histos | map[string]*promHistogram |
promHistogram
type promHistogram struct
Fields
| Name | Type | Description |
|---|---|---|
| buckets | map[float64]int64 | |
| sum | float64 | |
| count | int64 |
NewPrometheusExporter
NewPrometheusExporter creates a new PrometheusExporter.
Returns
func NewPrometheusExporter() *PrometheusExporter
{
return &PrometheusExporter{
counters: make(map[string]int64),
gauges: make(map[string]float64),
histos: make(map[string]*promHistogram),
}
}
sortedKeys
Parameters
Returns
func sortedKeys(m map[float64]int64) []float64
{
keys := make([]float64, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sortFloat64s(keys)
return keys
}
sortFloat64s
Parameters
func sortFloat64s(a []float64)
{
for i := 1; i < len(a); i++ {
for j := i; j > 0 && a[j] < a[j-1]; j-- {
a[j], a[j-1] = a[j-1], a[j]
}
}
}
TraceContext
type TraceContext struct
Methods
Returns
func (*TraceContext) Encode() string
{
return fmt.Sprintf("00-%s-%s-%s", tc.TraceID, tc.ParentID, tc.TraceFlags)
}
Fields
| Name | Type | Description |
|---|---|---|
| TraceID | string | |
| ParentID | string | |
| TraceFlags | string |
ParseTraceparent
ParseTraceparent parses a W3C traceparent header.
Parameters
Returns
func ParseTraceparent(header string) (*TraceContext, error)
{
parts := strings.Split(header, "-")
if len(parts) != 4 {
return nil, fmt.Errorf("invalid traceparent: expected 4 parts, got %d", len(parts))
}
if parts[0] != "00" {
return nil, fmt.Errorf("unsupported traceparent version: %s", parts[0])
}
tc := &TraceContext{
TraceID: parts[1],
ParentID: parts[2],
TraceFlags: parts[3],
}
if len(tc.TraceID) != 32 {
return nil, fmt.Errorf("invalid trace ID length: %d", len(tc.TraceID))
}
if len(tc.ParentID) != 16 {
return nil, fmt.Errorf("invalid parent ID length: %d", len(tc.ParentID))
}
return tc, nil
}
TelemetryMiddleware
type TelemetryMiddleware struct
Methods
SrvMiddleware returns a middleware function compatible with the srv package. It accepts a HandlerFunc type to avoid circular import - use via adapter.
Parameters
Returns
func (*TelemetryMiddleware) WrapHTTP(next http.Handler) http.Handler
{
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
traceCtx, _ := ParseTraceparent(r.Header.Get("traceparent"))
var span Span
ctx := r.Context()
if tm.Provider != nil && tm.Provider.Tracer != nil {
attrs := []Attribute{
{Key: "http.method", Value: r.Method},
{Key: "http.url", Value: r.URL.String()},
}
if traceCtx != nil {
attrs = append(attrs,
Attribute{Key: "trace.parent_id", Value: traceCtx.ParentID},
)
}
span, ctx = tm.Provider.Tracer.Start(ctx, r.Method+" "+r.URL.Path, attrs...)
}
rw := &telemetryResponseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(rw, r.WithContext(ctx))
if span != nil {
span.SetAttributes(
Attribute{Key: "http.status_code", Value: rw.statusCode},
Attribute{Key: "http.duration_ms", Value: time.Since(start).Milliseconds()},
)
span.End()
}
if tm.Provider != nil && tm.Provider.Meter != nil {
counter := tm.Provider.Meter.Counter("http_requests_total",
Attribute{Key: "method", Value: r.Method},
Attribute{Key: "path", Value: r.URL.Path},
)
counter.Add(ctx, 1)
hist := tm.Provider.Meter.Histogram("http_request_duration_ms",
Attribute{Key: "method", Value: r.Method},
)
hist.Record(ctx, float64(time.Since(start).Milliseconds()))
}
if traceCtx != nil {
w.Header().Set("traceparent", traceCtx.Encode())
}
})
}
Fields
| Name | Type | Description |
|---|---|---|
| Provider | *Provider |
NewTelemetryMiddleware
NewTelemetryMiddleware creates a new TelemetryMiddleware wrapping the given provider.
Parameters
Returns
func NewTelemetryMiddleware(provider *Provider) *TelemetryMiddleware
{
return &TelemetryMiddleware{Provider: provider}
}
telemetryResponseWriter
type telemetryResponseWriter struct
Methods
Parameters
func (*telemetryResponseWriter) WriteHeader(code int)
{
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
Fields
| Name | Type | Description |
|---|---|---|
| statusCode | int |
Tracer
Tracer creates and manages spans.
type Tracer interface
Methods
Span
Span represents an active span in a trace.
type Span interface
Methods
func End(...)
Meter
Meter creates and records metrics.
type Meter interface
Counter
Counter records monotonically increasing values.
type Counter interface
Methods
Histogram
Histogram records distribution of values.
type Histogram interface
Methods
Gauge
Gauge records current values.
type Gauge interface
Methods
Attribute
Attribute is a key-value pair for spans and metrics.
type Attribute struct
Fields
| Name | Type | Description |
|---|---|---|
| Key | string | |
| Value | any |
Provider
Provider is the central telemetry hub.
type Provider struct
Methods
Shutdown cleans up all providers.
func (*Provider) Shutdown()
{
for _, f := range p.shutdown {
f()
}
}
Option
Option configures a Provider.
type Option options.Option[Provider]
NewProvider
NewProvider creates a telemetry provider with noop defaults.
Parameters
Returns
func NewProvider(opts ...Option) *Provider
{
p := &Provider{
Tracer: noopTracerInst,
Meter: noopMeterInst,
}
for _, opt := range opts {
opt(p)
}
return p
}
WithTracer
WithTracer sets the tracer implementation.
func WithTracer(t Tracer) Option
{
return func(p *Provider) { p.Tracer = t }
}
WithMeter
WithMeter sets the meter implementation.
func WithMeter(m Meter) Option
{
return func(p *Provider) { p.Meter = m }
}
Timed
Timed measures and records the duration of fn as a metric.
Parameters
Returns
func Timed(ctx context.Context, h Histogram, fn func()) time.Duration
{
start := time.Now()
fn()
dur := time.Since(start)
h.Record(ctx, dur.Seconds())
return dur
}
noopTracer
type noopTracer struct
Methods
Parameters
Returns
func (*noopTracer) Start(ctx context.Context, name string, attrs ...Attribute) (Span, context.Context)
{
return &noopSpan{}, ctx
}
noopSpan
type noopSpan struct
Methods
Parameters
func (*noopSpan) SetAttributes(attrs ...Attribute)
{}
func (*noopSpan) End()
{}
noopMeter
type noopMeter struct
Methods
Parameters
Returns
func (*noopMeter) Counter(name string, attrs ...Attribute) Counter
{ return &noopCounter{} }
Parameters
Returns
func (*noopMeter) Histogram(name string, attrs ...Attribute) Histogram
{ return &noopHistogram{} }
noopCounter
type noopCounter struct
Methods
Parameters
func (*noopCounter) Add(ctx context.Context, delta int64)
{}
noopHistogram
type noopHistogram struct
Methods
Parameters
func (*noopHistogram) Record(ctx context.Context, value float64)
{}
noopGauge
type noopGauge struct
Methods
Parameters
func (*noopGauge) Set(ctx context.Context, value float64)
{}
SimpleTracer
SimpleTracer is a basic tracer that logs span starts and ends to stderr.
type SimpleTracer struct
Methods
Parameters
Returns
func (*SimpleTracer) Start(ctx context.Context, name string, attrs ...Attribute) (Span, context.Context)
{
span := &simpleSpan{name: name, start: time.Now(), attrs: attrs}
return span, ctx
}
Fields
| Name | Type | Description |
|---|---|---|
| mu | sync.Mutex |
NewSimpleTracer
NewSimpleTracer creates a SimpleTracer.
Returns
func NewSimpleTracer() *SimpleTracer
{ return &SimpleTracer{} }
simpleSpan
type simpleSpan struct
Methods
Parameters
func (*simpleSpan) SetAttributes(attrs ...Attribute)
{
s.attrs = append(s.attrs, attrs...)
}
func (*simpleSpan) End()
{
fmt.Printf("[TRACE] %s duration=%v attrs=%v\n", s.name, time.Since(s.start), s.attrs)
}
SimpleMeter
SimpleMeter is a basic meter that stores counters in memory.
type SimpleMeter struct
Methods
Parameters
Returns
func (*SimpleMeter) Counter(name string, attrs ...Attribute) Counter
{
return &simpleCounter{meter: m, name: name}
}
Parameters
Returns
func (*SimpleMeter) Histogram(name string, attrs ...Attribute) Histogram
{
return &simpleHistogram{meter: m, name: name}
}
Parameters
Returns
func (*SimpleMeter) Gauge(name string, attrs ...Attribute) Gauge
{
return &simpleGauge{meter: m, name: name}
}
GetCounter returns the current counter value by name.
Parameters
Returns
func (*SimpleMeter) GetCounter(name string) int64
{
m.mu.Lock()
defer m.mu.Unlock()
return m.counters[name]
}
GetGauge returns the current gauge value by name.
Parameters
Returns
func (*SimpleMeter) GetGauge(name string) float64
{
m.mu.Lock()
defer m.mu.Unlock()
return m.gauges[name]
}
Fields
| Name | Type | Description |
|---|---|---|
| mu | sync.Mutex | |
| counters | map[string]int64 | |
| histogram | map[string][]float64 | |
| gauges | map[string]float64 |
NewSimpleMeter
NewSimpleMeter creates a SimpleMeter.
Returns
func NewSimpleMeter() *SimpleMeter
{
return &SimpleMeter{
counters: make(map[string]int64),
histogram: make(map[string][]float64),
gauges: make(map[string]float64),
}
}
simpleCounter
type simpleCounter struct
Methods
Parameters
func (*simpleCounter) Add(ctx context.Context, delta int64)
{
c.meter.mu.Lock()
c.meter.counters[c.name] += delta
c.meter.mu.Unlock()
}
Fields
| Name | Type | Description |
|---|---|---|
| meter | *SimpleMeter | |
| name | string |
simpleHistogram
type simpleHistogram struct
Methods
Parameters
func (*simpleHistogram) Record(ctx context.Context, value float64)
{
h.meter.mu.Lock()
h.meter.histogram[h.name] = append(h.meter.histogram[h.name], value)
h.meter.mu.Unlock()
}
Fields
| Name | Type | Description |
|---|---|---|
| meter | *SimpleMeter | |
| name | string |
simpleGauge
type simpleGauge struct
Methods
Parameters
func (*simpleGauge) Set(ctx context.Context, value float64)
{
g.meter.mu.Lock()
g.meter.gauges[g.name] = value
g.meter.mu.Unlock()
}
Fields
| Name | Type | Description |
|---|---|---|
| meter | *SimpleMeter | |
| name | string |
TestProvider_NoopDefault
Parameters
func TestProvider_NoopDefault(t *testing.T)
{
p := NewProvider()
if p.Tracer == nil {
t.Error("Tracer should not be nil (noop)")
}
if p.Meter == nil {
t.Error("Meter should not be nil (noop)")
}
span, ctx := p.Tracer.Start(context.Background(), "test")
span.SetAttributes(Attribute{Key: "k", Value: "v"})
span.End()
p.Meter.Counter("c").Add(ctx, 1)
p.Meter.Histogram("h").Record(ctx, 3.14)
p.Meter.Gauge("g").Set(ctx, 42)
}
TestSimpleTracer
Parameters
func TestSimpleTracer(t *testing.T)
{
tracer := NewSimpleTracer()
span, _ := tracer.Start(context.Background(), "operation")
span.End()
}
TestSimpleMeter
Parameters
func TestSimpleMeter(t *testing.T)
{
meter := NewSimpleMeter()
ctx := context.Background()
counter := meter.Counter("requests")
counter.Add(ctx, 5)
counter.Add(ctx, 3)
if v := meter.GetCounter("requests"); v != 8 {
t.Errorf("counter: got %d, want 8", v)
}
gauge := meter.Gauge("cpu")
gauge.Set(ctx, 75.5)
if v := meter.GetGauge("cpu"); v != 75.5 {
t.Errorf("gauge: got %f, want 75.5", v)
}
}
TestTimed
Parameters
func TestTimed(t *testing.T)
{
meter := NewSimpleMeter()
h := meter.Histogram("duration")
ctx := context.Background()
called := false
dur := Timed(ctx, h, func() {
called = true
})
if !called {
t.Error("Timed should call fn")
}
if dur <= 0 {
t.Error("Timed should return positive duration")
}
}
TestProvider_WithTracer
Parameters
func TestProvider_WithTracer(t *testing.T)
{
custom := NewSimpleTracer()
p := NewProvider(WithTracer(custom))
if p.Tracer != custom {
t.Error("WithTracer should set custom tracer")
}
}
TestProvider_Shutdown
Parameters
func TestProvider_Shutdown(t *testing.T)
{
called := false
p := NewProvider()
p.shutdown = append(p.shutdown, func() { called = true })
p.Shutdown()
if !called {
t.Error("Shutdown should call cleanup functions")
}
}
TestOTLPExporter_ExportFlush
Parameters
func TestOTLPExporter_ExportFlush(t *testing.T)
{
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
e := NewOTLPExporter(WithOTLPBatchSize(100))
defer e.Close()
e.endpoint = server.URL + "/v1/traces"
e.ExportSpan(otlpSpan{
TraceID: "0af7651916cd43dd8448eb211c80319c",
SpanID: "b7ad6b7169203331",
Name: "test-span",
})
time.Sleep(100 * time.Millisecond)
e.ExportMetric(otlpMetric{
Name: "http_requests",
Kind: "COUNTER",
Value: 42,
})
err := e.Flush(context.Background())
if err != nil {
t.Errorf("Flush: %v", err)
}
}
TestPrometheusExporter_CounterGauge
Parameters
func TestPrometheusExporter_CounterGauge(t *testing.T)
{
p := NewPrometheusExporter()
p.IncCounter("http_requests_total", 5)
p.IncCounter("http_requests_total", 3)
p.SetGauge("cpu_usage", 75.5)
var buf bytes.Buffer
p.WriteText(&buf)
output := buf.String()
if !strings.Contains(output, "http_requests_total 8") {
t.Errorf("expected counter in output, got: %s", output)
}
if !strings.Contains(output, "cpu_usage 75.5") {
t.Errorf("expected gauge in output, got: %s", output)
}
}
TestPrometheusExporter_Histogram
Parameters
func TestPrometheusExporter_Histogram(t *testing.T)
{
p := NewPrometheusExporter()
buckets := []float64{0.1, 0.5, 1.0}
p.ObserveHistogram("request_duration", 0.05, buckets)
p.ObserveHistogram("request_duration", 0.25, buckets)
p.ObserveHistogram("request_duration", 0.75, buckets)
p.ObserveHistogram("request_duration", 1.5, buckets)
var buf bytes.Buffer
p.WriteText(&buf)
output := buf.String()
if !strings.Contains(output, "request_duration_bucket") {
t.Errorf("expected histogram buckets in output, got: %s", output)
}
if !strings.Contains(output, "request_duration_sum") {
t.Errorf("expected histogram sum in output, got: %s", output)
}
}
TestTraceparent
Parameters
func TestTraceparent(t *testing.T)
{
tc, err := ParseTraceparent("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")
if err != nil {
t.Fatalf("ParseTraceparent: %v", err)
}
if tc.TraceID != "0af7651916cd43dd8448eb211c80319c" {
t.Errorf("TraceID = %q, want full trace ID", tc.TraceID)
}
if tc.ParentID != "b7ad6b7169203331" {
t.Errorf("ParentID = %q", tc.ParentID)
}
if tc.TraceFlags != "01" {
t.Errorf("TraceFlags = %q", tc.TraceFlags)
}
encoded := tc.Encode()
if encoded != "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" {
t.Errorf("Encode = %q", encoded)
}
}
TestTraceparent_Invalid
Parameters
func TestTraceparent_Invalid(t *testing.T)
{
_, err := ParseTraceparent("invalid")
if err == nil {
t.Error("expected error for invalid traceparent")
}
_, err = ParseTraceparent("01-abc-def-01")
if err == nil {
t.Error("expected error for unsupported version")
}
}
TestTelemetryMiddleware_WrapHTTP
Parameters
func TestTelemetryMiddleware_WrapHTTP(t *testing.T)
{
meter := NewSimpleMeter()
provider := NewProvider(WithMeter(meter))
mw := NewTelemetryMiddleware(provider)
handler := mw.WrapHTTP(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}))
req := httptest.NewRequest("GET", "/test", nil)
req.Header.Set("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("status = %d, want 200", w.Code)
}
if v := meter.GetCounter("http_requests_total"); v == 0 {
t.Error("expected counter to be incremented")
}
}