tel

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: Apache-2.0 Imports: 38 Imported by: 0

README

tel

OTLP metrics and traces for Go. The record path is allocation-sensitive; export is batched OTLP/gRPC—keep those worlds separate.

Module: github.com/gopherust-io/tel · Architecture · JetStream: nats

OpenSSF Scorecard

go get github.com/gopherust-io/tel@latest

Example

Env-first (recommended): put knobs in the process environment or a .env file. tel.Init loads .env (if present), parses config, configures the logger, and starts exporters:

package main

import (
	"context"
	"log"

	"github.com/gopherust-io/tel"
)

func main() {
	ctx := context.Background()
	t, shutdown := tel.Init(ctx) // .env + GetConfigFromEnv + ConfigureLogger + Start + SetGlobal
	defer shutdown(ctx)

	ctx = tel.WrapContext(ctx, t)
	processed, err := t.Registry().Counter("orders.processed")
	if err != nil {
		log.Fatal(err)
	}
	processed.AddWith(ctx, 1, "orders.created")
}

Local/tests without a collector: tel.InitWithConfig(ctx, tel.DefaultDebugConfig()) (skips .env).

Environment knobs
Variable Default Notes
TEL_DOTENV .env path loaded by Init / GetConfigFromEnv; missing file is OK
TEL_SERVICE_NAME hostname service field + OTel service.name
POD_NAME hostname log pod + instance id
NAMESPACE default
DEPLOY_ENVIRONMENT dev
VERSION dev
LOG_LEVEL info
LOG_ENCODE console json | pretty | console
TEL_ENABLE true OTLP export on/off
TEL_COLLECTOR_GRPC_ADDR 127.0.0.1:4317
TEL_TRACES_ENABLE true
TEL_TRACES_SAMPLER parentbased_statustraceidratio:0.1
MONITOR_ENABLE true /healthz, /stats
MONITOR_ADDR 127.0.0.1:8011

Record

Create instruments once. Use subject-keyed *With helpers—they hit AttrCache. Subjects must be a bounded set or you will blow cardinality.

r := tel.FromCtx(ctx).Registry()

count, err := r.Counter("orders.processed")
if err != nil {
	return err
}
latency, err := r.Histogram("orders.latency_seconds")
if err != nil {
	return err
}

count.AddWith(ctx, 1, "orders.created")

timer := tel.NewTimer(latency)
timer.Start()
// work
timer.StopWith(ctx, "orders.created")

Propagate

headers := tel.InjectContext(ctx, nil)
ctx = tel.ExtractContext(ctx, inboundHeaders)

Prefer MessagingSystem / MessagingSubject (and friends) over hand-rolled attribute maps.

Logging

Process-global zerolog via tel.InitLogger / tel.ConfigureLogger and tel.Info() / tel.Ctx(ctx).

LOG_ENCODE: console / text (zerolog ConsoleWriter, default) | json (compact) | pretty / json_pretty (indented JSON).

ConfigureLogger attaches resource fields when set: service, pod, namespace, environment, version.

pod comes from Config.Pod / POD_NAME, else HOSTNAME, else os.Hostname(). Set TEL_SERVICE_NAME to the app name and POD_NAME (Downward API) for the instance.

Every line includes caller as funcName:line (e.g. main.main:42). Err(err) adds a stack field (func/file/line frames) for the call path to the log site.

Trace correlation

Use context-aware helpers so log lines carry trace_id / span_id from the active span. StartSpan also stores an enriched logger on the returned context.

ctx, span := tel.FromCtx(ctx).StartSpan(ctx, "orders.create")
defer tel.EndSpan(span, err)

tel.InfoCtx(ctx).Msg("creating order")

Or wrap work with TraceFunc (span + one log line with function and duration):

