Help us shape the future of Go! Take the 2026 Go Developer Survey

obs

package module
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 34 Imported by: 0

README

Nexss Observability

Nexss Flow 🌀 ready

CI Latest tag Go Reference License

Nexss Observability provides OpenTelemetry tracing, Prometheus metrics, contextual log/slog logging, and readiness checks for Nexss Kernel actions and Go services. Instrumentation can be attached at action and transport boundaries, so application code can use ordinary context.Context without hand-writing telemetry bookkeeping around every operation.

Requires Go 1.26.9 or newer.

Capabilities

  • Action instrumentation: actionhook.New(provider) adapts the provider to Kernel's action.AnyHook. Instrumented actions get spans, latency and error metrics, trace context, and retry/cache/concurrency events. Human-intervention suspensions are recorded separately from failures.

  • OpenTelemetry tracing: configure OTLP/HTTP trace export, or supply a custom span processor. No network trace exporter is attached by the Go provider unless an OTLP endpoint is configured.

  • Prometheus metrics: expose the provider's registry through MetricsHandler(). Action metrics are action_latency_ms, action_errors_total, and action_suspended_total; names can be prefixed or namespaced. Optional LLM metrics cover token counts, cost, and duration.

  • Contextual logging: Provider.Logger() returns a slog.Logger that adds trace_id, span_id, trace_flags, and the active action name when a valid span is present.

  • Propagation helpers: httptrace wraps HTTP clients/transports, and dbtrace wraps database/sql operations. TraceID and SpanID read IDs from the active OpenTelemetry span or Kernel context.

  • Manual spans and local collection: StartSpan creates application-level spans. WithCollector keeps a bounded in-memory ring of spans created with StartSpan for simple local timelines.

  • Readiness and business events: register readiness checks for HealthHandler( ), and emit structured business events with the events package. The Flow adapter also exposes observability.emit_event.

  • Stream and LLM hooks: attach stream statistics to an active span, or opt into LLM token/cost metrics, logs, and span attributes with the llm hook.

These are Go APIs unless specifically called out under Nexss Flow integration. The provider returns HTTP handlers; it does not start a server or bind a port.

Go integration

Install the module:

go get github.com/nexssp/observability@latest

Initialize the provider, attach its action hook, and shut it down with a bounded context:

package main

import (
    "context"
    "errors"
    "log"
    "time"

    "github.com/nexssp/kernel/action"
    obs "github.com/nexssp/observability"
    "github.com/nexssp/observability/actionhook"
)

func run() (err error) {
    ctx := context.Background()
    provider, shutdown, err := obs.Auto()
    if err != nil {
        return err
    }
    shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    defer func() { err = errors.Join(err, shutdown(shutdownCtx)) }()

    greet := action.New("demo.greet", func(_ context.Context, name string) (string, error) {
        return "hello " + name, nil
    }).AnyHook(actionhook.New(provider)).Build()

    result, err := greet.Do(ctx, "world")
    if err != nil {
        return err
    }
    provider.Logger().InfoContext(ctx, "greeting completed", "result", result)
    return nil
}

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

actionhook.New(provider) installs instrumentation on eligible actions as they are built; Kernel system-scope actions are excluded. The hook creates an action.<name> span, records latency/errors/suspensions, and places the active trace IDs into the action context. Attach the hook when building the action, or use the Flow bundle described below.

Configuration

obs.Auto() reads these environment variables. Empty values use the defaults shown; invalid health timeouts fall back to the default.

Variable Purpose Default / behavior
SERVICE_NAME Service resource name nexss
NODE_ID Service-instance identifier Generated from service name, hostname, and a random suffix when unset
ENV Deployment environment local
OTLP_ENDPOINT OTLP/HTTP trace endpoint Unset; no network trace exporter is attached
OTLP_HEADERS Comma-separated key=value exporter headers Empty
OTLP_INSECURE Allow insecure OTLP transport false; localhost endpoints are treated as insecure
SAMPLE_RATIO Trace sampling ratio in [0,1] 1 (record all sampled traces); explicit 0 disables sampling
HEALTH_CHECK_TIMEOUT Per-check readiness timeout, as a Go duration 3s

