2026-03-10 00:00:00 -07:00
# Observe Mode
Runtime visibility and drift detection without blocking by default.
2026-05-22 14:00:47 -07:00
APOPHIS observe is **programmatic only ** . Register the APOPHIS plugin with `observe.enabled: true` and `observe.sinks` to emit contract pass/violation/error events from live traffic without blocking responses. Sampling controls the fraction of observed requests.
2026-03-10 00:00:00 -07:00
2026-05-22 14:00:47 -07:00
Use `apophis doctor --mode observe` to validate your observe configuration (sinks, sampling, blocking, production safety) before deploying.
2026-03-10 00:00:00 -07:00
## 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: true` in 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:
``` javascript
environments : {
staging : {
name : 'staging' ,
2026-05-22 17:07:12 -07:00
allowedModes : [ 'verify' , 'observe' ] ,
blockQualify : true ,
requireSink : true ,
allowBlocking : false
2026-03-10 00:00:00 -07:00
}
}
```
2026-05-22 17:07:12 -07:00
You implement sinks for your observability backend. Common categories:
2026-03-10 00:00:00 -07:00
2026-05-22 17:07:12 -07:00
- **Logs**: Structured logging of contract violations (pino, winston)
- **Metrics**: Counter and histogram metrics for violation rates (Prometheus, OpenTelemetry)
- **Traces**: Distributed tracing integration for violation context (OpenTelemetry, Jaeger)
2026-03-10 00:00:00 -07:00
## Sampling
Control observation overhead with sampling:
``` javascript
profiles : {
'staging-observe' : {
name : 'staging-observe' ,
mode : 'observe' ,
preset : 'platform-observe' ,
routes : [ ]
}
}
```
2026-03-10 00:00:00 -07:00
The `platform-observe` preset enables sampling. Configure the rate explicitly:
``` javascript
profiles : {
'staging-observe' : {
mode : 'observe' ,
preset : 'platform-observe' ,
routes : [ ] ,
sampling : 1.0 // 100% of requests observed
}
}
```
2026-03-10 00:00:00 -07:00
## Staging vs Production
| Environment | Blocking | Sampling | Sink Required |
|---|---|---|---|
2026-03-10 00:00:00 -07:00
| Staging | No (default) | 100% | Yes |
| Production | No (default) | 100% | Yes |
Default is `1.0` (100%). Configure lower rates for production explicitly:
``` javascript
profiles : {
'prod-observe' : {
mode : 'observe' ,
preset : 'platform-observe' ,
routes : [ ] ,
sampling : 0.1 // 10% of requests observed
}
}
```
2026-03-10 00:00:00 -07:00
2026-05-22 14:00:47 -07:00
## Config Validation
2026-03-10 00:00:00 -07:00
2026-05-22 14:00:47 -07:00
Validate your observe config before deployment with doctor:
2026-03-10 00:00:00 -07:00
2026-05-22 17:07:12 -07:00
## Validation (via Doctor)
Validate observe configuration with `apophis doctor --mode observe` :
2026-03-10 00:00:00 -07:00
| Code | Meaning |
2026-05-22 17:07:12 -07:00
|---|---|---|
2026-03-10 00:00:00 -07:00
| 0 | Observe config is valid and safe |
| 2 | Safety violation or invalid config |
## Config Example
``` javascript
// 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
}
}
} ;
```
2026-03-10 00:00:00 -07:00
2026-05-21 20:39:36 -07:00
## Programmatic Runtime Activation
2026-03-10 00:00:00 -07:00
2026-05-22 14:00:47 -07:00
Register APOPHIS with observe options in your application before routes are registered.
2026-03-10 00:00:00 -07:00
2026-05-21 20:39:36 -07:00
``` typescript
import Fastify from 'fastify'
import apophisPlugin from '@apophis/fastify'
2026-05-22 11:05:52 -07:00
import type { ObserveSink , ObserveEvent } from '@apophis/fastify'
2026-05-21 20:39:36 -07:00
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' )
}
} ,
2026-03-10 00:00:00 -07:00
}
2026-05-22 11:05:52 -07:00
// 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:
``` typescript
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 ] ,
} ,
} ,
} )
2026-03-10 00:00:00 -07:00
```
2026-05-21 20:39:36 -07:00
Key constraints:
- Sink `emit()` can be sync or async (returns `void | Promise<void>` ).
- Sink rejections and thrown errors are silently caught — they never affect the route response or status code.
2026-05-22 11:05:52 -07:00
- In production, observe hooks still run when `observe.enabled` and `observe.sinks` are configured; blocking runtime validation does not.
2026-05-21 20:39:36 -07:00
- Sampling is applied per-formula evaluation via `Math.random() < sampling` .
At `sampling: 1` every formula is emitted. At `sampling: 0` nothing 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
2026-05-22 15:00:04 -07:00
plug in any backend.
2026-05-21 20:39:36 -07:00
2026-05-22 15:00:04 -07:00
### OpenTelemetry
Export contract evaluations as OTel custom metrics alongside your existing spans and traces:
``` typescript
import type { ObserveSink , ObserveEvent } from '@apophis/fastify'
import { metrics } from '@opentelemetry/api'
const meter = metrics . getMeter ( 'apophis' )
const contractCounter = meter . createCounter ( 'apophis.contract.evaluations' , {
description : 'Number of contract evaluations' ,
} )
const violationCounter = meter . createCounter ( 'apophis.contract.violations' , {
description : 'Number of contract violations' ,
} )
const durationHistogram = meter . createHistogram ( 'apophis.contract.duration_ms' , {
description : 'Contract evaluation duration' ,
} )
export const otelSink : ObserveSink = {
emit ( event : ObserveEvent ) {
const labels = { route : event.route , formula : event.formula }
contractCounter . add ( 1 , labels )
if ( event . type === 'contract.violation' ) {
violationCounter . add ( 1 , labels )
}
durationHistogram . record ( event . durationMs , labels )
} ,
}
```
Register it with your Fastify app:
``` typescript
await app . register ( apophisPlugin , {
observe : { enabled : true , sinks : [ otelSink ] } ,
} )
```
APOPHIS violations now appear in your OTel metrics backend (Prometheus, Datadog, New Relic, etc.) alongside latency and error data — no custom collector needed.
### Console (development / debugging)
``` typescript
export const consoleSink : ObserveSink = {
emit ( event : ObserveEvent ) {
if ( event . type === 'contract.violation' ) {
console . warn ( '[apophis]' , event . route , event . formula , event . observed )
}
} ,
}
```
### pino logger
``` typescript
export function createPinoSink ( logger : pino.Logger ) : ObserveSink {
return {
emit ( event : ObserveEvent ) {
logger [ event . type === 'contract.violation' ? 'warn' : 'info' ] ( event )
} ,
}
}
```
2026-05-21 20:39:36 -07:00
2026-03-10 00:00:00 -07:00
## Monorepo Validation
2026-05-22 14:00:47 -07:00
For monorepos, use `apophis doctor --mode observe --workspace` to validate observe configuration across all workspace packages.
2026-03-10 00:00:00 -07:00
## Mode Mismatch
2026-05-22 14:00:47 -07:00
Profiles configured for `verify` mode will be rejected at runtime. Only profiles with `mode: 'observe'` are valid for observe mode.
2026-03-10 00:00:00 -07:00
```