zoobzio December 12, 2025 Edit this page

Metrics Guide

Transform capitan signals into OTEL metrics.

Metric Types

Counter

Increments on each signal emission. No value extraction needed.

orderCreated := capitan.NewSignal("order.created", "Order created")

schema := aperture.Schema{
    Metrics: []aperture.MetricSchema{
        {
            Signal:      "order.created",
            Name:        "orders_created_total",
            Type:        "counter",
            Description: "Total number of orders created",
        },
    },
}

ap, _ := aperture.New(cap, logProvider, meterProvider, traceProvider)
ap.Apply(schema)

// Each emission increments by 1
cap.Emit(ctx, orderCreated)  // orders_created_total += 1
cap.Emit(ctx, orderCreated)  // orders_created_total += 1

Gauge

Records the current value from a field. Useful for instantaneous measurements.

cpuUsage := capitan.NewSignal("system.cpu", "CPU measurement")
percentKey := capitan.NewFloat64Key("percent")

schema := aperture.Schema{
    Metrics: []aperture.MetricSchema{
        {
            Signal:   "system.cpu",
            Name:     "cpu_usage_percent",
            Type:     "gauge",
            ValueKey: "percent",
        },
    },
}

// Records the value from the percent field
cap.Emit(ctx, cpuUsage, percentKey.Field(45.2))  // cpu_usage_percent = 45.2
cap.Emit(ctx, cpuUsage, percentKey.Field(67.8))  // cpu_usage_percent = 67.8

Histogram

Records value distributions. Ideal for latencies, sizes, or any value you want percentiles for.

requestDone := capitan.NewSignal("request.done", "Request completed")
durationKey := capitan.NewDurationKey("duration")

schema := aperture.Schema{
    Metrics: []aperture.MetricSchema{
        {
            Signal:   "request.done",
            Name:     "request_duration_ms",
            Type:     "histogram",
            ValueKey: "duration",
        },
    },
}

// Records duration values in the distribution
cap.Emit(ctx, requestDone, durationKey.Field(10*time.Millisecond))
cap.Emit(ctx, requestDone, durationKey.Field(250*time.Millisecond))
cap.Emit(ctx, requestDone, durationKey.Field(50*time.Millisecond))

UpDownCounter

Bidirectional counter for values that increase and decrease.

queueChanged := capitan.NewSignal("queue.changed", "Queue size changed")
deltaKey := capitan.NewInt64Key("delta")

schema := aperture.Schema{
    Metrics: []aperture.MetricSchema{
        {
            Signal:   "queue.changed",
            Name:     "queue_depth",
            Type:     "updowncounter",
            ValueKey: "delta",
        },
    },
}

// Track queue depth changes
cap.Emit(ctx, queueChanged, deltaKey.Field(int64(5)))   // queue_depth += 5
cap.Emit(ctx, queueChanged, deltaKey.Field(int64(-2)))  // queue_depth -= 2

Dimensions (Attributes)

Event fields automatically become metric dimensions:

orderCreated := capitan.NewSignal("order.created", "Order created")
regionKey := capitan.NewStringKey("region")
tierKey := capitan.NewStringKey("tier")

schema := aperture.Schema{
    Metrics: []aperture.MetricSchema{
        {Signal: "order.created", Name: "orders_total", Type: "counter"},
    },
}

// Metrics include region and tier as dimensions
cap.Emit(ctx, orderCreated, regionKey.Field("us-east"), tierKey.Field("premium"))
cap.Emit(ctx, orderCreated, regionKey.Field("eu-west"), tierKey.Field("standard"))

Produces:

orders_total{region="us-east", tier="premium"} = 1
orders_total{region="eu-west", tier="standard"} = 1

Cardinality Warning

High-cardinality dimensions exponentially increase storage:

// GOOD: Low cardinality
regionKey := capitan.NewStringKey("region")      // ~10 values
tierKey := capitan.NewStringKey("tier")          // ~3 values

// BAD: High cardinality
userIDKey := capitan.NewStringKey("user_id")     // ~millions of values
requestIDKey := capitan.NewStringKey("request_id") // ~infinite values

Use context extraction carefully for metrics:

ap.RegisterContextKey("region", regionKey)

schema := aperture.Schema{
    Context: &aperture.ContextSchema{
        Metrics: []string{"region"},    // OK
        // Metrics: []string{"user_id"}, // Avoid high-cardinality
    },
}

Value Key Types

For gauges, histograms, and up-down counters, the ValueKey extracts the numeric value:

Key TypeMetric Value
Int64KeyInt64 value
Float64KeyFloat64 value
DurationKeyFloat64 milliseconds
IntKeyInt64 (converted)
UintKeyInt64 (converted)
// Duration key extracts milliseconds
durationKey := capitan.NewDurationKey("duration")

// 100ms becomes 100.0
cap.Emit(ctx, sig, durationKey.Field(100*time.Millisecond))

Multiple Metrics per Signal

One signal can trigger multiple metrics:

requestDone := capitan.NewSignal("request.done", "Request completed")
durationKey := capitan.NewDurationKey("duration")

schema := aperture.Schema{
    Metrics: []aperture.MetricSchema{
        // Count total requests
        {
            Signal: "request.done",
            Name:   "requests_total",
            Type:   "counter",
        },
        // Record latency distribution
        {
            Signal:   "request.done",
            Name:     "request_duration_ms",
            Type:     "histogram",
            ValueKey: "duration",
        },
    },
}

// Both metrics updated
cap.Emit(ctx, requestDone, durationKey.Field(50*time.Millisecond))

Missing Values

If a gauge/histogram/updowncounter emission lacks the value key:

  • Metric operation is skipped for that emission
  • No error (best-effort approach)
schema := aperture.Schema{
    Metrics: []aperture.MetricSchema{
        {Signal: "test.signal", Name: "gauge", Type: "gauge", ValueKey: "value"},
    },
}

cap.Emit(ctx, sig)  // No value field - gauge update skipped
cap.Emit(ctx, sig, valueKey.Field(42.0))  // gauge = 42.0

Schema Configuration

Via YAML:

metrics:
  - signal: order.created
    name: orders_total
    type: counter
    description: Total orders placed

  - signal: request.done
    name: request_duration_ms
    type: histogram
    value_key: duration

  - signal: queue.changed
    name: queue_depth
    type: updowncounter
    value_key: delta

Load and apply:

configBytes, _ := os.ReadFile("config.yaml")
schema, _ := aperture.LoadSchemaFromYAML(configBytes)
ap.Apply(schema)