For code-level settings, use AutoWithOptions or NewWithShutdown with Config:

provider, shutdown, err := obs.AutoWithOptions(
    obs.WithPrometheusRegistry(prometheus.NewRegistry()),
    obs.WithMetricsNamespace("nexss"),
    obs.WithMetricsSubsystem("checkout"),
    obs.WithHistogramBuckets([]float64{1, 5, 10, 50, 100, 500}),
    obs.WithLoggerHandler(slog.NewJSONHandler(os.Stdout, nil)),
)

Handle err and call the returned shutdown function when the application exits. Available options also include WithMetricsPrefix; when set, the prefix takes precedence over the namespace/subsystem pair for the action metrics. These settings customize the registry, metric names and histogram boundaries, and base slog.Handler.

Metrics and readiness handlers

Mount the returned handlers on an HTTP server owned and managed by your application. Register checks with a context-aware function; each check receives a context bounded by HEALTH_CHECK_TIMEOUT (or the configured timeout).

provider.RegisterCheck("database", func(ctx context.Context) error {
    return db.PingContext(ctx)
})

mux := http.NewServeMux( )
mux.Handle("GET /metrics", provider.MetricsHandler())
mux.Handle("GET /healthz", provider.HealthHandler())
// Serve mux with the application's own http.Server and lifecycle.

The metrics handler serves the provider's Prometheus registry. The health handler returns JSON: HTTP 200 when all registered checks pass, or HTTP 503 when a check returns an error (including a deadline error from a check that honors its timeout context ). With no checks registered, it reports healthy. The library deliberately does not create a listener.

Context propagation and log correlation

Use the wrappers where you want client/database spans under the current action span. They preserve the supplied context so existing trace context propagates through the call.

tracedClient := httptrace.WrapClient(&http.Client{Timeout: 3 * time.Second} )
tracedDB := dbtrace.Wrap(sqlDB)

traceID := obs.TraceID(ctx) // empty when there is no trace ID
spanID := obs.SpanID(ctx)   // empty when there is no span ID
provider.Logger().InfoContext(ctx, "calling dependency", "trace_id", traceID)

httptrace.WrapClient returns a cloned client with an OpenTelemetry transport; httptrace.WrapTransport can wrap a transport directly. dbtrace.Wrap instruments supported database/sql query, execution, statement, and transaction calls. The wrappers do not replace normal resource ownership: the application still owns its clients and underlying database.

Provider.Logger( ) is a contextual slog logger. When the context contains a valid OpenTelemetry span, it adds trace IDs and trace flags; it also adds the action name when the action hook has placed it in the context. TraceID and SpanID are available without importing OpenTelemetry packages.

Manual spans and in-memory timelines

Use StartSpan for work inside an action that is not covered by a wrapper. Pass an error to the returned end function so it records the error and status before closing the span.

spanCtx, endSpan := obs.StartSpan(ctx, "cache.load")
value, err := loadFromCache(spanCtx)
endSpan(err)
if err != nil {
    return err
}
_ = value

WithCollector attaches a bounded collector to a context. It captures completed spans created with StartSpan (up to DefaultCollectorCapacity, currently 64); it is an in-memory aid, not an exporter or a capture of every span created by other instrumentation.

ctx, collector := obs.WithCollector(ctx)
spanCtx, endSpan := obs.StartSpan(ctx, "batch.persist")
err := persist(spanCtx)
endSpan(err)

for _, span := range collector.Spans() {
    log.Printf("%s %s %s", span.Status, span.Name, span.Duration)
}