err := tel.TraceFunc(ctx, "orders.create", func(ctx context.Context) error {
	return createOrder(ctx)
})
Operation metadata
Field When
trace_id, span_id Valid span on ctx (Ctx / *Ctx / after StartSpan)
function tel.Func(e, name) or TraceFunc
duration_ms tel.Duration(e, d) when d < 1s
duration_s tel.Duration(e, d) when d >= 1s
service, pod, namespace, environment, version From ConfigureLogger (pod via POD_NAME / hostname fallback)
start := time.Now()
// ...
tel.Duration(tel.Func(tel.InfoCtx(ctx), "HandlePay"), time.Since(start)).Msg("done")
Context fields

Immutable bag (copy-on-write). Nested WithFields appends; last key wins when logged.

ctx = tel.WithFields(ctx, tel.StrField("component", "api"), tel.IntField("user_shard", 3))
tel.InfoCtx(ctx).Msg("handling")
Log rate limits

Off by default (LOGS_MAX_MESSAGES_PER_SECOND=0). When set, ConfigureLogger installs an allocation-free RateSampler (atomics; never drops fatal/panic). Optional per-level caps: LOGS_MAX_LEVEL_MESSAGES_PER_SECOND=debug=50,info=200.

Trace sampling

TEL_TRACES_SAMPLER (default parentbased_statustraceidratio:0.1): always | never | traceidratio:N | statustraceidratio:N | parentbased_*. Status sampler force-records when start attrs/links include error (attrs at StartSpan only). DefaultDebugConfig() uses always.

fasthttp middleware

Native middleware (no net/http adaptor). Default span name is the HTTP method (low cardinality).

import telfasthttp "github.com/gopherust-io/tel/middleware/fasthttp"

h := telfasthttp.Server(next,
    telfasthttp.WithSkipPrefixes("/health", "/metrics"),
)

Knobs

Concern Knob
Collector TelConfig.Address / TEL_COLLECTOR_GRPC_ADDR
Export on/off TelConfig.Enable / TEL_ENABLE
Trace sampler TEL_TRACES_SAMPLER
Log rate limit LOGS_MAX_MESSAGES_PER_SECOND, LOGS_MAX_LEVEL_MESSAGES_PER_SECOND
Quiet local DefaultDebugConfig()
Compression On by default; gzip BestSpeed on export only (TEL_ENABLE_COMPRESSION)
Monitor MonitorConfigGET /healthz, GET /stats

Compression sets the process-wide gRPC gzip level. Default export is insecure—fine for a local collector; use TelConfig.Raw PEM for TLS/mTLS.

Lifecycle

Call Start before recording. Instruments obtained before Start are invalidated when Start runs—re-fetch via Registry() afterward. Shutdown is restart-safe (StartShutdownStartShutdown).

Do not

  1. Put network I/O, locks, or attribute allocation on the record path.
  2. Pass unbounded strings (user IDs, raw URLs) as *With subjects.
  3. Skip Start on a production DefaultConfig() and assume metrics still export.
  4. Keep using Counter/Histogram handles created before Start.

Development

make test
make demo

CONTRIBUTING.md

License

Apache License 2.0 — see LICENSE.

Documentation

Index

Examples

Constants

View Source
const (
	FieldTraceID     = "trace_id"
	FieldSpanID      = "span_id"
	FieldService     = "service"
	FieldPod         = "pod"
	FieldNamespace   = "namespace"
	FieldEnvironment = "environment"
	FieldVersion     = "version"
	FieldFunction    = "function"
	FieldDurationMs  = "duration_ms"
	FieldDurationS   = "duration_s"
)

Structured log field keys for correlation and operation metadata.

Variables

This section is empty.

Functions

func ConfigureLogger

func ConfigureLogger(cfg Config)

ConfigureLogger applies Config.LogLevel / LogEncode to the process logger. Call once at process startup before concurrent logging (not from NewWithConfig).

func Ctx

func Ctx(ctx context.Context) *zerolog.Logger

Ctx returns a logger for ctx. When ctx carries a valid OTel span and/or WithFields bag, a child logger is built once; otherwise the context/process logger is reused.

func Debug

func Debug() *zerolog.Event

func DebugCtx added in v0.3.0

func DebugCtx(ctx context.Context) *zerolog.Event

func Duration added in v0.3.0

func Duration(e *zerolog.Event, d time.Duration) *zerolog.Event

Duration attaches elapsed time: duration_ms when under 1s, otherwise duration_s.

func EndSpan

func EndSpan(span trace.Span, err error)

func Error

func Error() *zerolog.Event

func ErrorCtx added in v0.3.0

func ErrorCtx(ctx context.Context) *zerolog.Event

func Extract added in v0.3.0

Extract reads W3C trace context from carrier.

func ExtractContext

func ExtractContext(ctx context.Context, headers map[string][]string) context.Context

func Func added in v0.3.0

func Func(e *zerolog.Event, name string) *zerolog.Event

Func attaches a function name to the event.

func Info

func Info() *zerolog.Event

func InfoCtx added in v0.3.0

func InfoCtx(ctx context.Context) *zerolog.Event

func InitLogger

func InitLogger(opts LoggerOptions)

InitLogger configures structured logging. Safe to call at startup (and again to reconfigure).

func Inject added in v0.3.0

func Inject(ctx context.Context, carrier propagation.TextMapCarrier)

Inject writes W3C trace context into carrier.

func InjectContext

func InjectContext(ctx context.Context, headers map[string][]string) map[string][]string
Example

ExampleInjectContext demonstrates W3C trace context propagation via headers.

headers := InjectContext(context.Background(), map[string][]string{})
_ = ExtractContext(context.Background(), headers)

func Logger

func Logger() zerolog.Logger

func MessagingOperationProcess

func MessagingOperationProcess() attribute.KeyValue

func MessagingOperationPublish

func MessagingOperationPublish() attribute.KeyValue

func MessagingSubject

func MessagingSubject(subject string) attribute.KeyValue

func MessagingSystem

func MessagingSystem() attribute.KeyValue

func MustReloadConfig added in v0.3.0

func MustReloadConfig(cfg *Config)

func ReloadConfig added in v0.3.0

func ReloadConfig(cfg *Config) error

func SetExitFunc

func SetExitFunc(fn func(int))

SetExitFunc overrides the function called after Fatal logs (for tests).

func SetGlobal

func SetGlobal(t *Telemetry)

func SetLogger

func SetLogger(l zerolog.Logger)

SetLogger replaces the process-global logger and DefaultContextLogger.

func StatusTraceIDRatioBased added in v0.3.0

func StatusTraceIDRatioBased(fraction float64) sdktrace.Sampler

StatusTraceIDRatioBased samples like TraceIDRatioBased, but always records when start attributes or links include an "error" key.

func TraceFunc added in v0.3.0

func TraceFunc(ctx context.Context, name string, fn func(context.Context) error) error

TraceFunc starts a span named name, runs fn, ends the span, and logs once with function + adaptive duration (and trace_id/span_id via the span context).

func Warn

func Warn() *zerolog.Event

func WarnCtx added in v0.3.0

func WarnCtx(ctx context.Context) *zerolog.Event

func WithFields added in v0.3.0

func WithFields(ctx context.Context, fields ...Field) context.Context

WithFields returns a child context that carries additional log fields. Nested calls append (last-wins when applied). Parent slice is copied — never mutated.

func WrapContext

func WrapContext(ctx context.Context, l *Telemetry) context.Context

Types

type AttrCache

type AttrCache struct {
	// contains filtered or unexported fields
}

AttrCache interns attribute sets and metric options for hot-path label reuse. Entries are sharded by subject hash; hits take a short shard RLock.

func (*AttrCache) Len

func (c *AttrCache) Len() int

func (*AttrCache) SetDetector

func (c *AttrCache) SetDetector(d *cardinalityDetector)

SetDetector wires optional cardinality observation into the hot path.

func (*AttrCache) Subject

func (c *AttrCache) Subject(subject string) attribute.Set

func (*AttrCache) SubjectOpts

func (c *AttrCache) SubjectOpts(subject string) []metric.AddOption

func (*AttrCache) SubjectRecordOpts

func (c *AttrCache) SubjectRecordOpts(subject string) []metric.RecordOption

