Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cmd/blitz/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/observiq/blitz/internal/config"
"github.com/observiq/blitz/internal/dispatch"
"github.com/observiq/blitz/internal/logging"
"github.com/observiq/blitz/internal/runtime"
"github.com/observiq/blitz/internal/service"
"github.com/observiq/blitz/internal/telemetry/logs"
"github.com/observiq/blitz/internal/telemetry/metrics"
Expand Down Expand Up @@ -91,6 +92,11 @@ func main() {
}

func run(cmd *cobra.Command, args []string) error {
// Mark process start so the blitz.startup.duration metric can measure the
// full standalone startup: config load, provider construction, output
// wiring, and bringing every generator up.
startTime := time.Now()

// Configure Viper to handle env overrides
viper.SetConfigType("yaml")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
Expand Down Expand Up @@ -405,6 +411,13 @@ func run(cmd *cobra.Command, args []string) error {
return err
}

// Record process-level startup latency (best effort; a metric-build failure
// must not fail startup). Per-module and session startup are recorded by the
// runtime.
if lifecycleMetrics, merr := runtime.NewMetrics(tel.MeterProvider); merr == nil {
lifecycleMetrics.BlitzStartupDurationHistogram.Record(ctx, runtime.DurationMillis(time.Since(startTime)))
}

if tracker == nil {
<-ctx.Done()
} else if cfg.OnFinish == "idle" {
Expand Down
5 changes: 4 additions & 1 deletion embed/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ func (r *runner) Start(ctx context.Context, host Host) error {
for i, m := range r.cfg.Modules {
rtModules[i] = m
}
rt := runtime.New(logger, rtModules, tel.TracerProvider)
rt, err := runtime.New(logger, rtModules, tel.TracerProvider, tel.MeterProvider)
if err != nil {
return err
}
if err := rt.Start(ctx); err != nil {
return err
}
Expand Down
12 changes: 12 additions & 0 deletions internal/runtime/duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package runtime

import "time"

// DurationMillis converts a duration to fractional milliseconds for recording
// on the startup-latency histograms. It divides the nanosecond count as a
// float so a sub-millisecond value keeps its fractional part. time.Duration's
// Milliseconds() truncates to an integer, which would drop sub-millisecond
// samples to zero and understate the histogram sum.
func DurationMillis(d time.Duration) float64 {
return float64(d.Nanoseconds()) / 1e6
}
27 changes: 27 additions & 0 deletions internal/runtime/duration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package runtime_test

import (
"testing"
"time"

"github.com/observiq/blitz/internal/runtime"
"github.com/stretchr/testify/require"
)

func TestDurationMillis(t *testing.T) {
cases := []struct {
name string
in time.Duration
want float64
}{
{"whole milliseconds", 250 * time.Millisecond, 250},
{"seconds scale", 2 * time.Second, 2000},
{"sub-millisecond preserved", 500 * time.Microsecond, 0.5},
{"zero", 0, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.InDelta(t, tc.want, runtime.DurationMillis(tc.in), 1e-9)
})
}
}
94 changes: 94 additions & 0 deletions internal/runtime/monitoring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// DO NOT MODIFY: This code is autogenerated by "make generate-o11y".
// See templates/registry/go/metric.go.j2.

//nolint:unused,revive
package runtime

import (
"context"
"errors"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)

// Attribute name constants
const (
attrModuleName = "module_name"
)

// Constants for enum members

// Wrapper types for metrics with required attributes

// blitzModuleStartupDurationHistogramType wraps the blitz.module.startup.duration metric with type-safe required attributes
type blitzModuleStartupDurationHistogramType struct {
histogram metric.Float64Histogram
}

// Record records a value for blitz.module.startup.duration with required attributes
func (m blitzModuleStartupDurationHistogramType) Record(ctx context.Context, value float64, moduleName string, opts ...metric.RecordOption) {
attrs := metric.WithAttributeSet(attribute.NewSet(
attribute.String(attrModuleName, moduleName),
))

if len(opts) == 0 {
m.histogram.Record(ctx, value, attrs)
} else {
allOpts := append([]metric.RecordOption{attrs}, opts...)
m.histogram.Record(ctx, value, allOpts...)
}
}

// Metrics holds blitz's runtime self-telemetry instruments. Build one
// with NewMetrics from a caller-supplied MeterProvider so metrics can be routed
// to any provider (an embedding host's, or the process global).
type Metrics struct {
runtimeMeter metric.Meter

// time to start a single module during session startup
BlitzModuleStartupDurationHistogram blitzModuleStartupDurationHistogramType
// time to start all modules in the session
BlitzSessionStartupDurationHistogram metric.Float64Histogram
// time from process start to the service running
BlitzStartupDurationHistogram metric.Float64Histogram
}

// NewMetrics builds the runtime instruments from mp. A nil mp falls
// back to the process-global MeterProvider, preserving standalone behavior.
func NewMetrics(mp metric.MeterProvider) (*Metrics, error) {
if mp == nil {
mp = otel.GetMeterProvider()
}
m := &Metrics{}
var errs error

m.runtimeMeter = mp.Meter("runtime")

BlitzModuleStartupDurationHistogramRaw, err := m.runtimeMeter.Float64Histogram(
"blitz.module.startup.duration",
metric.WithDescription("time to start a single module during session startup"),
metric.WithUnit("ms"),
)
errs = errors.Join(errs, err)
m.BlitzModuleStartupDurationHistogram = blitzModuleStartupDurationHistogramType{histogram: BlitzModuleStartupDurationHistogramRaw}

BlitzSessionStartupDurationHistogramRaw, err := m.runtimeMeter.Float64Histogram(
"blitz.session.startup.duration",
metric.WithDescription("time to start all modules in the session"),
metric.WithUnit("ms"),
)
errs = errors.Join(errs, err)
m.BlitzSessionStartupDurationHistogram = BlitzSessionStartupDurationHistogramRaw

BlitzStartupDurationHistogramRaw, err := m.runtimeMeter.Float64Histogram(
"blitz.startup.duration",
metric.WithDescription("time from process start to the service running"),
metric.WithUnit("ms"),
)
errs = errors.Join(errs, err)
m.BlitzStartupDurationHistogram = BlitzStartupDurationHistogramRaw

return m, errs
}
78 changes: 78 additions & 0 deletions internal/runtime/monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Runtime Metrics Reference

## Quick Reference

| Metric | Type | Unit | Description |
|--------|------|------|-------------|
| [`blitz.module.startup.duration`](#blitzmodulestartupduration) | Histogram | `ms` | time to start a single module during session startup |
| [`blitz.session.startup.duration`](#blitzsessionstartupduration) | Histogram | `ms` | time to start all modules in the session |
| [`blitz.startup.duration`](#blitzstartupduration) | Histogram | `ms` | time from process start to the service running |

---

## Metrics Detail

### blitz.module.startup.duration

| Property | Value |
|----------|-------|
| **Type** | Histogram |
| **Unit** | `ms` |
| **Meter** | `runtime` |
| **Stability** | Stable |
| **Description** | time to start a single module during session startup |
| **Attributes** |`module_name` |

**Attributes:**

| Name | Type | Required | Values |
|------|------|----------|--------|
| `module_name` | string | ✓ | - |

**Usage:**
```go
// Type-safe wrapper with required attributes
blitzModuleStartupDurationHistogram.Record(ctx, 1, moduleNameValue)
```

---

### blitz.session.startup.duration

| Property | Value |
|----------|-------|
| **Type** | Histogram |
| **Unit** | `ms` |
| **Meter** | `runtime` |
| **Stability** | Stable |
| **Description** | time to start all modules in the session |

**Usage:**
```go
blitzSessionStartupDurationHistogram.Record(ctx, 1)
```

---

### blitz.startup.duration

| Property | Value |
|----------|-------|
| **Type** | Histogram |
| **Unit** | `ms` |
| **Meter** | `runtime` |
| **Stability** | Stable |
| **Description** | time from process start to the service running |

**Usage:**
```go
blitzStartupDurationHistogram.Record(ctx, 1)
```

---



---

**Generated:** `make generate-o11y` | **Registry:** `runtime/monitoring/` | **Templates:** `weaver/templates/`
44 changes: 44 additions & 0 deletions internal/runtime/monitoring/metric.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/open-telemetry/weaver/refs/heads/main/schemas/semconv.schema.json
groups:
- id: runtime.module.startup.duration
type: metric
metric_name: "blitz.module.startup.duration"
stability: stable
brief: "time to start a single module during session startup"
instrument: histogram
unit: "ms"
annotations:
exported: true
histogram:
type: Float64Histogram
attributes:
- id: module_name
type: string
stability: stable
requirement_level: required
brief: "name of the module"
examples: ["apache-common", "json", "hostmetrics", "traces"]

- id: runtime.session.startup.duration
type: metric
metric_name: "blitz.session.startup.duration"
stability: stable
brief: "time to start all modules in the session"
instrument: histogram
unit: "ms"
annotations:
exported: true
histogram:
type: Float64Histogram

- id: runtime.startup.duration
type: metric
metric_name: "blitz.startup.duration"
stability: stable
brief: "time from process start to the service running"
instrument: histogram
unit: "ms"
annotations:
exported: true
histogram:
type: Float64Histogram
4 changes: 4 additions & 0 deletions internal/runtime/monitoring/registry_manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: runtime
description: Runtime Lifecycle Metrics
semconv_version: 0.1.0
schema_base_url: https://github.com/observiq/blitz/schemas/runtime/
23 changes: 18 additions & 5 deletions internal/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package runtime
import (
"context"
"fmt"
"time"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -47,6 +49,7 @@ type Runtime struct {
logger *zap.Logger
modules []Module
tracer trace.Tracer
metrics *Metrics

// sessionSpan and moduleSpans hold the open self-telemetry spans between
// Start and Stop. Start and Stop are called once each and never
Expand All @@ -56,34 +59,42 @@ type Runtime struct {
moduleSpans []trace.Span
}

// New returns a Runtime configured with the given logger, modules, and tracer
// provider. A nil tracerProvider falls back to the process global, so span
// emission is always safe.
func New(logger *zap.Logger, modules []Module, tracerProvider trace.TracerProvider) *Runtime {
// New returns a Runtime configured with the given logger, modules, tracer
// provider, and meter provider. A nil tracerProvider or meterProvider falls
// back to the process global, so emission is always safe. It returns an error
// only if the runtime's own metric instruments cannot be built.
func New(logger *zap.Logger, modules []Module, tracerProvider trace.TracerProvider, meterProvider metric.MeterProvider) (*Runtime, error) {
if logger == nil {
logger = zap.NewNop()
}
if tracerProvider == nil {
tracerProvider = otel.GetTracerProvider()
}
metrics, err := NewMetrics(meterProvider)
if err != nil {
return nil, fmt.Errorf("build runtime metrics: %w", err)
}
return &Runtime{
logger: logger,
modules: modules,
tracer: tracerProvider.Tracer(tracerScope),
}
metrics: metrics,
}, nil
}

// Start begins every configured module. If any module's Start returns
// an error, Start stops the modules already started (in reverse order)
// and returns the failure.
func (r *Runtime) Start(ctx context.Context) error {
sessionStart := time.Now()
ctx, r.sessionSpan = r.tracer.Start(ctx, "blitz.session")
r.moduleSpans = make([]trace.Span, 0, len(r.modules))

started := make([]Module, 0, len(r.modules))
for _, m := range r.modules {
mctx, mspan := r.tracer.Start(ctx, "blitz.generator.run",
trace.WithAttributes(attribute.String("blitz.generator.name", m.Name())))
moduleStart := time.Now()
if err := m.Start(mctx); err != nil {
mspan.End()
// Roll back: stop modules already started, in reverse order.
Expand All @@ -98,9 +109,11 @@ func (r *Runtime) Start(ctx context.Context) error {
r.sessionSpan.End()
return fmt.Errorf("start module %s: %w", m.Name(), err)
}
r.metrics.BlitzModuleStartupDurationHistogram.Record(ctx, DurationMillis(time.Since(moduleStart)), m.Name())
started = append(started, m)
r.moduleSpans = append(r.moduleSpans, mspan)
}
r.metrics.BlitzSessionStartupDurationHistogram.Record(ctx, DurationMillis(time.Since(sessionStart)))
return nil
}

Expand Down
Loading
Loading