For a TraceRecord summary, call TraceRecordFromContext(ctx, actionName, transportName, err). FormatDuration formats durations for readable output. If you add dynamic attributes directly, use the standard OpenTelemetry span from trace.SpanFromContext(ctx) and check span.IsRecording() first.

Business events

events.Emit records a span named event.<type> and writes one structured business_event log record. Events are application telemetry, not Prometheus labels; keep high-cardinality values such as entity IDs in event fields rather than metric labels.

events.Emit(ctx, provider.Logger(), events.BusinessEvent{
    Type:     "order.created",
    Action:   "checkout.complete",
    EntityID: "ord-42",
    Tags:     map[string]string{"channel": "web"},
    Payload:  map[string]any{"total": 129.99},
})
LLM and stream telemetry

For LLM responses, implement llm.TokenCostProvider (Model, PromptTokens, and CompletionTokens). If the response also implements CachedTokenCostProvider, cached prompt tokens are reported separately. llm.NewLLMHook can add token and duration metrics, optional cost estimates, structured logs, and usage attributes to an active span. Attach hook.AsAnyHook() to the LLM action and pass provider.LLMMetrics() in its options. The metrics include prompt, cached-prompt, and completion token totals, estimated cost in USD, and call duration; cost metrics are emitted only when a cost calculator is supplied and returns a value.

For stream-producing code, populate obs.StreamStats with the measurements your consumer tracks and call provider.RecordStream(ctx, stats) to add stream attributes to the current span. The API carries stream identity, execution status/duration, emitted and delivered item counts, optional byte totals, and error details; it does not infer those counts or automatically wrap a stream iterator.

Nexss Flow integration

The nexssflow adapter is part of this Go module; there is no separate module to install. Its registered bundle adds the action hook, wraps the Flow invocation in a flow.run root span, and provides observability.emit_event for structured business events. For example, a Flow file can configure the adapter and emit an event like this:

@require github.com/nexssp/observability {
  service_name: "checkout-demo",
  trace_exporter: "stdout",
  sample_ratio: 1.0
}
@assert: result.emitted == true
{} -> observability.emit_event @{
  type: "order.created",
  action: "checkout.complete",
  entity_id: "ord-42",
  tags: { channel: "web" },
  payload: { outcome: "accepted" }
}

trace_exporter accepts otlp (the default), stdout, or none. stdout writes spans to stderr for local inspection. The adapter accepts these per-@require settings:

@require key Environment fallback Default when unset
service_name SERVICE_NAME nexss
env ENV local
sample_ratio SAMPLE_RATIO 1.0; explicit 0 disables sampling
otlp_endpoint OTLP_ENDPOINT Unset; no network trace exporter is attached
otlp_insecure OTLP_INSECURE false, except localhost endpoints are treated as insecure
metrics_prefix None Unset
metrics_namespace None Unset
metrics_subsystem None Unset
trace_exporter None otlp; also accepts stdout and none

metrics_prefix, when set, takes precedence over metrics_namespace and metrics_subsystem; these metric-naming settings have no environment-variable fallback. An explicit otlp_insecure: false overrides an environment-derived true. The Flow invocation owns the bundle shutdown callback.

WIP — Flow parity: Flow currently gets action instrumentation, the flow.run root span, exporter/configuration overrides, and the business-event action. It does not expose the complete Go API as Flow actions: manual spans and in-memory collection, HTTP/database wrappers, optional LLM and stream helpers, and Prometheus/readiness HTTP handlers remain Go-host or application concerns. The bundle does not start an HTTP listener. A Flow host that needs metrics or health endpoints must mount the provider handlers itself.

Runnable ecosystem examples are maintained in the centralized Nexss examples repository.

Development and tests

Install Task, then run the repository's test or CI commands:

task test       # go test -short ./...
task test:race  # go test -race -short ./...
task ci         # lint plus uncached race tests

The Taskfile also provides task test:nocache, task test:race:nocache, task test:cover, and task lint:nflow (lint the bundled Flow examples/specs). To check that all Go examples build, run task examples.

