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 Type | Metric Value |
|---|---|
Int64Key | Int64 value |
Float64Key | Float64 value |
DurationKey | Float64 milliseconds |
IntKey | Int64 (converted) |
UintKey | Int64 (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)