zoobzio December 12, 2025 Edit this page

Overview

Connecting application events to observability platforms usually means manual instrumentation scattered throughout your code.

Aperture offers a declarative alternative: configure how capitan events become OpenTelemetry signals, and the bridge handles the rest.

// Define signals and keys
orderCreated := capitan.NewSignal("order.created", "Order created")
orderID := capitan.NewStringKey("order_id")

// Create aperture with your OTEL providers
ap, _ := aperture.New(cap, logProvider, meterProvider, traceProvider)
defer ap.Close()

// Configure the bridge
schema := aperture.Schema{
    Metrics: []aperture.MetricSchema{
        {Signal: "order.created", Name: "orders_created_total", Type: "counter"},
    },
}
ap.Apply(schema)

// Events automatically become OTEL signals
cap.Emit(ctx, orderCreated, orderID.Field("ORDER-123"))
// ^ Logged to OTEL + counter incremented

Schema-driven, hot-reloadable, explicit provider configuration.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                         Aperture                            │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                   Observer                           │   │
│  │         (receives all capitan events)               │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│           ┌───────────────┼───────────────┐                 │
│           ▼               ▼               ▼                 │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │   Logger    │ │   Metrics   │ │   Traces    │           │
│  │  Transform  │ │  Transform  │ │  Transform  │           │
│  └─────────────┘ └─────────────┘ └─────────────┘           │
│           │               │               │                 │
│           ▼               ▼               ▼                 │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │    OTEL     │ │    OTEL     │ │    OTEL     │           │
│  │  LogProvider│ │MeterProvider│ │TraceProvider│           │
│  └─────────────┘ └─────────────┘ └─────────────┘           │
└─────────────────────────────────────────────────────────────┘

Aperture registers as a capitan observer, receives all events, and transforms them according to configuration. You provide pre-configured OTEL providers; aperture handles the event-to-signal mapping.

Philosophy

Aperture draws a clear line: opinionated about event transformation, agnostic about provider configuration.

What aperture decides:

  • How capitan fields become OTEL attributes
  • How signal pairs correlate into spans
  • How events filter to logs

What you decide:

  • Which exporters to use (OTLP, stdout, custom)
  • How to batch and buffer
  • Security, sampling, resource attributes
// Your configuration choices
logProvider := log.NewLoggerProvider(
    log.WithResource(myResource),
    log.WithProcessor(log.NewBatchProcessor(myExporter)),
)

// Aperture's transformation rules
schema := aperture.Schema{
    Metrics: []aperture.MetricSchema{...},
    Traces:  []aperture.TraceSchema{...},
    Logs:    &aperture.LogSchema{...},
}

// Two concerns, cleanly separated
ap, _ := aperture.New(cap, logProvider, meterProvider, traceProvider)
ap.Apply(schema)

Capabilities

Configuration drives behavior:

Metrics - Count events, record values, track distributions. Signal emissions become counter increments, gauge updates, or histogram observations.

Traces - Correlate signal pairs into spans. request.started and request.completed with matching request IDs create a single span.

Logs - Filter events to logs. Whitelist specific signals or log everything.

Context - Extract values from context.Context and add them as attributes to all three signal types.

Schema - Load configuration from YAML/JSON files with hot-reload support.

Priorities

Explicit Configuration

No magic defaults that might expose data unexpectedly. You construct providers explicitly, you configure transformations explicitly.

// You control the exporter
exporter, _ := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint("localhost:4318"))

// You control the provider
traceProvider := trace.NewTracerProvider(
    trace.WithSpanProcessor(trace.NewBatchSpanProcessor(exporter)),
    trace.WithSampler(trace.TraceIDRatioBased(0.1)),
)

// You pass it to aperture
ap, _ := aperture.New(cap, logProvider, meterProvider, traceProvider)

Automatic Type Handling

Field transformation preserves types. Built-in types convert directly to OTEL attributes; custom types are automatically JSON serialized.

type OrderInfo struct {
    ID     string  `json:"id"`
    Total  float64 `json:"total"`
    Secret string  `json:"-"` // Excluded from JSON
}

orderKey := capitan.NewKey[OrderInfo]("order", "Order details")

cap.Emit(ctx, orderCreated, orderKey.Field(OrderInfo{
    ID:     "ORD-123",
    Total:  99.99,
    Secret: "internal",
}))
// OrderInfo serialized as JSON: {"id":"ORD-123","total":99.99}

Use standard JSON struct tags to control what gets exported.

OTEL Native

Aperture exposes standard OTEL interfaces. Use them directly when you need to:

logger := ap.Logger("orders")
meter := ap.Meter("orders")
tracer := ap.Tracer("orders")

// Standard OTEL usage
ctx, span := tracer.Start(ctx, "process-order")
defer span.End()

The bridge adds automatic event transformation; it doesn't replace direct OTEL usage.