telemetry API

telemetry

package

API reference for the telemetry package.

S
struct

OTLPExporter

OTLPExporter sends spans and metrics to an OTLP-compatible endpoint over HTTP/JSON.

core/telemetry/exporters.go:22-41
type OTLPExporter struct

Methods

ExportSpan
Method

ExportSpan queues a span and flushes when the batch threshold is reached.

Parameters

span OTLPSpan

Returns

error
func (*OTLPExporter) ExportSpan(span OTLPSpan) error
{
	if err := validateOTLPSpan(span); err != nil {
		return err
	}
	e.batchMu.Lock()
	if e.closed {
		e.batchMu.Unlock()
		return ErrOTLPExporterClosed
	}
	if len(e.spans)+len(e.metrics)+e.inFlight >= e.maxQueue {
		e.batchMu.Unlock()
		return ErrOTLPQueueFull
	}
	e.exportWG.Add(1)
	defer e.exportWG.Done()
	e.spans = append(e.spans, span)
	shouldFlush := len(e.spans) >= e.maxBatch
	e.batchMu.Unlock()
	if shouldFlush {
		e.requestFlush()
	}
	return nil
}
ExportMetric
Method

ExportMetric queues a metric and flushes when the batch threshold is reached.

Parameters

metric OTLPMetric

Returns

error
func (*OTLPExporter) ExportMetric(metric OTLPMetric) error
{
	if err := validateOTLPMetric(metric); err != nil {
		return err
	}
	e.batchMu.Lock()
	if e.closed {
		e.batchMu.Unlock()
		return ErrOTLPExporterClosed
	}
	if len(e.spans)+len(e.metrics)+e.inFlight >= e.maxQueue {
		e.batchMu.Unlock()
		return ErrOTLPQueueFull
	}
	e.exportWG.Add(1)
	defer e.exportWG.Done()
	e.metrics = append(e.metrics, metric)
	shouldFlush := len(e.metrics) >= e.maxBatch
	e.batchMu.Unlock()
	if shouldFlush {
		e.requestFlush()
	}
	return nil
}
requestFlush
Method
func (*OTLPExporter) requestFlush()
{
	select {
	case e.flushCh <- struct{}{}:
	default:
	}
}
Flush
Method

Flush sends all currently queued telemetry.

Parameters

Returns

error
func (*OTLPExporter) Flush(ctx context.Context) error
{
	if ctx == nil {
		ctx = context.Background()
	}
	e.flushMu.Lock()
	defer e.flushMu.Unlock()
	e.batchMu.Lock()
	spans := e.spans
	metrics := e.metrics
	e.spans = nil
	e.metrics = nil
	batchSize := len(spans) + len(metrics)
	e.inFlight += batchSize
	e.batchMu.Unlock()

	if len(spans) == 0 && len(metrics) == 0 {
		return nil
	}
	spansSent := len(spans) == 0
	metricsSent := len(metrics) == 0
	defer func() {
		e.batchMu.Lock()
		if !spansSent {
			e.spans = append(spans, e.spans...)
		}
		if !metricsSent {
			e.metrics = append(metrics, e.metrics...)
		}
		e.inFlight -= batchSize
		e.batchMu.Unlock()
	}()

	if len(spans) > 0 {
		if err := e.send(ctx, "traces", otlpTracePayload(spans)); err != nil {
			var partial *otlpPartialSuccessError
			if errors.As(err, &partial) {
				spansSent = true
			}
			return err
		}
		spansSent = true
	}
	if len(metrics) > 0 {
		if err := e.send(ctx, "metrics", otlpMetricPayload(metrics)); err != nil {
			var partial *otlpPartialSuccessError
			if errors.As(err, &partial) {
				metricsSent = true
			}
			return err
		}
		metricsSent = true
	}
	return nil
}
send
Method

Parameters

signal string
payload any

Returns

error
func (*OTLPExporter) send(ctx context.Context, signal string, payload any) error
{
	data, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("otlp marshal %s: %w", signal, err)
	}
	endpoint, err := otlpSignalEndpoint(e.endpoint, signal)
	if err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data))
	if err != nil {
		return fmt.Errorf("otlp request %s: %w", signal, err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := e.client.Do(req)
	if err != nil {
		return fmt.Errorf("otlp send %s: %w", signal, err)
	}
	defer resp.Body.Close()
	const maxOTLPResponseSize = 4 << 20
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxOTLPResponseSize+1))
	if err != nil {
		return fmt.Errorf("otlp read %s response: %w", signal, err)
	}
	if len(body) > maxOTLPResponseSize {
		return fmt.Errorf("otlp %s response exceeds size limit", signal)
	}
	if resp.StatusCode >= 300 {
		return fmt.Errorf("otlp %s status %d: %s", signal, resp.StatusCode, string(body))
	}
	if len(bytes.TrimSpace(body)) > 0 {
		var response struct {
			PartialSuccess struct {
				RejectedSpans      string `json:"rejectedSpans"`
				RejectedDataPoints string `json:"rejectedDataPoints"`
				ErrorMessage       string `json:"errorMessage"`
			} `json:"partialSuccess"`
		}
		if err := json.Unmarshal(body, &response); err != nil {
			return fmt.Errorf("otlp decode %s response: %w", signal, err)
		}
		rejected := response.PartialSuccess.RejectedSpans
		if signal == "metrics" {
			rejected = response.PartialSuccess.RejectedDataPoints
		}
		if rejected != "" && rejected != "0" {
			return &otlpPartialSuccessError{
				signal:   signal,
				rejected: rejected,
				message:  response.PartialSuccess.ErrorMessage,
			}
		}
	}
	return nil
}
loop
Method
func (*OTLPExporter) loop()
{
	ticker := time.NewTicker(e.flushMs)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			_ = e.Flush(context.Background())
		case <-e.flushCh:
			if err := e.Flush(context.Background()); err != nil {
				select {
				case <-e.flushCh:
				default:
				}
			}
		case <-e.stopCh:
			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			err := e.Flush(ctx)
			cancel()
			e.closeMu.Lock()
			e.closeErr = err
			e.closeMu.Unlock()
			close(e.done)
			return
		}
	}
}
Close
Method

