shotel

package module
v0.0.13 Latest Latest
Warning

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

Go to latest
Published: Nov 6, 2025 License: MIT Imports: 19 Imported by: 0

README

shotel

CI Status codecov Go Report Card CodeQL Go Reference License Go Version Release

Opinionated capitan-to-OTEL bridge for automatic event observability.

Shotel observes all capitan events and transforms them into OpenTelemetry signals (logs, metrics, traces) based on configuration, while exposing standard OTEL Logger, Meter, and Tracer interfaces for direct use.

Quick Start

package main

import (
    "context"
    "github.com/zoobzio/capitan"
    "github.com/zoobzio/shotel"
)

func main() {
    ctx := context.Background()

    // Create OTEL providers with sensible defaults
    pvs, err := shotel.DefaultProviders(ctx, "my-service", "v1.0.0", "localhost:4318")
    if err != nil {
        panic(err)
    }
    defer pvs.Shutdown(ctx)

    // Create shotel bridge (nil config = log all events)
    sh, err := shotel.New(capitan.Default(), pvs.Log, pvs.Meter, pvs.Trace, nil)
    if err != nil {
        panic(err)
    }
    defer sh.Close()

    // Use OTEL primitives directly
    logger := sh.Logger("orders")
    meter := sh.Meter("orders")
    tracer := sh.Tracer("orders")

    // Capitan events automatically become OTEL logs
    sig := capitan.Signal("order.created")
    orderID := capitan.NewStringKey("order_id")

    capitan.Emit(ctx, sig, orderID.Field("ORDER-123"))
    // ↑ Automatically logged to OTEL
}

Architecture

Separation of Concerns

Capitan integration: Opinionated observability

  • Observes all capitan events
  • Transforms events to OTEL signals (logs, metrics, traces) based on config
  • Transforms fields to OTEL attributes
  • Exposes OTEL interfaces

Provider configuration: Flexible setup

  • DefaultProviders() for quick setup with sensible defaults
  • Or construct your own OTEL providers for full control
  • Handles exporters, processors, samplers
  • Manages lifecycle

This separation allows full control over OTEL configuration while keeping capitan integration opinionated and consistent.

Configuration

Shotel supports flexible configuration for transforming capitan events into OTEL signals.

Metrics: Auto-convert Signals to OTEL Metrics

Shotel supports four metric instrument types:

Counter - Count Signal Occurrences

Counters increment each time a signal is emitted:

orderCreated := capitan.Signal("order.created")

config := &shotel.Config{
    Metrics: []shotel.MetricConfig{
        {
            Signal:      orderCreated,
            Name:        "orders_created_total",
            Type:        shotel.MetricTypeCounter, // Optional, default
            Description: "Total orders created",
        },
    },
}
Gauge - Record Instantaneous Values

Gauges record the current value from a field:

cpuUsage := capitan.Signal("system.cpu.usage")
usageKey := capitan.NewFloat64Key("percent")

config := &shotel.Config{
    Metrics: []shotel.MetricConfig{
        {
            Signal:   cpuUsage,
            Name:     "cpu_usage_percent",
            Type:     shotel.MetricTypeGauge,
            ValueKey: usageKey, // Extract value from this field
        },
    },
}

// Emit gauge value
cap.Emit(ctx, cpuUsage, usageKey.Field(45.2))
// ↑ Gauge set to 45.2
Histogram - Record Value Distributions

Histograms record distributions (e.g., latencies, sizes):

requestCompleted := capitan.Signal("request.completed")
durationKey := capitan.NewDurationKey("duration")

config := &shotel.Config{
    Metrics: []shotel.MetricConfig{
        {
            Signal:   requestCompleted,
            Name:     "request_duration_ms",
            Type:     shotel.MetricTypeHistogram,
            ValueKey: durationKey,
        },
    },
}

// Emit duration measurements
cap.Emit(ctx, requestCompleted, durationKey.Field(250*time.Millisecond))
// ↑ Histogram records 250ms
UpDownCounter - Track Increments and Decrements

UpDownCounters can increase or decrease (e.g., queue depth, active connections):

queueDepth := capitan.Signal("queue.depth.changed")
deltaKey := capitan.NewInt64Key("delta")

