7.2 KiB
Observe Mode
Runtime visibility and drift detection without blocking by default.
APOPHIS observe has two paths:
-
CLI
apophis observe: Validates observe configuration readiness (policy, sinks, sampling, safety boundaries). Introduces no service process or runtime hooks. Use this for CI config validation before deployment. -
Programmatic runtime observation: Register the APOPHIS plugin with
observe.enabled: trueandobserve.sinksto emit contract pass/violation/error events from live traffic without blocking responses. Sampling controls the fraction of observed requests.
When to Use It
- Staging: Validate observe config before promoting to production
- Production: Monitor contract drift without affecting requests
- Platform teams: Centralized visibility across services
Safety Boundaries
Observe mode is non-blocking by default:
- Non-blocking by default: Contract violations are logged, not thrown
- No request failures in non-blocking mode: Violations are reported instead of thrown
- Explicit opt-in for blocking: Requires
allowBlocking: truein environment policy - Production gating: Blocking behavior is blocked in production by default
Sink Configuration
Observe mode requires a reporting sink. Configure it in your environment policy:
environments: {
staging: {
name: 'staging',
allowVerify: true,
allowObserve: true,
allowQualify: false,
allowChaos: false,
allowBlocking: false,
requireSink: true
}
}
APOPHIS supports these sink types:
- Logs: Structured logging of contract violations
- Metrics: Counter and histogram metrics for violation rates
- Traces: Distributed tracing integration for violation context
Sampling
Control observation overhead with sampling:
profiles: {
'staging-observe': {
name: 'staging-observe',
mode: 'observe',
preset: 'platform-observe',
routes: []
}
}
The platform-observe preset enables sampling. Configure the rate explicitly:
profiles: {
'staging-observe': {
mode: 'observe',
preset: 'platform-observe',
routes: [],
sampling: 1.0 // 100% of requests observed
}
}
Staging vs Production
| Environment | Blocking | Sampling | Sink Required |
|---|---|---|---|
| Staging | No (default) | 100% | Yes |
| Production | No (default) | 100% | Yes |
Default is 1.0 (100%). Configure lower rates for production explicitly:
profiles: {
'prod-observe': {
mode: 'observe',
preset: 'platform-observe',
routes: [],
sampling: 0.1 // 10% of requests observed
}
}
--check-config Flag
Validate config without activating observe mode:
apophis observe --profile staging-observe --check-config
This is useful in CI to ensure observe config is valid before deployment.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Observe config is valid and safe |
| 2 | Safety violation or invalid config |
Config Example
// apophis.config.js
export default {
mode: 'observe',
profile: 'staging-observe',
profiles: {
'staging-observe': {
name: 'staging-observe',
mode: 'observe',
preset: 'platform-observe',
routes: []
}
},
presets: {
'platform-observe': {
name: 'platform-observe',
timeout: 10000,
parallel: true,
chaos: false,
observe: true
}
},
environments: {
staging: {
name: 'staging',
allowVerify: true,
allowObserve: true,
allowQualify: false,
allowChaos: false,
allowBlocking: false,
requireSink: true
},
production: {
name: 'production',
allowVerify: true,
allowObserve: true,
allowQualify: false,
allowChaos: false,
allowBlocking: false,
requireSink: true
}
}
};
Programmatic Runtime Activation
The CLI only validates configuration. To activate runtime observation, register APOPHIS with observe options in your application before routes are registered. Observe remains active in production because it is non-blocking; blocking runtime validation still stays disabled in production.
import Fastify from 'fastify'
import apophisPlugin from '@apophis/fastify'
import type { ObserveSink, ObserveEvent } from '@apophis/fastify'
const app = Fastify({ logger: true })
// Implement the ObserveSink interface.
// Capture events to your preferred observability backend.
const metricsSink: ObserveSink = {
emit(event: ObserveEvent) {
// Emit a counter for each contract evaluation
myMetrics.increment(`apophis.contract.${event.type}`, {
route: event.route,
formula: event.formula,
})
// Record duration as a histogram
myMetrics.histogram('apophis.contract.duration_ms', event.durationMs, {
route: event.route,
})
// Log high-signal violations for immediate triage
if (event.type === 'contract.violation') {
logger.warn({ event }, 'APOPHIS contract violation')
}
},
}
// Register APOPHIS with observe enabled.
// This emits non-blocking contract pass/violation/error events
// for every covered request, gated by sampling.
await app.register(apophisPlugin, {
runtime: process.env.NODE_ENV === 'test' ? 'error' : 'off',
observe: {
enabled: true,
sampling: 0.1, // observe 10% of requests
sinks: [metricsSink],
},
})
For new services, createFastify() wires discovery and APOPHIS before your
routes, which avoids the most common ordering mistake:
import { createFastify } from '@apophis/fastify'
const app = await createFastify({
logger: true,
apophis: {
runtime: process.env.NODE_ENV === 'test' ? 'error' : 'off',
observe: {
enabled: true,
sampling: 0.1,
sinks: [metricsSink],
},
},
})
Key constraints:
- Sink
emit()can be sync or async (returnsvoid | Promise<void>). - Sink rejections and thrown errors are silently caught — they never affect the route response or status code.
- In production, observe hooks still run when
observe.enabledandobserve.sinksare configured; blocking runtime validation does not. - Sampling is applied per-formula evaluation via
Math.random() < sampling. Atsampling: 1every formula is emitted. Atsampling: 0nothing is emitted. - Only routes with APOPHIS annotations (
x-ensures,x-requires) produce events. Routes without annotations are not evaluated in observe mode.
Sink Implementations
APOPHIS does not ship with built-in sinks. The ObserveSink interface lets you
plug in any backend. Common patterns:
- OpenTelemetry: emit counters and histograms via
@opentelemetry/api. - pino logger: emit structured log records via
pino.info()/pino.warn(). - Internal metrics service: POST events to an internal collector endpoint.
- In-memory ring buffer: capture recent events for diagnostics endpoints.
Monorepo Validation
For monorepos, use apophis doctor --workspace to validate observe configuration across all workspace packages. observe itself does not support --workspace; use doctor to check config in each package.
Mode Mismatch
Profiles configured for verify mode will be rejected by apophis observe. Only profiles with mode: 'observe' are valid.