@norialabs/logger
v0.1.6
Published
Flexible structured JSON logger for Node.js services.
Maintainers
Readme
@norialabs/logger
Structured JSON logging for Node.js services, built on pino.
Supported destinations:
stdoutstderrfilecloudwatch
Node >=20 is required.
Install
npm install @norialabs/loggerRecommended Deployment Model
For most production systems:
- log structured JSON to
stdout - let the runtime or platform ship logs to CloudWatch, Loki, Datadog, or another backend
Use direct cloudwatch delivery only when the application must own log shipping itself.
Quick Start
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "payments",
environment: process.env.NODE_ENV,
});
export const logger = managedLogger.logger;
export const flushLogger = managedLogger.flush;
export const closeLogger = managedLogger.close;Default behavior:
- destination defaults to
["stdout"] - level defaults to
"info" levelis emitted as a string andlevelValuekeeps the numeric severitytimeis emitted as Unix epoch milliseconds andtimestampas ISO UTC- base log fields always include
service environmentis included when provided- common secret-like keys are redacted automatically
- runtime identity defaults to the current
hostnameandpid
Example output:
{
"level": "info",
"levelValue": 30,
"time": 1774566041398,
"timestamp": "2026-03-27T02:20:41.398Z",
"service": "payments",
"environment": "production",
"msg": "Service started"
}Structured error example:
{
"level": "error",
"levelValue": 50,
"time": 1774566041398,
"timestamp": "2026-03-27T02:20:41.398Z",
"service": "conversations",
"conversationId": "conv_123",
"err": {
"name": "Error",
"message": "Webhook signature mismatch",
"stack": "Error: Webhook signature mismatch\n at ..."
},
"msg": "Failed to process inbound webhook"
}Structured payload example:
{
"level": "info",
"levelValue": 30,
"time": 1774566041398,
"timestamp": "2026-03-27T02:20:41.398Z",
"service": "conversations",
"provider": "telegram",
"payload": {
"update_id": 1,
"message": {
"message_id": 10,
"text": "Hello"
}
},
"msg": "Received Telegram webhook"
}Driver Examples
stdout
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "api",
environment: "development",
destinations: ["stdout"],
});stderr
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "worker",
destinations: ["stderr"],
});Fixed File Path
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "mailer",
destinations: ["file"],
file: {
target: {
value: "/var/log/mailer/service.log",
},
},
});Rotated File Paths
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "batch",
environment: "production",
destinations: ["file"],
file: {
target: {
prefix: "./logs/batch",
rotation: "monthly",
timezone: "Africa/Nairobi",
includeEnvironment: true,
suffix: ".log",
},
},
});That resolves to paths like:
./logs/batch-2026-03-production.logCloudWatch With Rotating Streams
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "conversations",
environment: "production",
destinations: ["cloudwatch"],
cloudwatch: {
region: "af-south-1",
logGroupName: "/norialabs/conversations",
stream: {
prefix: "api",
rotation: "daily",
},
},
});That resolves to stream names like:
api-2026-03-27-hostnameRotation is based on each event timestamp, so rollover works without restarting the process.
By default, rotating CloudWatch streams with a configured prefix resolve to prefix-date-hostname.
CloudWatch With Explicit AWS Credentials
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "billing",
destinations: ["cloudwatch"],
cloudwatch: {
region: "af-south-1",
logGroupName: "/norialabs/billing",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
sessionToken: process.env.AWS_SESSION_TOKEN,
},
stream: {
prefix: "billing",
rotation: "monthly",
},
},
});If credentials is omitted, the AWS SDK credential chain is used as-is.
CloudWatch With Retention
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "billing",
destinations: ["cloudwatch"],
cloudwatch: {
region: "af-south-1",
logGroupName: "/norialabs/billing",
retentionInDays: 30,
stream: {
prefix: "billing",
rotation: "daily",
},
},
});Important:
- retention is applied at the CloudWatch log-group level
- all streams in the same log group share that retention policy
- if
retentionInDaysis omitted, the package leaves retention unmanaged and unchanged - that means the package does not set an expiry by default
- valid values are the CloudWatch-supported retention periods:
1,3,5,7,14,30,60,90,120,150,180,365,400,545,731,1096,1827,2192,2557,2922,3288,3653
Multiple Destinations
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "worker",
environment: "production",
destinations: ["stdout", "file"],
file: {
target: {
value: "./logs/worker.log",
},
},
});Custom Schema
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "api",
environment: "production",
schema: {
messageKey: "message",
levelKey: "severity",
levelValueKey: "severityValue",
timeKey: "ts",
timestampKey: "tsIso",
serviceKey: "app",
environmentKey: "stage",
errorKey: "error",
timeMode: "iso",
},
});This emits records like:
{
"severity": "info",
"severityValue": 30,
"tsIso": "2026-03-27T02:20:41.398Z",
"app": "api",
"stage": "production",
"message": "Service started"
}Custom Identity
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "worker",
identity: {
hostname: "pod-7",
instanceId: "replica-a",
pid: 42,
},
destinations: ["cloudwatch"],
cloudwatch: {
region: "af-south-1",
logGroupName: "/norialabs/worker",
stream: {
prefix: "jobs",
rotation: "daily",
includeHostname: false,
includeInstanceId: true,
},
},
});That resolves to stream names like:
jobs-2026-03-27-replica-aRedaction Modes
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "api",
redact: {
keys: ["session_id"],
mode: "replace",
},
});Redaction modes:
merge: built-in sensitive keys plus your custom keysreplace: only your custom keys
Target Model
file.target and cloudwatch.stream share the same structure:
type TargetConfig = {
value?: string;
prefix?: string;
suffix?: string;
separator?: string;
rotation?: "none" | "daily" | "monthly" | "annual";
timezone?: string;
identifier?: string;
includeServiceName?: boolean;
includeEnvironment?: boolean;
includeHostname?: boolean;
includeInstanceId?: boolean;
includePid?: boolean;
resolve?: (context) => string;
};Resolution order:
resolve(context)if providedvalueif provided- composed target from prefix, rotation stamp, and optional runtime fields
Available resolver context:
serviceNameenvironmenthostnameinstanceIdpidtimestampisoTimestamp
Notes:
timezonedefaults toUTCrotationdefaults to"none"- monthly and annual rotation are timezone-aware, not just daily
includeHostnamedefaults totruefor rotating CloudWatch streams with a configuredprefixincludeInstanceIddefaults tofalseidentifierlets you append a stable custom suffix after the date and runtime partsincludePiddefaults tofalseunless you opt in explicitly- otherwise inclusion flags default to
false
Target Examples
Fixed Value
target: {
value: "./logs/app.log",
}Daily Rotation
target: {
prefix: "./logs/app",
rotation: "daily",
suffix: ".log",
}Resolves to:
./logs/app-2026-03-27.logMonthly Rotation With Runtime Parts
target: {
prefix: "jobs",
rotation: "monthly",
identifier: "worker-a",
includeHostname: false,
includeEnvironment: true,
}Resolves to:
jobs-2026-03-production-worker-aPrefix-Date Only
target: {
prefix: "api",
rotation: "daily",
includeHostname: false,
}Resolves to:
api-2026-03-27Prefix-Date-Custom Identifier
target: {
prefix: "api",
rotation: "daily",
includeHostname: false,
identifier: "worker-a",
}Resolves to:
api-2026-03-27-worker-aCustom Resolver
import { createServiceLogger, formatDateStamp } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "api",
environment: "production",
destinations: ["cloudwatch"],
cloudwatch: {
region: "af-south-1",
logGroupName: "/norialabs/api",
stream: {
resolve: (context) =>
`${context.environment}-${context.serviceName}-${formatDateStamp(context.timestamp, {
mode: "annual",
})}`,
},
},
});Configuration Reference
createServiceLogger
createServiceLogger({
serviceName: string;
environment?: string;
level?: LogLevel; // default: "info"
destinations?: ("stdout" | "stderr" | "file" | "cloudwatch")[]; // default: ["stdout"]
schema?: LoggerSchemaConfig;
identity?: LoggerIdentityConfig;
redact?: LoggerRedactionConfig;
file?: FileLoggerConfig;
cloudwatch?: CloudWatchLoggerConfig;
redactKeys?: string[];
base?: Record<string, unknown>;
});FileLoggerConfig
type FileLoggerConfig = {
target?: TargetConfig;
mkdir?: boolean; // default: true
};LoggerSchemaConfig
type LoggerSchemaConfig = {
messageKey?: string; // default: "msg"
levelKey?: string; // default: "level"
levelValueKey?: string; // default: "levelValue"
timeKey?: string; // default: "time"
timestampKey?: string; // default: "timestamp"
serviceKey?: string; // default: "service"
environmentKey?: string; // default: "environment"
errorKey?: string; // default: "err"
timeMode?: "epoch" | "iso" | "both"; // default: "both"
};LoggerIdentityConfig
type LoggerIdentityConfig = {
hostname?: string; // default: os.hostname()
pid?: number; // default: process.pid
instanceId?: string;
};LoggerRedactionConfig
type LoggerRedactionConfig = {
keys?: string[];
mode?: "merge" | "replace"; // default: "merge"
};CloudWatchLoggerConfig
type CloudWatchLoggerConfig = {
region: string;
logGroupName: string;
stream?: TargetConfig;
credentials?: AwsCredentialIdentity | AwsCredentialIdentityProvider;
retentionInDays?: number; // default: unset, package leaves retention unchanged
client?: CloudWatchLogsClient;
createLogGroup?: boolean; // default: true
createLogStream?: boolean; // default: true
flushIntervalMs?: number; // default: 2000
maxBatchCount?: number; // default: 1000
maxBatchBytes?: number; // default: 900000
maxBufferedEvents?: number; // default: 20000
retryBaseDelayMs?: number; // default: 1000
};If client is supplied internally, it takes precedence over region/credentials settings. Otherwise the package constructs its own CloudWatchLogsClient.
Redaction
The logger redacts common secret-like keys by default, including:
tokensecretkeypasswordauthorizationapi_key
Add extra keys like this:
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "conversations",
destinations: ["stdout"],
redactKeys: ["session_id", "merchant_reference"],
});Or replace the built-in defaults entirely:
const managedLogger = createServiceLogger({
serviceName: "conversations",
destinations: ["stdout"],
redact: {
keys: ["session_id", "merchant_reference"],
mode: "replace",
},
});For env-driven config:
import { parseLoggerDestinations, parseLoggerRedactKeys } from "@norialabs/logger";
const destinations = parseLoggerDestinations(process.env.LOG_DESTINATIONS);
const redactKeys = parseLoggerRedactKeys(process.env.LOG_REDACT_KEYS);Graceful Shutdown
import { createServiceLogger } from "@norialabs/logger";
const managedLogger = createServiceLogger({
serviceName: "payments",
destinations: ["stdout"],
});
process.on("SIGTERM", () => {
void managedLogger.close().finally(() => process.exit(0));
});Low-Level Helpers
The package also exports:
createCloudWatchDestinationcreateFileDestinationcreateLoggerRuntimeContextcreateLoggerTargetContextresolveTargetformatDateStampparseLoggerDestinationsparseLoggerRedactKeyssanitizeLogValue
License
MIT