type CardinalityDetectorConfig added in v0.3.0

type CardinalityDetectorConfig struct {
	MaxCardinality     int           `env:"METRICS_CARDINALITY_DETECTOR_MAX_CARDINALITY"     default:"100"`
	MaxInstruments     int           `env:"METRICS_CARDINALITY_DETECTOR_MAX_INSTRUMENTS"     default:"500"`
	DiagnosticInterval time.Duration `env:"METRICS_CARDINALITY_DETECTOR_DIAGNOSTIC_INTERVAL" default:"10m"`
	Enable             bool          `env:"METRICS_CARDINALITY_DETECTOR_ENABLE"              default:"true"`
}

CardinalityDetectorConfig limits metric label / instrument cardinality.

goalign:ignore

type Config

type Config struct {
	MonitorConfig MonitorConfig
	TelConfig     TelConfig

	Service string `env:"TEL_SERVICE_NAME"`
	// Pod is the instance identity (K8s pod name). Falls back to HOSTNAME / os.Hostname.
	Pod                       string `env:"POD_NAME"`
	Namespace                 string `env:"NAMESPACE"                          default:"default"`
	Environment               string `env:"DEPLOY_ENVIRONMENT"                 default:"dev"`
	Version                   string `env:"VERSION"                            default:"dev"`
	LogLevel                  string `env:"LOG_LEVEL"                          default:"info"`
	LogEncode                 string `env:"LOG_ENCODE"                         default:"console"`
	MaxMessagesPerSecond      int    `env:"LOGS_MAX_MESSAGES_PER_SECOND"       default:"0"`
	MaxLevelMessagesPerSecond string `env:"LOGS_MAX_LEVEL_MESSAGES_PER_SECOND"`
	Debug                     bool   `env:"DEBUG"                              default:"false"`
}

Config holds process-wide telemetry settings. Embedded configs stay first (embeddedstructfieldcheck); fieldalignment is excluded for this file in .golangci.yml and make align.

goalign:ignore

func DefaultConfig

func DefaultConfig() Config

func DefaultDebugConfig

func DefaultDebugConfig() Config

func GetConfigFromEnv added in v0.3.0

func GetConfigFromEnv() (Config, error)

GetConfigFromEnv loads `.env` (if present), then parses Config from the process environment. Empty TEL_SERVICE_NAME falls back to a hostname-derived service name.

func LoadConfig added in v0.3.0

func LoadConfig() (Config, error)

func LoadConfigFrom added in v0.3.0

func LoadConfigFrom(snap *env.EnvSnapshot) (Config, error)

func MustLoadConfig added in v0.3.0

func MustLoadConfig() Config

type FastCounter

type FastCounter struct {
	// contains filtered or unexported fields
}

func (*FastCounter) Add

func (c *FastCounter) Add(ctx context.Context, n int64)

func (*FastCounter) AddWith

func (c *FastCounter) AddWith(ctx context.Context, n int64, subject string)

func (*FastCounter) WithAttrs

func (c *FastCounter) WithAttrs(attrs attribute.Set) *FastCounter

type FastGauge

type FastGauge struct {
	// contains filtered or unexported fields
}

func (*FastGauge) Record

func (g *FastGauge) Record(ctx context.Context, value int64)

func (*FastGauge) RecordWith

func (g *FastGauge) RecordWith(ctx context.Context, value int64, subject string)

func (*FastGauge) WithAttrs

func (g *FastGauge) WithAttrs(attrs attribute.Set) *FastGauge

type FastHistogram

type FastHistogram struct {
	// contains filtered or unexported fields
}

func (*FastHistogram) Record

func (h *FastHistogram) Record(ctx context.Context, value float64)

func (*FastHistogram) RecordWith

func (h *FastHistogram) RecordWith(ctx context.Context, value float64, subject string)

func (*FastHistogram) WithAttrs

func (h *FastHistogram) WithAttrs(attrs attribute.Set) *FastHistogram

type FatalEvent

type FatalEvent struct {
	// contains filtered or unexported fields
}

