Skip to content

Structured Logging

Slingshot emits one structured JSON log entry per HTTP request. The shape is designed for ingestion by Datadog, CloudWatch, Loki, Pino-compatible viewers — anything that parses JSON. Every entry carries the request ID, actor, tenant, response time, and active trace, so a single grep across the log stream tells you everything about a request.

The requestLogger middleware is mounted by the framework when logging.enabled is left at its default (true). It runs after your handler resolves, derives the log level from the status code (info for 2xx/3xx, warn for 4xx, error for 5xx and unhandled exceptions), builds a RequestLogEntry, and hands it to onLog. The default onLog is console.log(JSON.stringify(entry)) — replace it to ship to your own sink.

/health, /docs, /openapi.json, and /metrics are excluded by default; they are too noisy to be useful and would drown out the requests you actually care about.

app.config.ts
import { defineApp } from '@lastshotlabs/slingshot';
export default defineApp({
logging: {
enabled: true,
level: 'info',
excludePaths: ['/health', '/internal', /^\/admin\/_static/],
excludeMethods: ['OPTIONS'],
onLog: entry => {
// entry is a fully-typed RequestLogEntry
myLogger.write(entry);
},
},
});
declare const myLogger: { write(entry: unknown): void };

level filters out entries below the threshold — set it to warn in dev to silence chatter, or to info in prod for full visibility. excludePaths accepts strings (prefix match) or RegExp.

Every RequestLogEntry includes:

  • requestId — the server-generated UUID set by the request ID middleware
  • userId, sessionId — resolved from the actor, null if unauthenticated
  • requestTenantId — request-scoped tenant ID (distinct from actor.tenantId)
  • traceId, spanId — populated when OTel tracing is active
  • responseTime — round-trip in milliseconds
  • ip, userAgent, method, path, statusCode
  • err: { message, stack } — only when the handler threw

That gives you everything you need to reconstruct a request without needing a separate APM.

For non-request logs from framework helpers and plugins, use log():

import { log } from '@lastshotlabs/slingshot';
log('billing: refund issued', { invoiceId: '123' });

log() writes to console.log only when logging.verbose is true — by default that follows LOGGING_VERBOSE=true or NODE_ENV !== 'production'. Production stays quiet unless you opt in.