License

Apache License 2.0. See LICENSE for details.

Documentation

Overview

Package obs provides OpenTelemetry-based tracing, metrics, contextual logging, and readiness health probes.

Package obs provides tracing hooks for framework action execution.

Package obs provides interfaces for observability components.

Package obs provides OpenTelemetry contextual logging.

Package obs initializes OpenTelemetry meters, tracers, Prometheus registries, and health endpoints.

Index

Constants

View Source
const DefaultCollectorCapacity = 64

Variables

This section is empty.

Functions

func FormatDuration

func FormatDuration(d time.Duration) string

func SpanID

func SpanID(ctx context.Context) string

func StartSpan

func StartSpan(ctx context.Context, name string, detail ...string) (spanCtx context.Context, endSpan func(err ...error))

func TraceID

func TraceID(ctx context.Context) string

TraceID extracts active trace ID from context as string (or returns empty).

Types

type CompletedSpan

type CompletedSpan struct {
	Name          string `json:"name"`
	Status        string `json:"status"` // "OK", "ERROR", or "SUSPENDED"
	StartOffsetUs int64  `json:"start_offset_us"`
	StartOffsetNs int64  `json:"start_offset_ns"`
	DurationUs    int64  `json:"duration_us"`
	DurationNs    int64  `json:"duration_ns"`
	Duration      string `json:"duration"`
	Detail        string `json:"detail,omitempty"`
}

type Config

type Config struct {
	ServiceName  string
	NodeID       string
	Env          string
	OTLPEndpoint string
	OTLPHeaders  map[string]string
	OTLPInsecure bool
	// SampleRatio must be in [0,1]. A zero value defaults to 1 unless
	// SampleRatioSet is true, which distinguishes explicit zero sampling.
	SampleRatio        float64
	SampleRatioSet     bool
	HealthCheckTimeout time.Duration

	PrometheusRegistry *prometheus.Registry
	MetricsPrefix      string
	MetricsNamespace   string
	MetricsSubsystem   string
	HistogramBuckets   []float64
	LoggerHandler      slog.Handler
	TraceProcessor     sdktrace.SpanProcessor
}

Config holds observability initialization parameters.

func LoadConfigFromEnv

func LoadConfigFromEnv() Config

LoadConfigFromEnv builds Config from standard environment variables.

type ContextHandler

type ContextHandler struct {
	Handler slog.Handler
}

ContextHandler wraps a slog.Handler to inject active trace and span IDs into log records.

func NewContextHandler

func NewContextHandler(base slog.Handler) *ContextHandler

NewContextHandler constructs a ContextHandler around a base slog.Handler.

func (*ContextHandler) Enabled

func (h *ContextHandler) Enabled(ctx context.Context, level slog.Level) bool

Enabled reports whether the handler handles records at the given level.

func (*ContextHandler) Handle

func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error

Handle injects OpenTelemetry trace context and action metadata into the slog.Record.

func (*ContextHandler) WithAttrs