Returns

error
func (*OTLPExporter) Close() error
{
	e.stopOnce.Do(func() {
		e.batchMu.Lock()
		e.closed = true
		e.batchMu.Unlock()
		e.exportWG.Wait()
		close(e.stopCh)
	})
	<-e.done
	e.closeMu.Lock()
	defer e.closeMu.Unlock()
	return e.closeErr
}

Fields

Name Type Description
endpoint string
client *http.Client
batchMu sync.Mutex
exportWG sync.WaitGroup
flushMu sync.Mutex
spans []OTLPSpan
metrics []OTLPMetric
inFlight int
maxBatch int
maxQueue int
flushMs time.Duration
flushCh chan struct{}
stopCh chan struct{}
stopOnce sync.Once
done chan struct{}
closeMu sync.Mutex
closeErr error
closed bool
S
struct

OTLPSpan

OTLPSpan is a span accepted by OTLPExporter.

core/telemetry/exporters.go:44-51
type OTLPSpan struct

Fields

Name Type Description
TraceID string
SpanID string
Name string
StartTime time.Time
EndTime time.Time
Attributes map[string]any
T
type

OTLPMetricKind

OTLPMetricKind identifies the OTLP data-point representation.

core/telemetry/exporters.go:54-54
type OTLPMetricKind string
S
struct

OTLPMetric

OTLPMetric is a metric data point accepted by OTLPExporter.

core/telemetry/exporters.go:66-72
type OTLPMetric struct

Fields

Name Type Description
Name string
Kind OTLPMetricKind
Value float64
Time time.Time
Attributes map[string]any
T
type

OTLPOption

OTLPOption configures an OTLPExporter.

core/telemetry/exporters.go:82-82
type OTLPOption func(*OTLPExporter)
F
function

WithOTLPEndpoint

WithOTLPEndpoint sets the OTLP receiver base URL.

Parameters

url
string

Returns

core/telemetry/exporters.go:85-87
func WithOTLPEndpoint(url string) OTLPOption

{
	return func(e *OTLPExporter) { e.endpoint = url }
}
F
function

WithOTLPBatchSize

WithOTLPBatchSize sets the maximum batch size before flushing.

Parameters

n
int

Returns

core/telemetry/exporters.go:90-92
func WithOTLPBatchSize(n int) OTLPOption

{
	return func(e *OTLPExporter) { e.maxBatch = n }
}
F
function

WithOTLPQueueSize

WithOTLPQueueSize sets the maximum number of pending spans and metrics.

Parameters

n
int

Returns

core/telemetry/exporters.go:95-97
func WithOTLPQueueSize(n int) OTLPOption

{
	return func(e *OTLPExporter) { e.maxQueue = n }
}
F
function

NewOTLPExporter

NewOTLPExporter creates a new OTLPExporter with the given options.

Parameters

opts
...OTLPOption

Returns

core/telemetry/exporters.go:100-122
func NewOTLPExporter(opts ...OTLPOption) *OTLPExporter

{
	e := &OTLPExporter{
		endpoint: "http://localhost:4318",
		client:   &http.Client{Timeout: 5 * time.Second},
		maxBatch: 100,
		maxQueue: 10_000,
		flushMs:  5000 * time.Millisecond,
		flushCh:  make(chan struct{}, 1),
		stopCh:   make(chan struct{}),
		done:     make(chan struct{}),
	}
	for _, opt := range opts {
		opt(e)
	}
	if e.maxBatch <= 0 {
		panic("telemetry: OTLP batch size must be positive")
	}
	if e.maxQueue <= 0 {
		panic("telemetry: OTLP queue size must be positive")
	}
	go e.loop()
	return e
}
S
struct

otlpPartialSuccessError

core/telemetry/exporters.go:237-241
type otlpPartialSuccessError struct

Methods

Error
Method

Returns

string
func (*otlpPartialSuccessError) Error() string
{
	return fmt.Sprintf("otlp %s partial success rejected %s: %s", e.signal, e.rejected, e.message)
}

Fields

Name Type Description
signal string
rejected string
message string
F
function

otlpSignalEndpoint

Parameters

base
string
signal
string

Returns

string
error
core/telemetry/exporters.go:304-315
func otlpSignalEndpoint(base, signal string) (string, error)

{
	endpoint, err := url.Parse(base)
	if err != nil || endpoint.Scheme == "" || endpoint.Host == "" {
		return "", fmt.Errorf("otlp endpoint %q is invalid", base)
	}
	cleanPath := strings.TrimSuffix(endpoint.Path, "/")
	for _, suffix := range []string{"/v1/traces", "/v1/metrics"} {
		cleanPath = strings.TrimSuffix(cleanPath, suffix)
	}
	endpoint.Path = path.Join("/", cleanPath, "v1", signal)
	return endpoint.String(), nil
}
F
function

validateOTLPSpan

Parameters

span

Returns

error
core/telemetry/exporters.go:317-331
func validateOTLPSpan(span OTLPSpan) error