FatalEvent mirrors zerolog's chainable API but exits via SetExitFunc instead of os.Exit directly.

func Fatal

func Fatal() *FatalEvent

Fatal logs at fatal level and invokes the configured exit function (default os.Exit).

func (*FatalEvent) Err

func (f *FatalEvent) Err(err error) *FatalEvent

func (*FatalEvent) Msg

func (f *FatalEvent) Msg(msg string)

func (*FatalEvent) Msgf

func (f *FatalEvent) Msgf(format string, v ...any)

func (*FatalEvent) Str

func (f *FatalEvent) Str(key, val string) *FatalEvent

type Field added in v0.3.0

type Field struct {
	Key  string
	Str  string
	Int  int64
	Kind uint8
	Bool bool
}

Field is a typed context log attribute. goalign:ignore // trailing bool padding is unavoidable

func BoolField added in v0.3.0

func BoolField(key string, val bool) Field

BoolField builds a bool field.

func IntField added in v0.3.0

func IntField(key string, val int64) Field

IntField builds an int64 field.

func StrField added in v0.3.0

func StrField(key, val string) Field

StrField builds a string field.

type HistogramOpt

type HistogramOpt struct {
	Name       string
	Boundaries []float64
}

type LoggerOptions

type LoggerOptions struct {
	Level  string
	JSON   bool
	Pretty bool // indent JSON lines (only when JSON is true)
}

LoggerOptions configures the process-global zerolog logger.

goalign:ignore

type MetricsConfig added in v0.3.0

type MetricsConfig struct {
	CardinalityDetector CardinalityDetectorConfig
	EnableRetry         bool `env:"METRICS_ENABLE_RETRY" default:"false"`
}

MetricsConfig holds metric export / cardinality settings.

goalign:ignore

type MonitorConfig

type MonitorConfig struct {
	MonitorAddr string `env:"MONITOR_ADDR"   default:"127.0.0.1:8011"`
	Enable      bool   `env:"MONITOR_ENABLE" default:"true"`
}

MonitorConfig holds health/stats monitor settings.

goalign:ignore

type RateSampler added in v0.3.0

type RateSampler struct {
	// contains filtered or unexported fields
}

RateSampler caps log volume per second (global and optional per-level). Sample is allocation-free: atomics + fixed per-level counters.

func NewRateSampler added in v0.3.0

func NewRateSampler(globalLimit int, levelLimits map[zerolog.Level]uint64) *RateSampler

NewRateSampler builds a sampler. globalLimit 0 disables global capping (per-level limits may still apply). levelLimits maps zerolog level → max/sec; unset levels are unlimited.

func (*RateSampler) Sample added in v0.3.0

func (s *RateSampler) Sample(lvl zerolog.Level) bool

Sample implements zerolog.Sampler.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

func (*Registry) AttrCache

func (r *Registry) AttrCache() *AttrCache

func (*Registry) Counter

func (r *Registry) Counter(name string, opts ...metric.Int64CounterOption) (*FastCounter, error)

func (*Registry) Gauge

func (r *Registry) Gauge(name string, opts ...metric.Int64GaugeOption) (*FastGauge, error)

func (*Registry) Histogram

func (r *Registry) Histogram(name string, opts ...metric.Float64HistogramOption) (*FastHistogram, error)

type TLSRawConfig added in v0.3.0

type TLSRawConfig struct {
	CA   []byte `env:"OTEL_COLLECTOR_TLS_CA_CERT"`
	Cert []byte `env:"OTEL_COLLECTOR_TLS_CLIENT_CERT"`
	Key  []byte `env:"OTEL_COLLECTOR_TLS_CLIENT_KEY"`
}

TLSRawConfig holds PEM material for collector mTLS.

goalign:ignore

type TelConfig

