OpenTelemetry
WIPOtelInstrumentation auto-wires counters, up-down counters, and histograms onto the events bus. Redacted attributes by default.
Setup
import { metrics, trace } from '@opentelemetry/api'
import { OtelInstrumentation } from '@gentleduck/auth/telemetry/otel'
const otel = new OtelInstrumentation({
meter: metrics.getMeter('@gentleduck/auth'),
prefix: 'auth',
defaultAttributes: { service: 'api', region: 'us-east-1' },
})
const unsubscribe = otel.attach(auth.events)
// later: unsubscribe()
Requires @opentelemetry/api as a peer dependency. It is lazy-loaded
- apps that don't wire OTel pay no cost.
What it records
Every event on the bus translates to a metric:
| Event | Metric | Attributes |
|---|---|---|
session.created | auth.session.total (counter) | kind, aal, provider |
session.created | auth.session.active (up-down counter, +1) | - |
session.revoked | auth.session.active (up-down counter, -1) | - |
session.rotated | auth.session.rotations.total | reason |
password.changed | auth.password.changes.total | - |
password.reset | auth.password.resets.total | - |
mfa.enrolled | auth.mfa.enrolled.total | factor |
mfa.verified | auth.mfa.verified.total | factor |
anomaly | auth.anomaly.signals.total | detector, score |
anomaly | auth.anomaly.decisions.total | decision (allow/step-up/deny) |
lockout | auth.lockout.total | reason |
identity.created | auth.identity.signups.total | - |
Plus a histogram per provider:
| Histogram | Attributes |
|---|---|
auth.signin.duration_ms | provider, result (success/failure), code |
auth.provider.callback.duration_ms | provider |
Tracing
For trace-level observability, duck-auth emits a span around each provider call:
provider.password.complete -> trace.spanId, trace.traceId
|-- store.identity.findByEmail
|-- store.credential.findByHashedSecret
|-- hasher.verify
\-- session.rotateOrCreate
Spans carry attributes:
auth.provider_idauth.flow(signin/signout/refresh/provider-begin)auth.result(success/failure)auth.code(theAUTH/*code on failure)
PII attributes (email, ip, userAgent, profile fields) are
redacted by default - the OTel instrumentation runs every span
attribute through a denylist before emission. To override:
new OtelInstrumentation({
meter,
redact: {
extraDenylist: ['custom-sensitive-field'],
allowlist: [], // never include these - even if you ask
},
})
SOC2 / HIPAA
The SOC2 and HIPAA compliance presets configure OTel with the audit-log sink wired in. You get:
- Every event emitted to OTel.
- Every event also delivered via
AuthWebhookDelivererto a signed audit log endpoint. - PII redaction enforced at every layer.
import { authApplyCompliancePreset } from '@gentleduck/auth/core'
const cfg = authApplyCompliancePreset(base, 'soc2')
// result.events.middleware includes the PII redaction layer.
Dashboards
The metric names follow OTel semconv conventions where possible. A
ready-made Grafana dashboard JSON ships under
@gentleduck/auth/telemetry/otel/dashboards/grafana.json - import it
into your Grafana instance to get:
- Sign-in rate / failure rate, by provider.
- Active sessions, by AAL.
- Lockout count, with annotations for hijack-policy fires.
- p50/p95/p99 sign-in latency, by provider.
- Anomaly signal distribution.
Tag the dashboard's data source variable with your OTel collector's Prometheus endpoint.
Custom sinks
If you don't use OpenTelemetry, the same metrics can be wired by hand:
auth.events.on('session.created', () => {
statsd.increment('auth.session.total', 1, ['aal:aal1'])
})
auth.events.on('lockout', (event) => {
pagerduty.trigger('auth-lockout', {
severity: 'warning',
custom_details: event,
})
})
Pick whichever observability stack you already operate.