{
	if len(span.TraceID) != 32 || !isLowerHex(span.TraceID) || allZero(span.TraceID) {
		return errors.New("telemetry: OTLP span requires a non-zero 32-character lowercase hexadecimal trace ID")
	}
	if len(span.SpanID) != 16 || !isLowerHex(span.SpanID) || allZero(span.SpanID) {
		return errors.New("telemetry: OTLP span requires a non-zero 16-character lowercase hexadecimal span ID")
	}
	if span.Name == "" {
		return errors.New("telemetry: OTLP span name cannot be empty")
	}
	if span.StartTime.IsZero() || span.EndTime.IsZero() || span.EndTime.Before(span.StartTime) {
		return errors.New("telemetry: OTLP span requires a valid start and end time")
	}
	return nil
}
F
function

validateOTLPMetric

Parameters

metric

Returns

error
core/telemetry/exporters.go:333-343
func validateOTLPMetric(metric OTLPMetric) error

{
	if metric.Name == "" {
		return errors.New("telemetry: OTLP metric name cannot be empty")
	}
	switch metric.Kind {
	case OTLPMetricCounter, OTLPMetricGauge, OTLPMetricHistogram:
	default:
		return fmt.Errorf("telemetry: unsupported OTLP metric kind %q", metric.Kind)
	}
	return nil
}
F
function

otlpTracePayload

Parameters

spans

Returns

any
core/telemetry/exporters.go:345-367
func otlpTracePayload(spans []OTLPSpan) any

{
	items := make([]map[string]any, 0, len(spans))
	for _, span := range spans {
		items = append(items, map[string]any{
			"traceId":           span.TraceID,
			"spanId":            span.SpanID,
			"name":              span.Name,
			"kind":              1,
			"startTimeUnixNano": strconv.FormatInt(span.StartTime.UnixNano(), 10),
			"endTimeUnixNano":   strconv.FormatInt(span.EndTime.UnixNano(), 10),
			"attributes":        otlpAttributes(span.Attributes),
		})
	}
	return map[string]any{
		"resourceSpans": []any{
			map[string]any{
				"scopeSpans": []any{
					map[string]any{"spans": items},
				},
			},
		},
	}
}
F
function

otlpMetricPayload

Parameters

metrics

Returns

any
core/telemetry/exporters.go:369-415
func otlpMetricPayload(metrics []OTLPMetric) any

{
	items := make([]map[string]any, 0, len(metrics))
	for _, metric := range metrics {
		timestamp := metric.Time
		if timestamp.IsZero() {
			timestamp = time.Now()
		}
		point := map[string]any{
			"timeUnixNano": strconv.FormatInt(timestamp.UnixNano(), 10),
			"asDouble":     metric.Value,
			"attributes":   otlpAttributes(metric.Attributes),
		}
		item := map[string]any{"name": metric.Name}
		switch metric.Kind {
		case OTLPMetricCounter:
			item["sum"] = map[string]any{
				"aggregationTemporality": 2,
				"isMonotonic":            true,
				"dataPoints":             []any{point},
			}
		case OTLPMetricGauge:
			item["gauge"] = map[string]any{"dataPoints": []any{point}}
		case OTLPMetricHistogram:
			delete(point, "asDouble")
			point["count"] = "1"
			point["sum"] = metric.Value
			point["min"] = metric.Value
			point["max"] = metric.Value
			point["bucketCounts"] = []string{"1"}
			point["explicitBounds"] = []float64{}
			item["histogram"] = map[string]any{
				"aggregationTemporality": 2,
				"dataPoints":             []any{point},
			}
		}
		items = append(items, item)
	}
	return map[string]any{
		"resourceMetrics": []any{
			map[string]any{
				"scopeMetrics": []any{
					map[string]any{"metrics": items},
				},
			},
		},
	}
}
F
function

otlpAttributes

Parameters

attributes
map[string]any

Returns

[]map[string]any
core/telemetry/exporters.go:417-426
func otlpAttributes(attributes map[string]any) []map[string]any

{
	result := make([]map[string]any, 0, len(attributes))
	for key, value := range attributes {
		result = append(result, map[string]any{
			"key":   key,
			"value": otlpAnyValue(value),
		})
	}
	return result
}
F
function

otlpAnyValue

Parameters

value
any

Returns

map[string]any
core/telemetry/exporters.go:428-461
func otlpAnyValue(value any) map[string]any

{
	switch typed := value.(type) {
	case bool:
		return map[string]any{"boolValue": typed}
	case int:
		return map[string]any{"intValue": strconv.FormatInt(int64(typed), 10)}
	case int8:
		return map[string]any{"intValue": strconv.FormatInt(int64(typed), 10)}
	case int16:
		return map[string]any{"intValue": strconv.FormatInt(int64(typed), 10)}
	case int32:
		return map[string]any{"intValue": strconv.FormatInt(int64(typed), 10)}
	case int64:
		return map[string]any{"intValue": strconv.FormatInt(typed, 10)}
	case uint:
		return map[string]any{"intValue": strconv.FormatUint(uint64(typed), 10)}
	case uint8:
		return map[string]any{"intValue": strconv.FormatUint(uint64(typed), 10)}
	case uint16:
		return map[string]any{"intValue": strconv.FormatUint(uint64(typed), 10)}
	case uint32:
		return map[string]any{"intValue": strconv.FormatUint(uint64(typed), 10)}
	case uint64:
		return map[string]any{"intValue": strconv.FormatUint(typed, 10)}
	case float32:
		return map[string]any{"doubleValue": float64(typed)}
	case float64:
		return map[string]any{"doubleValue": typed}
	case string:
		return map[string]any{"stringValue": typed}
	default:
		return map[string]any{"stringValue": fmt.Sprint(typed)}
	}
}
S
struct