type TelConfig struct {
	Address                    string         `env:"TEL_COLLECTOR_GRPC_ADDR"       default:"127.0.0.1:4317"`
	ServerName                 string         `env:"TEL_COLLECTOR_TLS_SERVER_NAME"`
	Raw                        TLSRawConfig   `env:"-"`
	BucketView                 []HistogramOpt `env:"-"` // runtime-only
	Metrics                    MetricsConfig  `env:"-"`
	MetricsPeriodicIntervalSec int            `env:"TEL_METRIC_PERIODIC_INTERVAL_SEC" default:"15"`
	ExportIntervalSec          int            `env:"TEL_EXPORT_INTERVAL_SEC"          default:"0"`
	WithInsecure               bool           `env:"TEL_EXPORTER_WITH_INSECURE"       default:"true"`
	Enable                     bool           `env:"TEL_ENABLE"                       default:"true"`
	WithCompression            bool           `env:"TEL_ENABLE_COMPRESSION"           default:"true"`
	Traces                     TracesConfig   `env:"-"`
}

TelConfig holds OTLP export and related collector settings.

goalign:ignore

type Telemetry

type Telemetry struct {
	// contains filtered or unexported fields
}

Telemetry is the process-wide metrics/traces runtime.

goalign:ignore

func FromCtx

func FromCtx(ctx context.Context) *Telemetry

func Global

func Global() *Telemetry

func Init added in v0.3.0

func Init(ctx context.Context) (*Telemetry, func(context.Context) error)

Init loads .env + env config, configures the logger, creates telemetry, sets the process global, and starts exporters. On failure it Fatals (does not return an error). The returned shutdown flushes exporters; call it on process exit (e.g. defer).

func InitWithConfig added in v0.3.0

func InitWithConfig(ctx context.Context, cfg Config) (*Telemetry, func(context.Context) error, error)

InitWithConfig is like Init but uses an explicit Config (tests / custom setup). It does not load .env. The returned shutdown flushes exporters.

func New

func New() *Telemetry

func NewWithConfig

func NewWithConfig(cfg Config) *Telemetry

func NewWithTracerProvider

func NewWithTracerProvider(cfg Config, provider trace.TracerProvider) *Telemetry

NewWithTracerProvider wires a custom tracer provider (useful in tests and custom setups).

func (*Telemetry) Config

func (t *Telemetry) Config() Config

func (*Telemetry) Meter

func (t *Telemetry) Meter(ins string, opts ...metric.MeterOption) metric.Meter

func (*Telemetry) Registry

func (t *Telemetry) Registry() *Registry

func (*Telemetry) Shutdown

func (t *Telemetry) Shutdown(ctx context.Context) error

Shutdown flushes exporters and releases resources. Safe to call after Start, and again after a subsequent Start (restart-safe; not sync.Once).

func (*Telemetry) Start

func (t *Telemetry) Start(ctx context.Context) error

func (*Telemetry) StartSpan

func (t *Telemetry) StartSpan(
	ctx context.Context,
	spanName string,
	opts ...trace.SpanStartOption,
) (context.Context, trace.Span)

StartSpan starts a span on the telemetry tracer provider. The caller is responsible for ending the returned span.

func (*Telemetry) Tracer

func (t *Telemetry) Tracer(name string) trace.Tracer

type Timer

type Timer struct {
	// contains filtered or unexported fields
}

func NewTimer

func NewTimer(hist *FastHistogram) Timer

func (*Timer) Start

func (t *Timer) Start()

func (*Timer) Stop

func (t *Timer) Stop(ctx context.Context)

func (*Timer) StopWith

func (t *Timer) StopWith(ctx context.Context, subject string)

type TracesConfig added in v0.3.0

type TracesConfig struct {
	Enable  bool   `env:"TEL_TRACES_ENABLE"  default:"true"`
	Sampler string `env:"TEL_TRACES_SAMPLER" default:"parentbased_statustraceidratio:0.1"`
}

TracesConfig holds trace export settings.

goalign:ignore

Directories

Path Synopsis
examples
basic command
Example of tel lifecycle and subject-keyed metrics.
Example of tel lifecycle and subject-keyed metrics.
internal
bytesconv
Package bytesconv provides zero-allocation string↔[]byte conversions.
Package bytesconv provides zero-allocation string↔[]byte conversions.
middleware

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL