Structured JSON logging for Python services with stdout, stderr, file, and CloudWatch destinations.
Project description
norialog
Structured JSON logging for Python services, with support for stdout, stderr, rotating file targets, and direct CloudWatch delivery.
This package is intentionally small and explicit. It does not wrap the standard logging module. Instead, it gives you a service logger that writes JSON records directly to one or more destinations, with schema remapping, secret redaction, target resolution, and CloudWatch batching built in.
Install
pip install norialog
Python requirement: >=3.11
Main Exports
from norialog import (
ManagedLogger,
LoggerRuntimeContext,
LoggerTargetContext,
create_cloudwatch_destination,
create_file_destination,
create_logger_runtime_context,
create_logger_target_context,
create_redact_matcher,
create_service_logger,
format_date_stamp,
parse_logger_destinations,
parse_logger_redact_keys,
resolve_target,
sanitize_log_value,
)
Quick Start
from norialog import create_service_logger
managed = create_service_logger(
service_name="payments",
environment="production",
)
logger = managed.logger
logger.info("service started", provider="stripe")
logger.warn("slow upstream", duration_ms=812)
logger.exception("payment failed", RuntimeError("gateway timeout"), invoice_id="inv_123")
managed.flush()
managed.close()
What create_service_logger() Returns
create_service_logger() returns a ManagedLogger dataclass with:
logger: theServiceLoggerinstanceflush(): flushes every configured destinationclose(): flushes and closes managed destinations
Call close() before process exit when you use file or CloudWatch destinations.
Logger Methods
The returned logger exposes:
trace(message, **fields)debug(message, **fields)info(message, **fields)warn(message, **fields)warning(message, **fields)error(message, **fields)fatal(message, **fields)exception(message, error, **fields)log(level, message, **fields)
Supported levels are:
tracedebuginfowarnerrorfatalsilent
Records below the configured threshold are skipped.
Default Output Shape
By default, each log record contains:
{
"level": "info",
"levelValue": 30,
"time": 1711580000000,
"timestamp": "2024-03-27T12:13:20.000Z",
"service": "payments",
"environment": "production",
"msg": "service started"
}
Additional keyword arguments passed to the logger are merged into the record.
Exception values are normalized into objects with:
namemessagestack
Basic Configuration
from norialog import create_service_logger
managed = create_service_logger(
service_name="api",
environment="staging",
level="debug",
destinations=["stdout", "file"],
base={"team": "platform", "region": "eu-west-1"},
redact_keys=["session_id"],
file={
"target": {
"prefix": "/var/log/noria/api",
"rotation": "daily",
"suffix": ".jsonl",
}
},
)
create_service_logger() Options
service_name: required service identifier added to every recordenvironment: optional environment name added to every recordlevel: minimum level, defaultinfodestinations: list of destination names, default["stdout"]schema: field remapping configurationidentity: runtime identity overrides for hostname, instance id, and pidredact: advanced redaction configurationredact_keys: extra exact-match redact keysbase: base fields merged into every recordfile: file destination configuration, required whenfileis enabledcloudwatch: CloudWatch destination configuration, required whencloudwatchis enabled
Destinations
Supported destination names are:
stdoutstderrfilecloudwatch
Use parse_logger_destinations() if you want to accept a comma-separated environment variable:
from norialog import parse_logger_destinations
destinations = parse_logger_destinations("stdout,file,cloudwatch")
The parser:
- defaults to
["stdout"]when the input is empty - lowercases entries
- removes duplicates
- raises
ValueErrorfor unsupported names
Schema Remapping
Use the schema option to rename output fields and choose which time fields are emitted.
managed = create_service_logger(
service_name="billing",
schema={
"messageKey": "message",
"levelKey": "severity",
"levelValueKey": "severityValue",
"timeKey": "ts",
"timestampKey": "tsIso",
"serviceKey": "app",
"environmentKey": "stage",
"errorKey": "error",
"timeMode": "iso",
},
)
Supported schema keys:
messageKey: defaultmsglevelKey: defaultlevellevelValueKey: defaultlevelValuetimeKey: defaulttimetimestampKey: defaulttimestampserviceKey: defaultserviceenvironmentKey: defaultenvironmenterrorKey: defaulterrtimeMode: one ofepoch,iso,both
Rules:
timeMode="epoch"emits only the integer millisecond timestamptimeMode="iso"emits only the ISO timestamptimeMode="both"emits both- when
timeMode="both",timeKeyandtimestampKeymust be different
Redaction
Redaction happens before records are encoded to JSON.
By default, the built-in matcher treats keys containing common secret-like names as sensitive, including:
tokensecretkeypasswordauthorizationcredentialapi_key
Simple Redaction
managed = create_service_logger(
service_name="auth",
redact_keys=["session_id", "otp"],
)
Advanced Redaction
managed = create_service_logger(
service_name="auth",
redact={
"keys": ["session_id"],
"mode": "replace",
},
)
redact.mode controls how custom keys behave:
merge: exact keys are added to the built-in secret matcherreplace: only the explicitly listed keys are redacted
redact_keys and redact["keys"] can be combined. If redact is provided, its mode wins.
You can also use the helpers directly:
from norialog import create_redact_matcher, sanitize_log_value
matcher = create_redact_matcher({"keys": ["session_id"], "mode": "merge"})
safe = sanitize_log_value({"token": "secret", "session_id": "abc"}, matcher)
Runtime Identity
By default, runtime context uses the current hostname and process id. Override it with identity when you need deterministic names in tests or custom deployment metadata:
managed = create_service_logger(
service_name="worker",
identity={
"hostname": "queue-1",
"instanceId": "i-abc123",
"pid": 4242,
},
)
Available identity keys:
hostnameinstanceIdpid
Base Fields
Use base to inject fields into every record:
managed = create_service_logger(
service_name="payments",
base={
"team": "platform",
"component": "webhook-consumer",
},
)
Base fields are merged after the standard service and environment fields.
File Destination
Enable the file destination by including "file" in destinations and supplying file=....
managed = create_service_logger(
service_name="api",
destinations=["file"],
file={
"target": {
"prefix": "/var/log/noria/api",
"rotation": "daily",
"suffix": ".jsonl",
},
"mkdir": True,
},
)
File Config
target: required target configmkdir: optional, defaultTrue; creates parent directories automatically
File Target Resolution
file["target"] supports three styles:
- Fixed path
file={"target": {"value": "/var/log/noria/api.jsonl"}}
- Declarative path building
file={
"target": {
"prefix": "/var/log/noria/api",
"rotation": "monthly",
"includeServiceName": True,
"includeEnvironment": True,
"includeHostname": True,
"includeInstanceId": True,
"includePid": True,
"suffix": ".jsonl",
"separator": "/",
"timezone": "America/New_York",
}
}
- Custom resolver
file={
"target": {
"resolve": lambda context: (
f"/var/log/{context.environment}/{context.service_name}-{context.pid}.jsonl"
)
}
}
Supported target keys:
value: fixed pathprefix: base path or prefixrotation:none,daily,monthly,annualtimezone: IANA timezone used for rotation boundariesincludeServiceNameincludeEnvironmentincludeHostnameincludeInstanceIdincludePididentifierseparator: join string, default-suffixresolve: callable that receivesLoggerTargetContext
Important behavior:
- file targets are resolved per event timestamp, not only once at startup
- that allows date-aware rollovers from the actual event time
- if the emitted JSON contains
timeortimestamp, the file destination uses it to choose the target
CloudWatch Destination
Enable the CloudWatch destination by including "cloudwatch" in destinations and supplying cloudwatch=....
managed = create_service_logger(
service_name="api",
destinations=["stdout", "cloudwatch"],
cloudwatch={
"region": "eu-west-1",
"logGroupName": "/noria/api",
"stream": {
"prefix": "api",
"rotation": "daily",
"includeHostname": False,
"includePid": False,
},
"retentionInDays": 30,
},
)
CloudWatch Config
region: required unless you injectclientlogGroupName: requiredcredentials: optional mapping withaccess_key_id,secret_access_key,session_tokenclient: optional boto logs client overridestream: optional target config for stream namescreateLogGroup: defaultTruecreateLogStream: defaultTrueretentionInDays: optional CloudWatch retention policyflushIntervalMs: default2000maxBatchCount: default1000maxBatchBytes: default900000maxBufferedEvents: default20000retryBaseDelayMs: default1000
Stream Naming
CloudWatch stream naming uses the same target resolution engine as file targets.
If you do not provide stream, the fallback stream name is:
{hostname}-{pid}
When you provide a rotating stream prefix, rotation happens from each event timestamp, not wall-clock flush time.
Retention Values
Supported retentionInDays values are:
1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653
CloudWatch Operational Behavior
- log events are buffered in memory and flushed in batches
- batches are grouped by stream name
- oversized buffers are trimmed from the oldest events
- transient flush failures are retried with backoff
- CloudWatch setup can create the log group and stream automatically
Target Helper Functions
These helpers are available when you want to build your own file or CloudWatch wrappers:
from norialog import (
create_logger_runtime_context,
create_logger_target_context,
format_date_stamp,
resolve_target,
)
Example:
runtime = create_logger_runtime_context(
service_name="payments",
environment="prod",
)
target_context = create_logger_target_context(runtime, 1711578600000)
path = resolve_target(
{
"prefix": "logs",
"rotation": "daily",
"includeServiceName": True,
"includeEnvironment": True,
"suffix": ".jsonl",
"separator": "/",
},
target_context,
)
Direct Destination Construction
If you do not want the full managed logger, you can create destinations directly:
from norialog import create_cloudwatch_destination, create_file_destination
runtime = create_logger_runtime_context(service_name="api", environment="prod")
file_destination = create_file_destination(
{"target": {"value": "/tmp/api.jsonl"}},
runtime,
)
cloudwatch_destination = create_cloudwatch_destination(
{
"region": "eu-west-1",
"logGroupName": "/noria/api",
"createLogGroup": False,
"createLogStream": False,
},
runtime,
)
Each destination exposes:
emit_line(line, timestamp_ms=None)flush()close()
Usage Patterns
Stdout Only
managed = create_service_logger(service_name="api")
managed.logger.info("ready")
Stdout and File
managed = create_service_logger(
service_name="api",
destinations=["stdout", "file"],
file={"target": {"prefix": "/tmp/api", "rotation": "daily", "suffix": ".jsonl"}},
)
File Only with Custom Schema
managed = create_service_logger(
service_name="jobs",
destinations=["file"],
schema={"messageKey": "message", "errorKey": "error", "timeMode": "iso"},
file={"target": {"value": "/tmp/jobs.log"}},
)
CloudWatch Only
managed = create_service_logger(
service_name="worker",
destinations=["cloudwatch"],
cloudwatch={
"region": "eu-west-1",
"logGroupName": "/noria/worker",
},
)
Notes and Caveats
close()is the safe way to finish file and CloudWatch loggingStdDestination.close()only flushes; it does not closestdoutorstderr- file and CloudWatch targets can rotate based on the timestamp inside each emitted JSON record
warn()andwarning()are equivalent- passing
error=orerr=fields is normalized to the configured error key - JSON is emitted with compact separators and
ensure_ascii=False
Development
Run tests:
uv sync --extra dev
uv run pytest
Run lint:
uv run ruff check .
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file norialog-0.1.0.tar.gz.
File metadata
- Download URL: norialog-0.1.0.tar.gz
- Upload date:
- Size: 22.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
499dd1d13395ba179c2bf714b110880d4f271cc75dbcd4568d3883bb704f3314
|
|
| MD5 |
e69fefc23712e11e9116644b0ab9786e
|
|
| BLAKE2b-256 |
7078b3f18d8c71ef40332ca74a9592d3341413f5bf1078e1879157b68fd6d6b4
|
Provenance
The following attestation bundles were made for norialog-0.1.0.tar.gz:
Publisher:
ci.yml on thekiharani/py-packages
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
norialog-0.1.0.tar.gz -
Subject digest:
499dd1d13395ba179c2bf714b110880d4f271cc75dbcd4568d3883bb704f3314 - Sigstore transparency entry: 1261945866
- Sigstore integration time:
-
Permalink:
thekiharani/py-packages@952d29f64a03e8f8db5642ec9845687845753bee -
Branch / Tag:
refs/heads/release - Owner: https://github.com/thekiharani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@952d29f64a03e8f8db5642ec9845687845753bee -
Trigger Event:
push
-
Statement type:
File details
Details for the file norialog-0.1.0-py3-none-any.whl.
File metadata
- Download URL: norialog-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4fe7e61d609a0453399479ee3a77c5a0fca508c76d0e94669d02c0ece40a8cb5
|
|
| MD5 |
03471cd7fdc05138e02359a52b45a782
|
|
| BLAKE2b-256 |
cb9a6fb011c2593455b08a6c99b8539a6d2398980c625535d2f1828f37d4fca1
|
Provenance
The following attestation bundles were made for norialog-0.1.0-py3-none-any.whl:
Publisher:
ci.yml on thekiharani/py-packages
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
norialog-0.1.0-py3-none-any.whl -
Subject digest:
4fe7e61d609a0453399479ee3a77c5a0fca508c76d0e94669d02c0ece40a8cb5 - Sigstore transparency entry: 1261945870
- Sigstore integration time:
-
Permalink:
thekiharani/py-packages@952d29f64a03e8f8db5642ec9845687845753bee -
Branch / Tag:
refs/heads/release - Owner: https://github.com/thekiharani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@952d29f64a03e8f8db5642ec9845687845753bee -
Trigger Event:
push
-
Statement type: