@ossy/observability
v1.8.3
Published
Structured logger with pluggable backends for the Ossy platform
Readme
@ossy/observability
Structured logger with pluggable backends for the Ossy platform. The integration files in this package (console.integration.js, sentry.integration.js, grafana-loki.integration.js, grafana-metrics.integration.js) are platform primitives auto-discovered by @ossy/app build. See PRIMITIVES.md for how integrations work.
createLogger(namespace)
Returns a logger bound to a namespace string. All log methods accept an optional context object that is merged into the output.
import { createLogger } from '@ossy/observability'
const log = createLogger('my-service')
log.info('Server started', { port: 3000 })
log.warn('Deprecated config key', { key: 'FOO' })
log.error('Request failed', { url: '/api/foo' }, error)
log.debug('Processing item', { id: '123' })Methods
| Method | Signature |
|--------|-----------|
| log.info | (message: string, context?: object) => void |
| log.warn | (message: string, context?: object) => void |
| log.error | (message: string, context?: object, error?: Error) => void |
| log.debug | (message: string, context?: object) => void |
Output format
- Development (
NODE_ENV !== 'production'): pretty-printed lines with a[namespace]prefix. - Production (
NODE_ENV === 'production'): newline-delimited JSON:{ "level": "info", "namespace": "my-service", "message": "Server started", "port": 3000, "timestamp": "2026-01-01T00:00:00.000Z" }
Adding a custom backend
A backend is an object with four functions. Register it with registerBackend:
import { registerBackend } from '@ossy/observability'
registerBackend({
info(namespace, message, context) { /* ... */ },
warn(namespace, message, context) { /* ... */ },
error(namespace, message, context, error) { /* ... */ },
debug(namespace, message, context) { /* ... */ },
})All registered backends receive every log call (fan-out). The built-in console backend is always active unless you call clearBackends() first.
Built-in integrations
console.integration.js
| Field | Value |
|-------|-------|
| id | logger-console |
| credentials | (none) |
Provides the same pretty/JSON output as the default backend, but as a discoverable *.integration.js primitive. Use it when you want the platform's integration loader to manage the console backend explicitly:
import { consoleIntegration } from '@ossy/observability'
import { registerBackend, clearBackends } from '@ossy/observability'
clearBackends()
registerBackend(await consoleIntegration.connect({ env: process.env }))sentry.integration.js
| Field | Value |
|-------|-------|
| id | logger-sentry |
| credentials | SENTRY_DSN |
Forwards error-level calls to Sentry via captureException. info, warn, and debug are no-ops in this backend.
Add the integration file to your app's manifest (or import it directly) and ensure SENTRY_DSN is set in the environment:
import { sentryIntegration } from '@ossy/observability'
import { registerBackend } from '@ossy/observability'
registerBackend(await sentryIntegration.connect({ env: process.env }))grafana-loki.integration.js
| Field | Value |
|-------|-------|
| id | logger-grafana-loki |
| credentials | GRAFANA_LOKI_URL, GRAFANA_LOKI_USER, GRAFANA_LOKI_API_KEY |
Forwards every log call to a Grafana Loki instance as a structured JSON stream. Calls are fire-and-forget — Loki outages never block application code.
Each log entry is pushed as a Loki stream labelled with level and namespace, making it queryable in Grafana Explore:
{namespace="my-service"} | json
{level="error"}Setup:
import { grafanaLokiIntegration, registerBackend } from '@ossy/observability'
registerBackend(await grafanaLokiIntegration.connect({ env: process.env }))Required environment variables:
| Variable | Description |
|----------|-------------|
| GRAFANA_LOKI_URL | Full Loki push endpoint, e.g. https://logs-prod-eu-west-0.grafana.net/loki/api/v1/push |
| GRAFANA_LOKI_USER | Numeric user ID from the Grafana Cloud Loki data source page |
| GRAFANA_LOKI_API_KEY | Grafana Cloud API key with the MetricsPublisher role |
grafana-metrics.integration.js
| Field | Value |
|-------|-------|
| id | metrics-grafana |
| credentials | GRAFANA_METRICS_URL, GRAFANA_METRICS_USER, GRAFANA_METRICS_API_KEY |
Batches counter, gauge, and histogram observations in memory and flushes them every 10 seconds as a Prometheus text-format body to the configured push endpoint. No extra npm dependencies — uses native fetch only.
Setup:
import { grafanaMetricsIntegration } from '@ossy/observability'
await grafanaMetricsIntegration.connect({ env: process.env })Call connect() once at startup. This wires up the metrics singleton exported from the package and starts the flush interval.
Using the metrics client:
import { metrics } from '@ossy/observability'
metrics.increment('task.run', { task: 'resize-image' }) // counter
metrics.gauge('queue.depth', 42, { queue: 'images' }) // gauge
metrics.timing('task.duration', 1234, { task: 'resize-image' }) // histogram (ms)Metric names use dots in application code (task.run) and are normalised to underscores (task_run) in the Prometheus output.
Required environment variables:
| Variable | Description |
|----------|-------------|
| GRAFANA_METRICS_URL | Prometheus-compatible push endpoint, e.g. https://prometheus-prod-13-prod-us-east-0.grafana.net/api/prom/push |
| GRAFANA_METRICS_USER | Numeric user ID from the Grafana Cloud Prometheus data source page |
| GRAFANA_METRICS_API_KEY | Grafana Cloud API key with the MetricsPublisher role |
What appears in Grafana:
| Prometheus metric | Type | Labels |
|-------------------|------|--------|
| task_run | counter | task |
| task_duration_bucket / _sum / _count | histogram | task |
Histogram buckets are pre-defined at 10 ms, 50 ms, 100 ms, 250 ms, 500 ms, 1 s, 2.5 s, 5 s, 10 s, and +Inf. Use a PromQL query like the following to graph p95 task duration:
histogram_quantile(0.95, sum by (le, task) (rate(task_duration_bucket[5m]))) / 1000How tasks receive log automatically
@ossy/platform's TaskService injects a log instance into every task's run() call, scoped to the task's metadata.id:
// your-task.js
export const metadata = { id: 'my-task', triggers: [...] }
export async function run({ event, sdk, integrations, log }) {
log.info('Task started', { eventType: event.type })
}No extra setup needed — the logger is created automatically by the platform before dispatching the task.
