@orionaisystems/betterlogs
v0.12.0
Published
A durable, context-aware logger for Node.js and browser TypeScript projects.
Maintainers
Readme
@orionaisystems/betterlogs
@orionaisystems/betterlogs is a reusable logging package for TypeScript projects that want elegant console output locally and stronger delivery guarantees in production. It keeps the default developer experience clean while supporting scoped child loggers, structured metadata, redaction, async context propagation, durable batching, durable spool inspection, pluggable transports, broker helpers, health-aware delivery, exporter-friendly diagnostics, diagnostics endpoint adapters, generic production presets, and framework adapters.
Installation
npm install @orionaisystems/betterlogsFor browser-focused usage, import the dedicated subpath:
import { createBrowserLogger } from "@orionaisystems/betterlogs/browser";Quick Start
import {
createAsyncContextBindingsProvider,
createDefaultRedactionRules,
createLogger,
createPartialKeyRedactionRule,
runWithLogContext
} from "@orionaisystems/betterlogs";
const log = createLogger({
scope: "api",
minLevel: "debug",
bindingsProvider: createAsyncContextBindingsProvider(),
redact: [
...createDefaultRedactionRules(),
createPartialKeyRedactionRule(["email"], {
keepStart: 2,
keepEnd: 10
})
]
});
await runWithLogContext(
{
requestId: "req_123",
correlationId: "corr_987",
context: {
route: "/users",
tenantId: "team_42"
}
},
async () => {
const requestLog = log.child("users");
const timer = requestLog.time("User created", {
id: "u_123"
});
timer.finish({
level: "success",
meta: {
email: "[email protected]",
password: "demo-password-value"
}
});
await requestLog.flush();
}
);Example output:
2026-03-30 11:45:22 SUCCESS [api:users] [req:req_123] [corr:corr_987] User created
{
"meta": {
"durationMs": 18,
"email": "us********ple.com",
"id": "u_123",
"password": "[REDACTED]"
}
}Why BetterLogs
- clean human-readable pretty output by default
- structured JSON formatting and flattening for ingestion pipelines and MCP-oriented consumers
- async-aware logger bindings for request IDs, correlation IDs, and shared context
- durable batching with spool-file persistence and acknowledgement-aware retries
- CLI and programmatic inspection for durable spool, rotated log, and archive JSONL files
- built-in buffering, file output, file rotation, archival retention, HTTP delivery, and queue helpers
- transport retry, health tracking, health transition hooks, circuit breaker wrapping, Prometheus-friendly diagnostics, and explicit
flush()support - framework adapters for Express-style, Fastify-style, Koa-style, and fetch-style runtimes
- redaction helpers, OpenTelemetry bridge utilities, and test-friendly memory transports
API Overview
createLogger(options?)
Creates a logger instance.
import { createLogger } from "@orionaisystems/betterlogs";
const log = createLogger();Logger Methods
log.trace(message, meta?);
log.debug(message, meta?);
log.info(message, meta?);
log.success(message, meta?);
log.warn(message, meta?);
log.error(message, meta?);
log.fatal(message, meta?);
log.child(scope);
log.withContext(context);
log.withRequestId(requestId);
log.withCorrelationId(correlationId);
log.withBindings(bindings);
const timer = log.time(message, meta?);
await log.flush();flush() waits for async hooks and transports, then asks flush-capable transports to drain buffered work.
Logger Options
type LoggerOptions = {
scope?: string;
minLevel?: LogLevel;
timestamps?: boolean;
colors?: boolean;
prettyPrintObjects?: boolean;
showStackTrace?: boolean;
format?: "pretty" | "json" | "browser";
formatter?: LogFormatter;
transports?: LogTransport | LogTransport[];
hooks?: LogHook | LogHook[];
sample?: LogSampler | LogSampler[];
serializers?: LogSerializer | LogSerializer[];
redact?: LogRedactionRule | LogRedactionRule[];
context?: Record<string, unknown>;
requestId?: string;
correlationId?: string;
bindingsProvider?: LoggerBindingsProvider;
};| Option | Type | Default | Description |
| --- | --- | --- | --- |
| scope | string | undefined | Adds a scope block like [api] to every entry. |
| minLevel | LogLevel | "info" | Filters out logs below the configured severity. |
| timestamps | boolean | true | Prints a local timestamp at the start of each entry. |
| colors | boolean | true | Enables colored pretty output for the main Node entry point. |
| prettyPrintObjects | boolean | true | Uses multi-line formatting for structured details. |
| showStackTrace | boolean | true | Includes stacks when logging Error objects. |
| format | "pretty" \| "json" \| "browser" | "pretty" | Chooses the default formatter used by the built-in console transport. |
| formatter | LogFormatter | undefined | Overrides the default formatter for the built-in console transport. |
| transports | LogTransport \| LogTransport[] | built-in console transport | Supplies custom transports. Pass [] to disable default output. |
| hooks | LogHook \| LogHook[] | [] | Observes each LogRecord before transports run. Hooks may be async. |
| sample | LogSampler \| LogSampler[] | [] | Drops records before hooks and transports run. Useful for cost control, high-volume debug logs, and burst protection. |
| serializers | LogSerializer \| LogSerializer[] | [] | Custom serializers for domain objects in metadata or context. |
| redact | LogRedactionRule \| LogRedactionRule[] | [] | Redaction rules for sensitive keys or exact paths. Supports full replacement and partial masking. |
| context | Record<string, unknown> | {} | Attaches reusable structured context to every entry. |
| requestId | string | undefined | Adds a request ID tag and top-level record field. |
| correlationId | string | undefined | Adds a correlation ID tag and top-level record field. |
| bindingsProvider | LoggerBindingsProvider | undefined | Pulls bindings from async context or another ambient source for every record. |
Async Context Propagation
Use the built-in async context store when you want request and correlation IDs to flow through asynchronous work automatically.
import {
createAsyncContextBindingsProvider,
createLogger,
runWithLogContext
} from "@orionaisystems/betterlogs";
const log = createLogger({
scope: "worker",
bindingsProvider: createAsyncContextBindingsProvider()
});
await runWithLogContext(
{
requestId: "req_123",
correlationId: "corr_987",
context: {
jobId: "job_42"
}
},
async () => {
log.info("Processing started");
await Promise.resolve();
log.success("Processing finished");
}
);If you need more control, BetterLogs also exports createLogContextStore(), bindLogContext(), enterLogContext(), and getLogContext().
Request Timing Helpers
Use time() when you want a structured duration without hand-rolling start and end timestamps.
const timer = log.time("Tool call processed", {
tool: "search"
});
try {
timer.finish({
level: "success",
meta: {
tokenCount: 1_234
}
});
} catch (error) {
timer.fail(error as Error, {
message: "Tool call failed"
});
}Redaction Helpers
Use key or path based rules to protect secrets and PII before records reach hooks, transports, or formatters.
import {
createDefaultRedactionRules,
createKeyRedactionRule,
createPartialKeyRedactionRule,
createPathRedactionRule,
createLogger
} from "@orionaisystems/betterlogs";
const log = createLogger({
redact: [
...createDefaultRedactionRules(),
createKeyRedactionRule(["authorization", "cookie"]),
createPartialKeyRedactionRule(["email"], {
keepStart: 2,
keepEnd: 10
}),
createPathRedactionRule("meta.user.ssn")
]
});Durable Batching With Acknowledgement-Aware Retries
Use durable batching when you want a transport to persist records locally before attempting delivery.
import {
createDurableBatchingTransport,
createKafkaTransport,
createLogger
} from "@orionaisystems/betterlogs";
const kafkaTransport = createKafkaTransport({
producer: {
async send(input) {
void input;
}
},
topic: "app.logs",
key: (record) => record.requestId
});
const durableTransport = createDurableBatchingTransport({
filePath: "./.betterlogs/spool.jsonl",
maxBatchSize: 50,
retry: {
retries: 5,
baseDelayMs: 250,
maxDelayMs: 2_000
},
async sink(records) {
for (const record of records) {
await kafkaTransport.write(record);
}
return {
acknowledgedCount: records.length
};
}
});
const log = createLogger({
scope: "pipeline",
transports: [durableTransport]
});If the sink only acknowledges part of a batch, BetterLogs keeps the remaining records in the spool file for the next flush cycle.
Durable Spool Inspection CLI
BetterLogs ships a small Node CLI for inspecting JSONL spool files, rotated log files, and archive directories without writing a one-off script.
npx @orionaisystems/betterlogs inspect ./.betterlogs/spool.jsonl
npx @orionaisystems/betterlogs inspect ./logs/archive --limit 5 --jsonThe human output summarizes total records, invalid JSONL lines, timestamp windows, levels, scopes, request IDs, and recent records per file. --json prints the same inspection result as structured data for operational scripts.
The same inspection logic is available from the root package when an application wants to build its own operator surface:
import { inspectDurableLogPaths } from "@orionaisystems/betterlogs";
const inspection = await inspectDurableLogPaths(["./.betterlogs/spool.jsonl"], {
limit: 20
});
console.log(inspection.totalRecordCount);Transport Health And Circuit Breakers
Wrap transports when you want delivery diagnostics or a guardrail around unstable downstream systems.
import {
createCircuitBreakerTransport,
createHealthTrackedTransport,
createHttpTransport,
createLogger,
createTransportDiagnosticsSnapshot,
formatTransportDiagnosticsAsPrometheus,
getTransportHealth
} from "@orionaisystems/betterlogs";
const delivery = createCircuitBreakerTransport({
name: "log-ingest",
transport: createHealthTrackedTransport({
name: "log-ingest",
transport: createHttpTransport({
url: "https://logs.internal.example/ingest"
})
}),
failureThreshold: 3,
resetTimeoutMs: 5_000,
onStateChange(transition) {
console.warn("Log delivery state changed", {
from: transition.previousState,
to: transition.currentState,
reason: transition.reason
});
}
});
const log = createLogger({ transports: [delivery] });
log.info("Queued for delivery");
await log.flush();
const health = getTransportHealth(delivery);
const diagnostics = createTransportDiagnosticsSnapshot([delivery], {
labels: {
service: "api"
}
});
console.log(formatTransportDiagnosticsAsPrometheus(diagnostics));Health-aware transports expose state such as healthy, degraded, unhealthy, open, and half-open alongside counters and timestamps.
createTransportDiagnosticsSnapshot() turns one or more health-aware transports into JSON-safe diagnostics with ISO timestamps, Unix millisecond fields, success and failure ratios, writability, open-circuit timing, aggregate status, and labels. formatTransportDiagnosticsAsPrometheus() renders the same snapshot as text metrics for lightweight scrape endpoints without adding a metrics SDK dependency. The Prometheus formatter includes aggregate snapshot metrics plus per-transport metrics.
Diagnostics Endpoint Adapters
Use the endpoint helpers when a service wants to expose JSON diagnostics or Prometheus metrics without adding a framework package to BetterLogs.
import {
createExpressTransportDiagnosticsHandler,
createFetchTransportDiagnosticsHandler
} from "@orionaisystems/betterlogs";
export const fetchMetrics = createFetchTransportDiagnosticsHandler([delivery], {
format: "prometheus",
statusCode: "from-health"
});
app.get(
"/internal/logging/diagnostics",
createExpressTransportDiagnosticsHandler([delivery], {
format: "json",
statusCode: "from-health"
})
);The same adapter family includes createFastifyTransportDiagnosticsHandler() and createKoaTransportDiagnosticsMiddleware(). By default, diagnostics endpoints return HTTP 200 so scrapers can collect degraded-state payloads; set statusCode: "from-health" when an unhealthy logging pipeline should return 503.
Production Logging Preset
Services can use the production preset when they want the common BetterLogs stack without redoing the transport wiring in every gateway or worker.
import {
createFetchTransportDiagnosticsHandler,
createProductionLoggingPreset,
formatTransportDiagnosticsAsPrometheus
} from "@orionaisystems/betterlogs";
const logging = createProductionLoggingPreset({
scope: "api-gateway",
serviceName: "api-gateway",
serviceVersion: process.env.APP_VERSION,
environment: process.env.NODE_ENV,
http: {
url: process.env.LOG_INGEST_URL!,
headers: {
"x-log-source": "api-gateway"
}
},
durable: {
filePath: "./.betterlogs/api-gateway-spool.jsonl",
maxBatchSize: 25,
flushIntervalMs: 1_000
},
circuitBreaker: {
onStateChange(transition) {
console.warn("Log delivery state changed", {
currentState: transition.currentState,
previousState: transition.previousState,
reason: transition.reason,
transport: transition.name
});
}
},
debugBurstLimit: {
maxRecords: 100,
intervalMs: 60_000
}
});
logging.logger.info("Task accepted", {
taskId: "task_001"
});
await logging.flush();
const metrics = formatTransportDiagnosticsAsPrometheus(logging.getDiagnostics());
export const metricsHandler = createFetchTransportDiagnosticsHandler(
logging.healthTransports,
{
format: "prometheus",
statusCode: "from-health"
}
);The preset configures JSON output, async request/correlation context, default redaction, optional durable HTTP delivery, health tracking, circuit breaking, debug burst control, and reusable delivery diagnostics. Pass console: false when the service should only emit through the configured production transport.
Consumer Wrapper Pattern
Public packages and applications should usually expose their own logging boundary instead of importing BetterLogs everywhere. Put app-specific scopes, labels, environment variable names, diagnostics routes, and additional redaction rules in that local wrapper. Keep BetterLogs as the generic transport, formatting, context, diagnostics, and redaction foundation underneath.
Queue And Broker Helpers
BetterLogs ships lightweight helpers for common broker-style destinations without taking hard dependencies on their SDKs.
Generic Queue Transport
import { createLogger, createQueueTransport } from "@orionaisystems/betterlogs";
const queueLog = createLogger({
transports: [
createQueueTransport({
async send(payload) {
void payload;
}
})
]
});SQS Helper
import { createLogger, createSqsTransport } from "@orionaisystems/betterlogs";
const sqsLog = createLogger({
transports: [
createSqsTransport({
client: {
async sendMessage(input) {
void input;
}
},
queueUrl: "https://sqs.us-east-1.amazonaws.com/123456789012/app-logs",
messageGroupId: (record) => record.requestId ?? "default"
})
]
});Kafka Helper
import { createKafkaTransport, createLogger } from "@orionaisystems/betterlogs";
const kafkaLog = createLogger({
transports: [
createKafkaTransport({
producer: {
async send(input) {
void input;
}
},
topic: "app.logs",
key: (record) => record.requestId
})
]
});BullMQ Helper
import { createBullMqTransport, createLogger } from "@orionaisystems/betterlogs";
const workerLog = createLogger({
transports: [
createBullMqTransport({
queue: {
async add(name, data, opts) {
void name;
void data;
void opts;
}
},
mode: "record",
name: (record) => `${record.level}.log`
})
]
});JSON Flattening
When logs are headed to ingestion pipelines, queue consumers, or an MCP layer, flattened JSON can be easier to query and route.
import { createJsonFormatter } from "@orionaisystems/betterlogs";
const formatter = createJsonFormatter({
flatten: {
enabled: true,
include: ["context", "meta", "error"],
delimiter: "."
},
prettyPrintObjects: false
});This converts nested sections like context.user.id and meta.durationMs into top-level JSON keys while keeping the rest of the record shape stable.
Buffered Transports And Flush Support
import { createBufferedTransport, createLogger } from "@orionaisystems/betterlogs";
const records: string[] = [];
const transport = createBufferedTransport({
maxBufferSize: 10,
flushIntervalMs: 1_000,
sink: async (batch) => {
records.push(...batch.map((record) => record.message));
}
});
const log = createLogger({ transports: [transport] });
log.info("queued");
await log.flush();Buffered transports are useful when you want to batch network, file, or analytics writes without making the logging callsite async.
Sampling And Burst Rate Limiting
Use samplers when you want to reduce log volume before hooks, formatters, or transports do any downstream work. Samplers run after level filtering, record creation, serialization, and redaction, so custom decisions can inspect the structured LogRecord without exposing raw sensitive input.
import {
createBurstRateLimitSampler,
createLogger,
createPercentageSampler
} from "@orionaisystems/betterlogs";
const log = createLogger({
scope: "worker",
minLevel: "debug",
sample: [
createPercentageSampler({
rate: 0.1,
levels: ["debug", "trace"]
}),
createBurstRateLimitSampler({
maxRecords: 100,
intervalMs: 60_000,
levels: ["debug", "trace", "info"]
})
]
});Percentage sampling is useful for noisy low-severity records. Burst rate limiting is useful when a hot loop, retry storm, or repeated integration failure could otherwise multiply transport cost. If multiple samplers are configured, a record must pass every sampler before hooks and transports receive it.
Retry And Backoff Policies
Wrap a transport when you want retry behavior without baking retry logic into every destination.
import {
createHttpTransport,
createRetryingTransport,
createLogger
} from "@orionaisystems/betterlogs";
const transport = createRetryingTransport({
transport: createHttpTransport({
url: "https://logs.internal.example/ingest"
}),
retry: {
retries: 5,
baseDelayMs: 250,
maxDelayMs: 2_000
}
});
const log = createLogger({ transports: [transport] });File Transport, Rotation, And Retention
import { createFileTransport, createLogger } from "@orionaisystems/betterlogs";
const fileLog = createLogger({
scope: "audit",
format: "json",
transports: [
createFileTransport({
filePath: "./logs/audit.log",
rotate: {
maxBytes: 1_000_000,
maxFiles: 5
},
retention: {
maxAgeMs: 7 * 24 * 60 * 60 * 1_000,
archiveDirectory: "./logs/archive"
}
})
]
});
fileLog.info("Audit event recorded", {
actorId: "u_123",
event: "user.updated"
});
await fileLog.flush();The file transport writes asynchronously, rotates files once a size threshold is exceeded, and can archive or prune old log segments on a retention schedule.
Formatter Variants
Pretty Formatter
The default formatter for local development and service logs.
import { createPrettyFormatter } from "@orionaisystems/betterlogs";JSON Formatter
Best for pipelines, ingestion, or machine processing.
import { createJsonFormatter } from "@orionaisystems/betterlogs";Browser Formatter And Browser Entry
For browser-targeted imports, use the dedicated subpath:
import {
createBrowserConsoleTransport,
createBrowserLogger
} from "@orionaisystems/betterlogs/browser";
const log = createBrowserLogger({ scope: "ui" });
log.info("Mounted application shell");The browser subpath is built separately from the Node entry point and should remain free of Node.js builtin imports. CI and the publish workflow run a dedicated browser bundle smoke gate before packaging. That gate scans the ESM and CommonJS browser artifacts plus sourcemaps for Node builtin imports and accidental Node-only source inclusion.
Framework Adapters
BetterLogs stays dependency-light, so the framework adapters are opt-in helpers rather than hard integrations.
Express-Style Middleware
import { createExpressLoggingMiddleware, createLogger } from "@orionaisystems/betterlogs";
const baseLogger = createLogger({ scope: "http" });
export const requestLogger = createExpressLoggingMiddleware(baseLogger, {
includeHeaders: ["user-agent"],
successLevel: "info"
});Fastify-Style Hooks
import { createFastifyLoggingHooks, createLogger } from "@orionaisystems/betterlogs";
const baseLogger = createLogger({ scope: "http" });
export const hooks = createFastifyLoggingHooks(baseLogger, {
includeHeaders: ["user-agent"]
});Koa-Style Middleware
import { createKoaLoggingMiddleware, createLogger } from "@orionaisystems/betterlogs";
const baseLogger = createLogger({ scope: "http" });
export const koaMiddleware = createKoaLoggingMiddleware(baseLogger, {
includeHeaders: ["user-agent"]
});Fetch-Style Request Wrapper
import { createLogger, withFetchRequestLogging } from "@orionaisystems/betterlogs";
const baseLogger = createLogger({ scope: "http" });
export async function handle(request: Request) {
return withFetchRequestLogging(
baseLogger,
request,
async (logger) => {
logger.info("Handling request");
return new Response("ok", { status: 200 });
}
);
}includeHeaders works with both plain object header maps and Headers-style request objects, which keeps Edge and Fetch runtimes aligned with the Node adapters.
OpenTelemetry Bridge Utilities
@orionaisystems/betterlogs stays dependency-light, so the OpenTelemetry helpers work through small compatible interfaces rather than taking a hard dependency on the OTel SDK.
import {
createLogger,
createOpenTelemetryLogHook,
createOpenTelemetrySpanHook
} from "@orionaisystems/betterlogs";
const emitted: unknown[] = [];
const log = createLogger({
hooks: [
createOpenTelemetryLogHook({
emit(record) {
emitted.push(record);
}
}),
createOpenTelemetrySpanHook({
addEvent(name, attributes) {
emitted.push({ name, attributes });
}
})
]
});Testing Utilities And Snapshots
import {
createTestLogger,
snapshotRecords
} from "@orionaisystems/betterlogs";
const { logger, transport } = createTestLogger({
timestamps: false,
colors: false
});
logger.info("Captured in tests", { id: "u_123" });
await logger.flush();
const snapshot = snapshotRecords(transport.records);The memory transport and snapshot helpers make it easy to assert on structured log records in unit tests without stubbing global console methods.
Custom Serializers
Serializers let you normalize domain objects before they reach formatters or transports.
import { createLogger, type LogSerializer } from "@orionaisystems/betterlogs";
class Money {
constructor(
public readonly amount: number,
public readonly currency: string
) {}
}
const moneySerializer: LogSerializer<Money> = {
name: "money",
test(value): value is Money {
return value instanceof Money;
},
serialize(value) {
return `${value.currency} ${value.amount.toFixed(2)}`;
}
};
const log = createLogger({
serializers: [moneySerializer]
});
log.info("Invoice settled", {
amount: new Money(129.99, "USD")
});Error Logging
const log = createLogger({ scope: "worker", showStackTrace: true });
try {
throw new Error("Queue processing failed");
} catch (error) {
log.error("Unhandled exception", error);
}Errors are routed to console.error by the console transport, formatted clearly, and keep stack traces unless you disable them.
Security
If you discover a vulnerability, please use the reporting guidance in SECURITY.md rather than opening a public issue with exploit details.
Default redaction rules are case-insensitive and cover common credential, cookie, session, API key, token, connection string, and private-key field variants. Production preset users get those defaults unless they explicitly set includeDefaultRedaction: false.
Example Project Files
The repository includes:
examples/basic.tsfor async context propagation, scoped logging, timing, redaction, and structured outputexamples/advanced.tsfor durable batching, Kafka-style delivery, health reporting, and circuit breaker wrappingexamples/server.tsfor Express-style, Fastify-style, Koa-style, and fetch-style runtime adaptersexamples/production-preset.tsfor the generic production preset, durable HTTP delivery, and metrics diagnostics
Development Scripts
npm run build
npm run dev
npm run typecheck
npm run typecheck:api
npm run smoke:browser
npm run smoke:exports
npm run smoke:runtime
npm run cleansmoke:browser, smoke:exports, smoke:runtime, and typecheck:api expect dist/ to exist, so run them after npm run build when working locally. The browser smoke gate checks browser bundle artifacts and sourcemaps for Node-only leaks. The API type tests compile small consumer-style imports from the root package and browser subpath to catch accidental public surface regressions. The runtime smoke test exercises the Fetch/Fastify adapter behavior, transport diagnostics endpoints, health transition hooks, Prometheus metric formatting, and the durable inspection CLI against the built package.
Release Flow
BetterLogs now includes GitHub Actions for validation and publishing:
CIruns on pushes and pull requests and verifies install, typecheck, build, browser bundle smoke checks, export smoke checks, runtime smoke checks, public API type checks, and publish contentsPublishruns when a GitHub release is published, uses npm trusted publishing, runs the same package browser/export/runtime smoke checks, and publishes with provenanceprepublishOnlyruns typecheck, build, browser bundle smoke checks, export smoke checks, runtime adapter/CLI smoke checks, and public API type checks for local npm publishes
Recommended release flow:
npm version patch # or npm version minor for new public API
git push origin main --follow-tagsThen publish a GitHub release for the new tag from the repository UI to trigger npm publication automatically.
Design Notes
@orionaisystems/betterlogs v0.12.0 keeps the runtime small while separating:
- record creation and ambient binding resolution
- redaction and serialization
- formatter selection and JSON shaping
- sampling and burst rate limiting
- transport delivery, batching, retry, health tracking, health transition hooks, and flushing
- exporter-friendly transport diagnostics, endpoint adapters, and metrics formatting
- generic production logging presets from lower-level transport primitives
- hook observation
- Node and browser entry points
- runtime-specific adapter helpers
- CLI/programmatic inspection for durable JSONL spool and archive files
- browser bundle leak checks, compile-only public API type tests, and runtime adapter/diagnostics smoke tests for the root and browser package exports
That separation makes it practical to add more outputs and integrations later without disturbing the logger API most callers use every day.
Roadmap
Future ideas for the package:
- schema-driven structured event helpers for shared internal log contracts
- additional metrics exporter adapters for services that already run a metrics SDK
- additional production presets for OTLP and vendor-specific delivery patterns
- consumer adoption guides for application-owned logging boundaries
- worker-thread and multi-process relay transports
License
MIT