config := &shotel.Config{
    Metrics: []shotel.MetricConfig{
        {
            Signal:   queueDepth,
            Name:     "queue_depth",
            Type:     shotel.MetricTypeUpDownCounter,
            ValueKey: deltaKey,
        },
    },
}

// Emit changes
cap.Emit(ctx, queueDepth, deltaKey.Field(5))   // +5
cap.Emit(ctx, queueDepth, deltaKey.Field(-2))  // -2

Note: Event fields automatically become metric dimensions (attributes) for all metric types.

Logs: Whitelist Filtering

Filter which signals get logged:

config := &shotel.Config{
    Logs: &shotel.LogConfig{
        Whitelist: []capitan.Signal{
            capitan.Signal("order.created"),
            capitan.Signal("order.failed"),
            // Only these signals are logged
        },
    },
}

If no whitelist is configured, all events are logged (default behavior).

Traces: Correlate Signal Pairs into Spans

Create spans from start/end signal pairs:

requestStarted := capitan.Signal("request.started")
requestCompleted := capitan.Signal("request.completed")
requestIDKey := capitan.NewStringKey("request_id")

config := &shotel.Config{
    Traces: []shotel.TraceConfig{
        {
            Start:          requestStarted,
            End:            requestCompleted,
            CorrelationKey: &requestIDKey,
            SpanName:       "http_request",
        },
    },
}

// Emit correlated events
cap.Emit(ctx, requestStarted, requestIDKey.Field("REQ-123"))
// ... work happens ...
cap.Emit(ctx, requestCompleted, requestIDKey.Field("REQ-123"))
// ↑ Creates a span from start to end

Both start and end events must have matching correlation key values.

Combined Configuration

Mix metrics, logs, and traces:

config := &shotel.Config{
    Metrics: []shotel.MetricConfig{
        {Signal: orderCreated, Name: "orders_created_total"},
    },
    Logs: &shotel.LogConfig{
        Whitelist: []capitan.Signal{orderCreated, orderFailed},
    },
    Traces: []shotel.TraceConfig{
        {
            Start:          orderCreated,
            End:            orderCompleted,
            CorrelationKey: &orderIDKey,
            SpanName:       "order_processing",
        },
    },
}

A single event can trigger multiple signal types (counted as metric + logged + start/end span).

Context Extraction: Enrich Signals with Context Values

Extract values from context.Context and automatically add them as attributes to logs, metrics, and traces:

// Define custom context keys
type ctxKey string
const (
    userIDKey  ctxKey = "user_id"
    regionKey  ctxKey = "region"
)

config := &shotel.Config{
    ContextExtraction: &shotel.ContextExtractionConfig{
        // Extract for logs
        Logs: []shotel.ContextKey{
            {Key: userIDKey, Name: "user_id"},
            {Key: regionKey, Name: "region"},
        },

        // Extract for metrics (use low-cardinality values only!)
        Metrics: []shotel.ContextKey{
            {Key: regionKey, Name: "region"},  // Good: limited values
            // Avoid: {Key: userIDKey, ...}    // Bad: high cardinality
        },

        // Extract for traces
        Traces: []shotel.ContextKey{
            {Key: userIDKey, Name: "user_id"},
            {Key: regionKey, Name: "region"},
        },
    },
}

// Add values to context
ctx := context.Background()
ctx = context.WithValue(ctx, userIDKey, "user-123")
ctx = context.WithValue(ctx, regionKey, "us-east-1")

// Emit event - context values automatically extracted
cap.Emit(ctx, orderCreated, orderIDKey.Field("ORDER-456"))
// ↑ Logs/metrics/traces will include user_id and region attributes

Supported Context Value Types:

  • string, int, int32, int64, uint, uint32, uint64
  • float32, float64, bool, []byte

Missing Values: If a context key is configured but not present in the context, it is skipped (no nil/empty attributes added).

Metrics Cardinality Warning: Only use low-cardinality context values for metrics (e.g., region, environment, service tier). High-cardinality values like user IDs or request IDs can exponentially increase metric storage costs.

API

Shotel
// Create shotel with pre-configured providers
func New(
    c *capitan.Capitan,
    logProvider log.LoggerProvider,
    meterProvider metric.MeterProvider,
    traceProvider trace.TracerProvider,
    config *Config, // Optional: nil means log all events
) (*Shotel, error)

// Get OTEL interfaces
func (s *Shotel) Logger(name string) log.Logger
func (s *Shotel) Meter(name string) metric.Meter
func (s *Shotel) Tracer(name string) trace.Tracer

// Stop observing (does NOT shutdown providers)
func (s *Shotel) Close()
Providers
// DefaultProviders creates providers with opinionated configuration
func DefaultProviders(
    ctx context.Context,
    serviceName string,
    serviceVersion string,
    otlpEndpoint string,
) (*Providers, error)

// Providers holds all three OTEL providers
type Providers struct {
    Log   *log.LoggerProvider
    Meter *metric.MeterProvider
    Trace *trace.TracerProvider
}

// Shutdown all providers
func (p *Providers) Shutdown(ctx context.Context) error

Capitan Field Transformation

Shotel transforms capitan event fields to OTEL attributes:

  • Event.Signal()log.String("capitan.signal", ...)
  • Event.Timestamp()LogRecord.SetTimestamp(...)
  • Event.Context() → Trace context propagation
  • Event.Fields() → Log attributes via type-safe transformation
Supported Field Types

All built-in capitan field types are supported:

  • string, int, int32, int64, uint, uint32, uint64
  • float32, float64, bool
  • time.Time (as Unix timestamp), time.Duration (as nanoseconds)
  • []byte, error (as string)
Custom Field Types

Register transformers for custom types:

type OrderInfo struct {
    ID     string
    Total  float64
    Secret string // Not logged
}

orderVariant := capitan.Variant("myapp.OrderInfo")

// Register transformer - receives typed value directly
shotel.RegisterTransformer(orderVariant, func(key string, order OrderInfo) []log.KeyValue {
    return []log.KeyValue{
        log.String(key+".id", order.ID),
        log.Float64(key+".total", order.Total),
        // Secret field intentionally omitted
    }
})

// Now OrderInfo fields are automatically transformed
orderKey := capitan.NewKey[OrderInfo]("order", orderVariant)
capitan.Emit(ctx, sig, orderKey.Field(OrderInfo{...}))

No manual type assertions needed - you receive the typed value directly.

Custom Provider Configuration

For full control over OTEL configuration, construct providers yourself:

// Build your own providers with custom configuration
logProvider := log.NewLoggerProvider(
    log.WithResource(myResource),
    log.WithProcessor(myProcessor),
    // Your custom configuration
)

meterProvider := metric.NewMeterProvider(
    metric.WithResource(myResource),
    metric.WithReader(myReader),
    // Your custom configuration
)

traceProvider := trace.NewTracerProvider(
    trace.WithResource(myResource),
    trace.WithSpanProcessor(myProcessor),
    trace.WithSampler(mySampler),
    // Your custom configuration
)

// Pass to shotel
sh, err := shotel.New(capitan.Default(), logProvider, meterProvider, traceProvider)

Shotel doesn't care how providers are configured - it just bridges capitan to OTEL.

DefaultProviders() Configuration

The DefaultProviders() helper uses these opinionated defaults:

  • Logs: Batch processor with OTLP HTTP exporter
  • Metrics: Periodic reader (60s interval) with OTLP HTTP exporter
  • Traces: Batch span processor, always-sample strategy, OTLP HTTP exporter
  • Connection: Insecure HTTP (for local development)
  • Resource: Service name, version, and host metadata

All exporters connect to the same otlpEndpoint (typically localhost:4318 for local OTEL collectors).

Philosophy

Shotel is opinionated about capitan integration, agnostic about OTEL configuration.

  • How events transform to logs? Opinionated (type-safe, field mapping)
  • Which exporter to use? Your choice
  • How to batch? Your choice
  • Security/TLS? Your choice

Shotel's value: "I observe capitan, I transform fields correctly, I give you back OTEL interfaces."

Everything else is provider configuration, which OTEL already solves.

Installation

go get github.com/zoobzio/shotel

Requirements: Go 1.24+

License

MIT

Documentation

Overview

Package shotel bridges capitan event coordination with OpenTelemetry observability.

Shotel observes all capitan events and transforms them into OTEL logs automatically, while exposing standard OTEL Logger, Meter, and Tracer interfaces for direct use.

OTEL provider configuration is handled externally - use the providers package for common setups, or construct your own providers for full control.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterTransformer added in v0.0.11

func RegisterTransformer[T any](variant capitan.Variant, fn TransformerFunc[T])

RegisterTransformer registers a custom transformer for a specific variant.

The transformer receives the field key and typed value, returning OTEL log attributes. Type assertion is handled automatically - your function receives the concrete type.

Example:

type OrderInfo struct {
    ID    string
    Total float64
    Secret string // Not logged
}

orderVariant := capitan.Variant("myapp.OrderInfo")

shotel.RegisterTransformer(orderVariant, func(key string, order OrderInfo) []log.KeyValue {
    return []log.KeyValue{
        log.String(key+".id", order.ID),
        log.Float64(key+".total", order.Total),
        // Secret field omitted
    }
})

func UnregisterTransformer added in v0.0.11

func UnregisterTransformer(variant capitan.Variant)

UnregisterTransformer removes a custom transformer for a specific variant.

Types

type Config

type Config struct {
	// Metrics specifies which signals should be auto-converted to OTEL counters.
	Metrics []MetricConfig

	// Logs configures which signals should be logged.
	// If nil or empty, all signals are logged (default behavior).
	Logs *LogConfig

	// Traces configures signal pairs that should be correlated into spans.
	Traces []TraceConfig

	// ContextExtraction specifies context keys to extract and add to OTEL signals.
	// If nil, no context extraction is performed.
	ContextExtraction *ContextExtractionConfig

	// StdoutLogging enables duplication of OTEL output to stdout.
	// When true, all OTEL signals are logged to stdout in human-readable format using slog.
	StdoutLogging bool
}

Config configures how capitan events are transformed to OTEL signals.

type ContextExtractionConfig added in v0.0.12

type ContextExtractionConfig struct {
	// Logs specifies context keys to extract and add to log attributes.
	Logs []ContextKey

	// Metrics specifies context keys to extract and add to metric dimensions.
	// WARNING: High-cardinality values (like unique request IDs) can significantly
	// increase metric storage costs. Use only low-cardinality values.
	Metrics []ContextKey

	// Traces specifies context keys to extract and add to span attributes.
	Traces []ContextKey
}

ContextExtractionConfig defines context values to extract for each signal type.

type ContextKey added in v0.0.12

type ContextKey struct {
	// Key is the context key used with context.Value().
	// Typically an unexported type to avoid collisions.
	Key any

	// Name is the attribute name to use in OTEL signals.
	Name string
}

ContextKey defines a key-name pair for extracting values from context.Context.

type LogConfig added in v0.0.11

type LogConfig struct {
	// Whitelist specifies which signals should be logged.
	// If empty, all signals are logged.
	Whitelist []capitan.Signal
}

LogConfig configures log filtering.

type MetricConfig added in v0.0.11

type MetricConfig struct {
	// Signal is the capitan signal to observe.
	Signal capitan.Signal

	// Name is the OTEL metric name.
	// Required - must be a valid OTEL metric name.
	Name string

	// Type is the metric instrument type.
	// Defaults to MetricTypeCounter if not specified.
	Type MetricType

	// ValueKey is the field key to extract metric value from.
	// Required for Gauge, Histogram, and UpDownCounter.
	// Not used for Counter (counts signal occurrences).
	// Must have a numeric variant (int, int64, float64, etc.).
	ValueKey capitan.Key

	// Description is optional metric description.
	Description string
}

MetricConfig defines a signal-to-metric conversion.

type MetricType added in v0.0.11

type MetricType string

MetricType specifies the type of OTEL metric instrument.

const (
	// MetricTypeCounter increments on each signal occurrence.
	// Does not use ValueKey - counts signals.
	MetricTypeCounter MetricType = "counter"

	// MetricTypeUpDownCounter increments or decrements based on ValueKey.
	// Requires ValueKey with numeric variant (int64 or float64).
	MetricTypeUpDownCounter MetricType = "updowncounter"

	// MetricTypeGauge records instantaneous value from ValueKey.
	// Requires ValueKey with numeric variant (int64 or float64).
	MetricTypeGauge MetricType = "gauge"

	// MetricTypeHistogram records value distribution from ValueKey.
	// Requires ValueKey with numeric variant (int64 or float64).
	MetricTypeHistogram MetricType = "histogram"
)

type Providers added in v0.0.11

type Providers struct {
	Log   *log.LoggerProvider
	Meter *metric.MeterProvider
	Trace *trace.TracerProvider
}

Providers holds configured OTEL providers for logs, metrics, and traces.