func (h *ContextHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs returns a new ContextHandler whose attributes include given attrs.

func (*ContextHandler) WithGroup

func (h *ContextHandler) WithGroup(name string) slog.Handler

WithGroup returns a new ContextHandler with the given group name appended.

type HealthChecker

type HealthChecker interface {
	Check(ctx context.Context) error
}

HealthChecker defines a component that can be polled for readiness.

type HealthRegistry

type HealthRegistry interface {
	RegisterCheck(name string, check func(ctx context.Context) error)
	HealthHandler() http.Handler
}

HealthRegistry allows dynamic registration of system readiness probes.

type Hook

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

Hook instruments action executions with OpenTelemetry spans and metrics.

func (*Hook) After

func (h *Hook) After(ctx context.Context, err error)

After ends the OpenTelemetry span and records latency/error/suspension metrics.

func (*Hook) Before

func (h *Hook) Before(ctx context.Context, actionName string, meta map[string]string) context.Context

Before is preserved for backward compatibility with map-based callers.

func (*Hook) BeforeWithAttributes added in v0.2.0

func (h *Hook) BeforeWithAttributes(ctx context.Context, actionName string, extraAttrs ...attribute.KeyValue) context.Context

BeforeWithAttributes starts an OTel span with stack-allocated attributes (0 heap allocations on hot path).

type Option

type Option func(*Config)

func WithHistogramBuckets added in v0.2.0

func WithHistogramBuckets(buckets []float64) Option

func WithLoggerHandler

func WithLoggerHandler(handler slog.Handler) Option

func WithMetricsNamespace added in v0.2.0

func WithMetricsNamespace(ns string) Option

func WithMetricsPrefix

func WithMetricsPrefix(prefix string) Option

func WithMetricsSubsystem added in v0.2.0

func WithMetricsSubsystem(sub string) Option

func WithPrometheusRegistry

func WithPrometheusRegistry(reg *prometheus.Registry) Option

type Provider

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

Provider manages OpenTelemetry meters, tracers, and health checks.

func Auto

func Auto() (*Provider, func(context.Context) error, error)

Auto initializes standard observability using environment configuration.

func AutoWithOptions

func AutoWithOptions(opts ...Option) (*Provider, func(context.Context) error, error)

AutoWithOptions initializes observability with optional functional modifications.

func New

func New(ctx context.Context, cfg Config, opts ...Option) (*Provider, error)

New constructs Provider with OpenTelemetry tracer, meter, and Prometheus registry.

func NewWithShutdown

func NewWithShutdown(cfg Config) (*Provider, func(context.Context) error, error)

NewWithShutdown instantiates Provider alongside an explicit cleanup function.

func (*Provider) Config

func (p *Provider) Config() Config

Config exposes active Provider configuration.

func (*Provider) Handler

func (p *Provider) Handler() slog.Handler

Handler returns the ContextHandler.

func (*Provider) HealthHandler

func (p *Provider) HealthHandler() http.Handler

HealthHandler serves JSON HTTP readiness probe responses.

func (*Provider) Hook

func (p *Provider) Hook() *Hook

Hook initializes an execution Hook from the Provider. The tracer is taken from the provider's own TracerProvider rather than the global one, so hooks stay valid when another Provider is installed on the global slot (parallel tests, multiple providers in one process).

func (*Provider) LLMMetrics added in v0.2.0

func (p *Provider) LLMMetrics() *llm.Metrics

func (*Provider) Logger

func (p *Provider) Logger() *slog.Logger

Logger returns the configured contextual slog logger.

func (*Provider) MetricsHandler

func (p *Provider) MetricsHandler() http.Handler

MetricsHandler provides standard HTTP handler for Prometheus scraping.

func (*Provider) RecordStream added in v0.4.0

func (p *Provider) RecordStream(ctx context.Context, stats StreamStats)

func (*Provider) RegisterCheck

func (p *Provider) RegisterCheck(name string, checkFunc func(context.Context) error)

RegisterCheck attaches a readiness health probe.

func (*Provider) Sink added in v0.2.0

func (p *Provider) Sink() *Sink

Sink returns an implementation of kernel/observe.Sink that routes Kernel lifecycle events to OpenTelemetry and Prometheus.

func (*Provider) Tracer added in v0.6.0

func (p *Provider) Tracer(name string) trace.Tracer

Tracer returns a tracer backed by this Provider's tracer provider.

type Sink added in v0.2.0

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

func NewSink added in v0.2.0

func NewSink(provider *Provider) *Sink

func (*Sink) Emit added in v0.2.0

func (s *Sink) Emit(ctx context.Context, event observe.Event)

func (*Sink) RecordStream added in v0.4.0

func (s *Sink) RecordStream(ctx context.Context, stats StreamStats)

RecordStream enriches the active OpenTelemetry span with stream execution metrics.

type StreamBytes added in v0.4.0

type StreamBytes struct {
	Emitted   int64 `json:"emitted,omitempty"`
	Delivered int64 `json:"delivered,omitempty"`
}

type StreamError added in v0.4.0

type StreamError struct {
	Kind    string `json:"kind"`
	Message string `json:"message"`
}

type StreamExecution added in v0.4.0

type StreamExecution struct {
	StartedAt time.Time     `json:"started_at"`
	Duration  time.Duration `json:"duration_ns"`
	Status    StreamStatus  `json:"status"`
	Canceled  bool          `json:"canceled,omitempty"`
	Truncated bool          `json:"truncated,omitempty"`
}

type StreamIdentity added in v0.4.0

type StreamIdentity struct {
	Name      string `json:"name"`
	RequestID string `json:"request_id,omitempty"`
	TraceID   string `json:"trace_id,omitempty"`
}

type StreamItems added in v0.4.0

type StreamItems struct {
	Emitted   int64 `json:"emitted"`
	Delivered int64 `json:"delivered"`
	Dropped   int64 `json:"dropped,omitempty"`
	Errors    int64 `json:"errors,omitempty"`
}

type StreamStats added in v0.4.0

type StreamStats struct {
	Stream StreamIdentity  `json:"stream"`
	Exec   StreamExecution `json:"execution"`
	Items  StreamItems     `json:"items"`
	Bytes  StreamBytes     `json:"bytes,omitzero"`
	Error  *StreamError    `json:"error,omitempty"`
}

type StreamStatus added in v0.4.0

type StreamStatus string
const (
	StreamStatusCompleted StreamStatus = "completed"
	StreamStatusFailed    StreamStatus = "failed"
	StreamStatusCanceled  StreamStatus = "canceled"
	StreamStatusTimeout   StreamStatus = "timeout"
	StreamStatusStopped   StreamStatus = "stopped"
)

type TraceCollector added in v0.3.0

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

TraceCollector is a true circular ring buffer with index arithmetic (zero slice reallocations).

func WithCollector

func WithCollector(ctx context.Context) (context.Context, *TraceCollector)

WithCollector attaches a TraceCollector to the context for capturing spans.

func (*TraceCollector) Spans added in v0.3.0

func (c *TraceCollector) Spans() []CompletedSpan

type TraceRecord

type TraceRecord struct {
	TraceID         string          `json:"trace_id"`
	Action          string          `json:"action"`
	Transport       string          `json:"transport"`
	Timestamp       time.Time       `json:"timestamp"`
	TotalDurationUs int64           `json:"total_duration_us"`
	Duration        string          `json:"duration"`
	Status          string          `json:"status"`
	Spans           []CompletedSpan `json:"spans"`
}

func TraceRecordFromContext

func TraceRecordFromContext(ctx context.Context, actionName, transportName string, err error) (TraceRecord, bool)

type Tracer

type Tracer interface {
	Span(ctx context.Context, name string) (context.Context, func(error))
}

Tracer defines custom span generation wrappers.

Directories

Path Synopsis
Package actionhook adapts *obs.Hook to the framework action system.
Package actionhook adapts *obs.Hook to the framework action system.
Package dbtrace provides OpenTelemetry tracing wrappers for database/sql operations.
Package dbtrace provides OpenTelemetry tracing wrappers for database/sql operations.
Package events provides business telemetry event emission.
Package events provides business telemetry event emission.
Package httptrace provides OpenTelemetry instrumentation wrappers for http Clients and Transports.
Package httptrace provides OpenTelemetry instrumentation wrappers for http Clients and Transports.
Package llm provides OpenTelemetry and Prometheus instrumentation for LLM calls.
Package llm provides OpenTelemetry and Prometheus instrumentation for LLM calls.
Package nexssflow adapts Nexss Observability to Nexss Flow.
Package nexssflow adapts Nexss Observability to Nexss Flow.

Jump to

Keyboard shortcuts

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