PrometheusExporter

core/telemetry/exporters.go:506-511
type PrometheusExporter struct

Methods

IncCounter
Method

Parameters

name string
delta int64
func (*PrometheusExporter) IncCounter(name string, delta int64)
{
	p.mu.Lock()
	defer p.mu.Unlock()
	p.counters[name] += delta
}
SetGauge
Method

Parameters

name string
value float64
func (*PrometheusExporter) SetGauge(name string, value float64)
{
	p.mu.Lock()
	defer p.mu.Unlock()
	p.gauges[name] = value
}

Parameters

name string
value float64
buckets []float64
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]++
		}
	}
}
WriteText
Method

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
S
struct

promHistogram

core/telemetry/exporters.go:513-517
type promHistogram struct

Fields

Name Type Description
buckets map[float64]int64
sum float64
count int64
F
function

NewPrometheusExporter

NewPrometheusExporter creates a new PrometheusExporter.

core/telemetry/exporters.go:520-526
func NewPrometheusExporter() *PrometheusExporter

{
	return &PrometheusExporter{
		counters: make(map[string]int64),
		gauges:   make(map[string]float64),
		histos:   make(map[string]*promHistogram),
	}
}
F
function

sortedKeys

Parameters

m
map[float64]int64

Returns

[]float64
core/telemetry/exporters.go:581-588
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
}
F
function

sortFloat64s

Parameters

a
[]float64
core/telemetry/exporters.go:590-596
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]
		}
	}
}
S
struct

TraceContext

core/telemetry/exporters.go:600-604
type TraceContext struct

Methods

Encode
Method

Returns

string
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
F
function

ParseTraceparent

ParseTraceparent parses a W3C traceparent header.

Parameters

header
string

Returns

error
core/telemetry/exporters.go:607-636
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))
	}
	if len(tc.TraceFlags) != 2 {
		return nil, fmt.Errorf("invalid trace flags length: %d", len(tc.TraceFlags))
	}
	if !isLowerHex(tc.TraceID) || !isLowerHex(tc.ParentID) || !isLowerHex(tc.TraceFlags) {
		return nil, fmt.Errorf("traceparent IDs and flags must use lowercase hexadecimal")
	}
	if allZero(tc.TraceID) || allZero(tc.ParentID) {
		return nil, fmt.Errorf("traceparent trace and parent IDs cannot be all zero")
	}
	return tc, nil
}
F
function

isLowerHex

Parameters

value
string

Returns

bool
core/telemetry/exporters.go:638-647
func isLowerHex(value string) bool

{
	for _, character := range value {
		if character < '0' || character > '9' {
			if character < 'a' || character > 'f' {
				return false
			}
		}
	}
	return true
}
F
function

allZero

Parameters

value
string

Returns

bool
core/telemetry/exporters.go:649-651
func allZero(value string) bool

{
	return strings.Trim(value, "0") == ""
}
S
struct

TelemetryMiddleware

core/telemetry/exporters.go:659-661
type TelemetryMiddleware struct

Methods

WrapHTTP
Method

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: telemetryURL(r)},
			}
			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}

		if traceCtx != nil {
			w.Header().Set("traceparent", traceCtx.Encode())
		}
		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()))
		}

	})
}

Fields

Name Type Description
Provider *Provider
F
function

NewTelemetryMiddleware

NewTelemetryMiddleware creates a new TelemetryMiddleware wrapping the given provider.

Parameters

provider
core/telemetry/exporters.go:664-666
func NewTelemetryMiddleware(provider *Provider) *TelemetryMiddleware

{
	return &TelemetryMiddleware{Provider: provider}
}
F
function

telemetryURL

Parameters

Returns

string
core/telemetry/exporters.go:721-731
func telemetryURL(r *http.Request) string

{
	if r == nil || r.URL == nil {
		return ""
	}
	safe := *r.URL
	safe.User = nil
	safe.RawQuery = ""
	safe.ForceQuery = false
	safe.Fragment = ""
	return safe.String()
}
S
struct

telemetryResponseWriter

core/telemetry/exporters.go:733-737
type telemetryResponseWriter struct

Methods

WriteHeader
Method

Parameters

code int
func (*telemetryResponseWriter) WriteHeader(code int)
{
	if w.wroteHeader {
		return
	}
	w.wroteHeader = true
	w.statusCode = code
	w.ResponseWriter.WriteHeader(code)
}
Write
Method

Parameters

data []byte

Returns

int
error
func (*telemetryResponseWriter) Write(data []byte) (int, error)
{
	if !w.wroteHeader {
		w.WriteHeader(http.StatusOK)
	}
	return w.ResponseWriter.Write(data)
}
Unwrap
Method
func (*telemetryResponseWriter) Unwrap() http.ResponseWriter
{
	return w.ResponseWriter
}
Flush
Method
func (*telemetryResponseWriter) Flush()
{
	if !w.wroteHeader {
		w.WriteHeader(http.StatusOK)
	}
	if flusher, ok := w.ResponseWriter.(http.Flusher); ok {
		flusher.Flush()
	}
}
Hijack
Method

Returns

func (*telemetryResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)
{
	hijacker, ok := w.ResponseWriter.(http.Hijacker)
	if !ok {
		return nil, nil, errors.New("telemetry: response writer does not support hijacking")
	}
	return hijacker.Hijack()
}
Push
Method

Parameters

target string

Returns

error
func (*telemetryResponseWriter) Push(target string, opts *http.PushOptions) error
{
	pusher, ok := w.ResponseWriter.(http.Pusher)
	if !ok {
		return http.ErrNotSupported
	}
	return pusher.Push(target, opts)
}
ReadFrom
Method