func DefaultProviders added in v0.0.11

func DefaultProviders(
	ctx context.Context,
	serviceName string,
	serviceVersion string,
	otlpEndpoint string,
) (*Providers, error)

DefaultProviders creates OTLP providers with opinionated defaults.

Configuration:

  • OTLP HTTP exporters for all signals
  • Insecure connection (for local development)
  • Batch processing for logs and traces
  • Periodic reader (60s) for metrics
  • Always-sample strategy for traces

Example:

providers, err := shotel.DefaultProviders(ctx, "my-service", "v1.0.0", "localhost:4318")
if err != nil {
    log.Fatal(err)
}
defer providers.Shutdown(ctx)

sh, err := shotel.New(capitan.Default(), providers.Log, providers.Meter, providers.Trace, nil)
if err != nil {
    log.Fatal(err)
}
defer sh.Close()

func (*Providers) Shutdown added in v0.0.11

func (p *Providers) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down all providers.

type Shotel

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

Shotel bridges capitan events to OTEL providers.

func New

func New(
	c *capitan.Capitan,
	logProvider log.LoggerProvider,
	meterProvider metric.MeterProvider,
	traceProvider trace.TracerProvider,
	config *Config,
) (*Shotel, error)

New creates a Shotel instance that observes capitan events and forwards them to OTEL.

Shotel automatically transforms capitan events based on the provided configuration. If no config is provided, all events are logged (backward compatible).

Parameters:

  • c: Capitan instance to observe (required)
  • logProvider: OTEL LoggerProvider (required)
  • meterProvider: OTEL MeterProvider (required)
  • traceProvider: OTEL TracerProvider (required)
  • config: Optional configuration (pass nil for defaults)

Example with providers package:

providers, err := providers.Default(ctx, "my-service", "v1.0.0", "localhost:4318")
if err != nil {
    log.Fatal(err)
}
defer providers.Shutdown(ctx)

sh := shotel.New(capitan.Default(), providers.Log, providers.Meter, providers.Trace, nil)
defer sh.Close()

Example with configuration:

config := &shotel.Config{
    Metrics: []shotel.MetricConfig{
        {Signal: orderCreated, Name: "orders_created_total"},
    },
    Logs: &shotel.LogConfig{Whitelist: []capitan.Signal{orderCreated}},
}
sh := shotel.New(capitan.Default(), providers.Log, providers.Meter, providers.Trace, config)

func (*Shotel) Close added in v0.0.11

func (s *Shotel) Close()

Close stops observing capitan events.

Note: This does NOT shutdown the OTEL providers - that is the caller's responsibility. If using the providers package, call providers.Shutdown(ctx) separately.

func (*Shotel) Logger added in v0.0.11

func (s *Shotel) Logger(name string) log.Logger

Logger returns an OTEL logger for the given scope name.

The scope name typically represents the package or component emitting logs.

func (*Shotel) Meter added in v0.0.11

func (s *Shotel) Meter(name string) metric.Meter

Meter returns an OTEL meter for the given scope name.

The scope name typically represents the package or component emitting metrics.

func (*Shotel) Tracer added in v0.0.11

func (s *Shotel) Tracer(name string) trace.Tracer

Tracer returns an OTEL tracer for the given scope name.

The scope name typically represents the package or component emitting traces.

type TraceConfig added in v0.0.11

type TraceConfig struct {
	// Start is the signal that begins the span.
	Start capitan.Signal

	// End is the signal that completes the span.
	End capitan.Signal

	// CorrelationKey is the field key used to correlate start/end events.
	// Both start and end events must have this field with matching values.
	CorrelationKey *capitan.StringKey

	// SpanName is the name of the generated span.
	// If empty, uses the Start signal as the span name.
	SpanName string

	// SpanTimeout is the maximum duration to wait for an end event.
	// If the end event doesn't arrive within this timeout, the span is
	// automatically ended and cleaned up to prevent memory leaks.
	// Defaults to 5 minutes if not specified or zero.
	SpanTimeout time.Duration
}

TraceConfig defines a signal pair that forms a trace span.

type TransformerFunc added in v0.0.11

type TransformerFunc[T any] func(key string, value T) []log.KeyValue

TransformerFunc converts a typed value to OTEL log attributes. Return nil or empty slice to skip the field.

Jump to

Keyboard shortcuts

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