Parameters

reader io.Reader

Returns

int64
error
func (*telemetryResponseWriter) ReadFrom(reader io.Reader) (int64, error)
{
	if !w.wroteHeader {
		w.WriteHeader(http.StatusOK)
	}
	if readerFrom, ok := w.ResponseWriter.(io.ReaderFrom); ok {
		return readerFrom.ReadFrom(reader)
	}
	return io.Copy(w.ResponseWriter, reader)
}

Fields

Name Type Description
statusCode int
wroteHeader bool
I
interface

Tracer

Tracer creates and manages spans.

core/telemetry/telemetry.go:13-15
type Tracer interface

Methods

Start
Method

Parameters

name string
attrs ...Attribute
func Start(...)
I
interface

Span

Span represents an active span in a trace.

core/telemetry/telemetry.go:18-21
type Span interface

Methods

SetAttributes
Method

Parameters

attrs ...Attribute
func SetAttributes(...)
End
Method
func End(...)
I
interface

Meter

Meter creates and records metrics.

core/telemetry/telemetry.go:24-28
type Meter interface

Methods

Counter
Method

Parameters

name string
attrs ...Attribute

Returns

func Counter(...)
Histogram
Method

Parameters

name string
attrs ...Attribute

Returns

func Histogram(...)
Gauge
Method

Parameters

name string
attrs ...Attribute

Returns

func Gauge(...)
I
interface

Counter

Counter records monotonically increasing values.

core/telemetry/telemetry.go:31-33
type Counter interface

Methods

Add
Method

Parameters

delta int64
func Add(...)
I
interface

Histogram

Histogram records distribution of values.

core/telemetry/telemetry.go:36-38
type Histogram interface

Methods

Record
Method

Parameters

value float64
func Record(...)
I
interface

Gauge

Gauge records current values.

core/telemetry/telemetry.go:41-43
type Gauge interface

Methods

Set
Method

Parameters

value float64
func Set(...)
S
struct

Attribute

Attribute is a key-value pair for spans and metrics.

core/telemetry/telemetry.go:46-49
type Attribute struct

Fields

Name Type Description
Key string
Value any
S
struct

Provider

Provider is the central telemetry hub.

core/telemetry/telemetry.go:52-56
type Provider struct

Methods

Shutdown
Method

Shutdown cleans up all providers.

func (*Provider) Shutdown()
{
	for _, f := range p.shutdown {
		f()
	}
}

Fields

Name Type Description
Tracer Tracer
Meter Meter
shutdown []func()
T
type

Option

Option configures a Provider.

core/telemetry/telemetry.go:59-59
type Option options.Option[Provider]
F
function

NewProvider

NewProvider creates a telemetry provider with noop defaults.

Parameters

opts
...Option

Returns

core/telemetry/telemetry.go:62-71
func NewProvider(opts ...Option) *Provider

{
	p := &Provider{
		Tracer: noopTracerInst,
		Meter:  noopMeterInst,
	}
	for _, opt := range opts {
		opt(p)
	}
	return p
}
F
function

WithTracer

WithTracer sets the tracer implementation.

Parameters

t

Returns

core/telemetry/telemetry.go:74-76
func WithTracer(t Tracer) Option

{
	return func(p *Provider) { p.Tracer = t }
}
F
function

WithMeter

WithMeter sets the meter implementation.

Parameters

m

Returns

core/telemetry/telemetry.go:79-81
func WithMeter(m Meter) Option

{
	return func(p *Provider) { p.Meter = m }
}
F
function

Timed

Timed measures and records the duration of fn as a metric.

Parameters

fn
func()

Returns

core/telemetry/telemetry.go:91-97
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
}
S
struct
Implements: Tracer

noopTracer

core/telemetry/telemetry.go:104-104
type noopTracer struct

Methods

Start
Method

Parameters

name string
attrs ...Attribute
func (*noopTracer) Start(ctx context.Context, name string, attrs ...Attribute) (Span, context.Context)
{
	return &noopSpan{}, ctx
}
S
struct
Implements: Span

noopSpan

core/telemetry/telemetry.go:110-110
type noopSpan struct

Methods

SetAttributes
Method

Parameters

attrs ...Attribute
func (*noopSpan) SetAttributes(attrs ...Attribute)
{}
End
Method
func (*noopSpan) End()
{}
S
struct
Implements: Meter

noopMeter

core/telemetry/telemetry.go:115-115
type noopMeter struct

Methods

Counter
Method

Parameters

name string
attrs ...Attribute

Returns

func (*noopMeter) Counter(name string, attrs ...Attribute) Counter
{ return &noopCounter{} }
Histogram
Method

Parameters

name string
attrs ...Attribute

Returns

func (*noopMeter) Histogram(name string, attrs ...Attribute) Histogram
{ return &noopHistogram{} }
Gauge
Method

Parameters

name string
attrs ...Attribute

Returns

func (*noopMeter) Gauge(name string, attrs ...Attribute) Gauge
{ return &noopGauge{} }
S
struct
Implements: Counter

noopCounter

core/telemetry/telemetry.go:121-121
type noopCounter struct

Methods

Add
Method

Parameters

delta int64
func (*noopCounter) Add(ctx context.Context, delta int64)
{}
S
struct
Implements: Histogram

noopHistogram

core/telemetry/telemetry.go:125-125
type noopHistogram struct

Methods

Record
Method

Parameters

value float64
func (*noopHistogram) Record(ctx context.Context, value float64)
{}
S
struct
Implements: Gauge

noopGauge

core/telemetry/telemetry.go:129-129
type noopGauge struct

Methods

Set
Method

Parameters

value float64
func (*noopGauge) Set(ctx context.Context, value float64)
{}
S
struct
Implements: Tracer

SimpleTracer

SimpleTracer is a basic tracer that logs span starts and ends to stderr.

core/telemetry/telemetry.go:134-136
type SimpleTracer struct

Methods

Start
Method

Parameters

name string
attrs ...Attribute
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
F
function

NewSimpleTracer

NewSimpleTracer creates a SimpleTracer.

Returns

core/telemetry/telemetry.go:139-139
func NewSimpleTracer() *SimpleTracer

{ return &SimpleTracer{} }
S
struct
Implements: Span

simpleSpan

core/telemetry/telemetry.go:146-150
type simpleSpan struct

Methods

SetAttributes
Method

Parameters

attrs ...Attribute
func (*simpleSpan) SetAttributes(attrs ...Attribute)
{
	s.attrs = append(s.attrs, attrs...)
}
End
Method
func (*simpleSpan) End()
{
	fmt.Printf("[TRACE] %s duration=%v attrs=%v\n", s.name, time.Since(s.start), s.attrs)
}

Fields

Name Type Description
name string
start time.Time
attrs []Attribute
S
struct
Implements: Meter

SimpleMeter

SimpleMeter is a basic meter that stores counters in memory.

core/telemetry/telemetry.go:161-166
type SimpleMeter struct

Methods

Counter
Method

Parameters

name string
attrs ...Attribute

Returns

func (*SimpleMeter) Counter(name string, attrs ...Attribute) Counter
{
	return &simpleCounter{meter: m, name: name}
}
Histogram
Method

Parameters

name string
attrs ...Attribute

Returns

func (*SimpleMeter) Histogram(name string, attrs ...Attribute) Histogram
{
	return &simpleHistogram{meter: m, name: name}
}
Gauge
Method

Parameters

name string
attrs ...Attribute

Returns

func (*SimpleMeter) Gauge(name string, attrs ...Attribute) Gauge
{
	return &simpleGauge{meter: m, name: name}
}
GetCounter
Method

GetCounter returns the current counter value by name.

Parameters

name string

Returns

int64
func (*SimpleMeter) GetCounter(name string) int64
{
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.counters[name]
}
GetGauge
Method

GetGauge returns the current gauge value by name.

Parameters

name string

Returns

float64
func (*SimpleMeter) GetGauge(name string) float64
{
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.gauges[name]
}
GetHistogram
Method

GetHistogram returns the aggregate histogram values by name.

Parameters

name string
func (*SimpleMeter) GetHistogram(name string) HistogramSnapshot
{
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.histogram[name]
}

Fields

Name Type Description
mu sync.Mutex
counters map[string]int64
histogram map[string]HistogramSnapshot
gauges map[string]float64
S
struct

HistogramSnapshot

HistogramSnapshot is the bounded aggregate for a recorded histogram.

core/telemetry/telemetry.go:169-174
type HistogramSnapshot struct

Fields

Name Type Description
Count int64
Sum float64
Min float64
Max float64
F
function

NewSimpleMeter

NewSimpleMeter creates a SimpleMeter.

Returns

core/telemetry/telemetry.go:177-183
func NewSimpleMeter() *SimpleMeter

{
	return &SimpleMeter{
		counters:  make(map[string]int64),
		histogram: make(map[string]HistogramSnapshot),
		gauges:    make(map[string]float64),
	}
}
S
struct
Implements: Counter

simpleCounter

core/telemetry/telemetry.go:218-221
type simpleCounter struct

Methods

Add
Method

Parameters

delta int64
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
S
struct
Implements: Histogram

simpleHistogram

core/telemetry/telemetry.go:229-232
type simpleHistogram struct

Methods

Record
Method

Parameters

value float64
func (*simpleHistogram) Record(ctx context.Context, value float64)
{
	h.meter.mu.Lock()
	snapshot := h.meter.histogram[h.name]
	if snapshot.Count == 0 || value < snapshot.Min {
		snapshot.Min = value
	}
	if snapshot.Count == 0 || value > snapshot.Max {
		snapshot.Max = value
	}
	snapshot.Count++
	snapshot.Sum += value
	h.meter.histogram[h.name] = snapshot
	h.meter.mu.Unlock()
}

Fields

Name Type Description
meter *SimpleMeter
name string
S
struct
Implements: Gauge

simpleGauge

core/telemetry/telemetry.go:249-252
type simpleGauge struct

Methods

Set
Method

Parameters

value float64
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
F
function

TestProvider_NoopDefault

Parameters

core/telemetry/telemetry_test.go:17-33
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)
}
F
function

TestSimpleTracer

Parameters

core/telemetry/telemetry_test.go:35-39
func TestSimpleTracer(t *testing.T)

{
	tracer := NewSimpleTracer()
	span, _ := tracer.Start(context.Background(), "operation")
	span.End()
}
F
function

TestSimpleMeter

Parameters

core/telemetry/telemetry_test.go:41-65
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)
	}
	histogram := meter.Histogram("latency")
	histogram.Record(ctx, 2)
	histogram.Record(ctx, 6)
	snapshot := meter.GetHistogram("latency")
	if snapshot.Count != 2 || snapshot.Sum != 8 || snapshot.Min != 2 || snapshot.Max != 6 {
		t.Fatalf("histogram = %+v", snapshot)
	}
}
F
function

TestTimed

Parameters

core/telemetry/telemetry_test.go:67-83
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")
	}
}
F
function

TestProvider_WithTracer

Parameters

core/telemetry/telemetry_test.go:85-91
func TestProvider_WithTracer(t *testing.T)

{
	custom := NewSimpleTracer()
	p := NewProvider(WithTracer(custom))
	if p.Tracer != custom {
		t.Error("WithTracer should set custom tracer")
	}
}
F
function

TestProvider_Shutdown

Parameters

core/telemetry/telemetry_test.go:93-101
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")
	}
}
F
function

TestOTLPExporter_ExportFlush

Parameters

core/telemetry/telemetry_test.go:103-154
func TestOTLPExporter_ExportFlush(t *testing.T)

{
	type request struct {
		path string
		body map[string]any
	}
	requests := make(chan request, 2)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var body map[string]any
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Errorf("decode OTLP request: %v", err)
		}
		requests <- request{path: r.URL.Path, body: body}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	e := NewOTLPExporter(WithOTLPEndpoint(server.URL), WithOTLPBatchSize(100))
	defer e.Close()
	start := time.Now().Add(-time.Second)
	if err := e.ExportSpan(OTLPSpan{
		TraceID:   "0af7651916cd43dd8448eb211c80319c",
		SpanID:    "b7ad6b7169203331",
		Name:      "test-span",
		StartTime: start,
		EndTime:   time.Now(),
	}); err != nil {
		t.Fatal(err)
	}
	if err := e.ExportMetric(OTLPMetric{
		Name:  "http_requests",
		Kind:  OTLPMetricCounter,
		Value: 42,
	}); err != nil {
		t.Fatal(err)
	}

	err := e.Flush(context.Background())
	if err != nil {
		t.Errorf("Flush: %v", err)
	}
	seen := make(map[string]map[string]any)
	for range 2 {
		req := <-requests
		seen[req.path] = req.body
	}
	if seen["/v1/traces"]["resourceSpans"] == nil {
		t.Fatal("trace request did not contain resourceSpans")
	}
	if seen["/v1/metrics"]["resourceMetrics"] == nil {
		t.Fatal("metric request did not contain resourceMetrics")
	}
}
F
function

TestOTLPExporterCloseIsIdempotentAndFlushes

Parameters

core/telemetry/telemetry_test.go:156-179
func TestOTLPExporterCloseIsIdempotentAndFlushes(t *testing.T)

{
	requests := make(chan struct{}, 1)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests <- struct{}{}
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	exporter := NewOTLPExporter(WithOTLPEndpoint(server.URL))
	if err := exporter.ExportMetric(OTLPMetric{Name: "requests", Kind: OTLPMetricCounter, Value: 1}); err != nil {
		t.Fatal(err)
	}
	if err := exporter.Close(); err != nil {
		t.Fatal(err)
	}
	if err := exporter.Close(); err != nil {
		t.Fatal(err)
	}
	select {
	case <-requests:
	case <-time.After(time.Second):
		t.Fatal("Close() did not flush pending telemetry")
	}
}
F
function

TestOTLPExporterRestoresFailedBatch

Parameters

core/telemetry/telemetry_test.go:181-203
func TestOTLPExporterRestoresFailedBatch(t *testing.T)

{
	exporter := NewOTLPExporter(WithOTLPEndpoint("://invalid"))
	start := time.Now().Add(-time.Second)
	if err := exporter.ExportSpan(OTLPSpan{
		TraceID:   "0af7651916cd43dd8448eb211c80319c",
		SpanID:    "b7ad6b7169203331",
		Name:      "span",
		StartTime: start,
		EndTime:   time.Now(),
	}); err != nil {
		t.Fatal(err)
	}
	if err := exporter.Flush(context.Background()); err == nil {
		t.Fatal("Flush() succeeded with invalid endpoint")
	}
	exporter.batchMu.Lock()
	count := len(exporter.spans)
	exporter.batchMu.Unlock()
	if count != 1 {
		t.Fatalf("retained spans = %d, want 1", count)
	}
	_ = exporter.Close()
}
F
function

TestOTLPExporterBoundsQueueAndRejectsUseAfterClose

Parameters

core/telemetry/telemetry_test.go:205-232
func TestOTLPExporterBoundsQueueAndRejectsUseAfterClose(t *testing.T)

{
	exporter := NewOTLPExporter(
		WithOTLPBatchSize(10),
		WithOTLPQueueSize(1),
	)
	if err := exporter.ExportMetric(OTLPMetric{
		Name: "load",
		Kind: OTLPMetricGauge,
	}); err != nil {
		t.Fatal(err)
	}
	if err := exporter.ExportMetric(OTLPMetric{
		Name: "second",
		Kind: OTLPMetricGauge,
	}); !errors.Is(err, ErrOTLPQueueFull) {
		t.Fatalf("ExportMetric() error = %v, want ErrOTLPQueueFull", err)
	}
	exporter.endpoint = "://invalid"
	if err := exporter.Close(); err == nil {
		t.Fatal("Close() succeeded with an invalid endpoint")
	}
	if err := exporter.ExportMetric(OTLPMetric{
		Name: "closed",
		Kind: OTLPMetricGauge,
	}); !errors.Is(err, ErrOTLPExporterClosed) {
		t.Fatalf("ExportMetric() after Close() = %v", err)
	}
}
F
function

TestOTLPExporterValidatesPublicInputs

Parameters

core/telemetry/telemetry_test.go:234-243
func TestOTLPExporterValidatesPublicInputs(t *testing.T)

{
	exporter := NewOTLPExporter()
	defer exporter.Close()
	if err := exporter.ExportSpan(OTLPSpan{Name: "invalid"}); err == nil {
		t.Fatal("ExportSpan() accepted missing IDs and timestamps")
	}
	if err := exporter.ExportMetric(OTLPMetric{Name: "invalid"}); err == nil {
		t.Fatal("ExportMetric() accepted an invalid kind")
	}
}
F
function

TestOTLPExporterDoesNotRetryPartialSuccessBatch

Parameters

core/telemetry/telemetry_test.go:245-269
func TestOTLPExporterDoesNotRetryPartialSuccessBatch(t *testing.T)

{
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"partialSuccess":{"rejectedDataPoints":"1","errorMessage":"invalid metric"}}`))
	}))
	defer server.Close()
	exporter := NewOTLPExporter(WithOTLPEndpoint(server.URL))
	defer exporter.Close()
	if err := exporter.ExportMetric(OTLPMetric{
		Name: "invalid",
		Kind: OTLPMetricGauge,
	}); err != nil {
		t.Fatal(err)
	}

	if err := exporter.Flush(context.Background()); err == nil {
		t.Fatal("Flush() ignored an OTLP partial success")
	}
	exporter.batchMu.Lock()
	queued := len(exporter.metrics)
	exporter.batchMu.Unlock()
	if queued != 0 {
		t.Fatalf("partial-success batch was queued for retry: %d metrics", queued)
	}
}
F
function

TestOTLPExporterCoalescesAutomaticFlushDuringOutage

Parameters

core/telemetry/telemetry_test.go:271-330
func TestOTLPExporterCoalescesAutomaticFlushDuringOutage(t *testing.T)

{
	started := make(chan struct{}, 1)
	release := make(chan struct{})
	var requests atomic.Int32
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests.Add(1)
		select {
		case started <- struct{}{}:
		default:
		}
		<-release
		w.WriteHeader(http.StatusServiceUnavailable)
	}))

	exporter := NewOTLPExporter(
		WithOTLPEndpoint(server.URL),
		WithOTLPBatchSize(1),
	)
	var wait sync.WaitGroup
	wait.Add(10)
	for index := 0; index < 10; index++ {
		go func(value float64) {
			defer wait.Done()
			_ = exporter.ExportMetric(OTLPMetric{
				Name:  "load",
				Kind:  OTLPMetricGauge,
				Value: value,
			})
		}(float64(index))
	}
	select {
	case <-started:
	case <-time.After(time.Second):
		t.Fatal("automatic flush did not start")
	}
	wait.Wait()
	close(release)
	deadline := time.Now().Add(time.Second)
	for {
		exporter.batchMu.Lock()
		inFlight := exporter.inFlight
		exporter.batchMu.Unlock()
		if inFlight == 0 || time.Now().After(deadline) {
			break
		}
		time.Sleep(time.Millisecond)
	}
	if got := requests.Load(); got != 1 {
		t.Fatalf("automatic outage flush requests = %d, want 1", got)
	}

	exporter.batchMu.Lock()
	exporter.spans = nil
	exporter.metrics = nil
	exporter.batchMu.Unlock()
	if err := exporter.Close(); err != nil {
		t.Fatal(err)
	}
	server.Close()
}
F
function

TestPrometheusExporter_CounterGauge

Parameters

core/telemetry/telemetry_test.go:332-348
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)
	}
}
F
function

TestPrometheusExporter_Histogram

Parameters

core/telemetry/telemetry_test.go:350-368
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)
	}
}
F
function

TestTraceparent

Parameters

core/telemetry/telemetry_test.go:370-389
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)
	}
}
F
function

TestTraceparent_Invalid

Parameters

core/telemetry/telemetry_test.go:391-413
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")
	}

	invalid := []string{
		"00-00000000000000000000000000000000-b7ad6b7169203331-01",
		"00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01",
		"00-0AF7651916CD43DD8448EB211C80319C-b7ad6b7169203331-01",
		"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-zz",
	}
	for _, header := range invalid {
		if _, err := ParseTraceparent(header); err == nil {
			t.Fatalf("ParseTraceparent() accepted %q", header)
		}
	}
}
F
function

TestTelemetryMiddleware_WrapHTTP

Parameters

core/telemetry/telemetry_test.go:415-442
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")
	}
	if got := w.Header().Get("traceparent"); got == "" {
		t.Fatal("traceparent response header was not written")
	}
}
F
function

TestTelemetryURLRedactsSecrets

Parameters

core/telemetry/telemetry_test.go:444-454
func TestTelemetryURLRedactsSecrets(t *testing.T)

{
	request := httptest.NewRequest(
		http.MethodGet,
		"https://user:[email protected]/path?token=secret#fragment",
		nil,
	)
	got := telemetryURL(request)
	if got != "https://example.com/path" {
		t.Fatalf("telemetryURL() = %q, want safe URL", got)
	}
}
F
function

TestTelemetryResponseWriterPreservesFirstStatusAndUnwraps

Parameters

core/telemetry/telemetry_test.go:456-476
func TestTelemetryResponseWriterPreservesFirstStatusAndUnwraps(t *testing.T)

{
	recorder := httptest.NewRecorder()
	writer := &telemetryResponseWriter{
		ResponseWriter: recorder,
		statusCode:     http.StatusOK,
	}
	writer.WriteHeader(http.StatusCreated)
	writer.WriteHeader(http.StatusInternalServerError)
	if writer.statusCode != http.StatusCreated {
		t.Fatalf("recorded status = %d, want 201", writer.statusCode)
	}
	if recorder.Code != http.StatusCreated {
		t.Fatalf("response status = %d, want 201", recorder.Code)
	}
	if writer.Unwrap() != recorder {
		t.Fatal("Unwrap() did not return the underlying writer")
	}
	if err := http.NewResponseController(writer).Flush(); err != nil {
		t.Fatalf("ResponseController.Flush() error = %v", err)
	}
}