Logging
EdgeCommons configures your process’s logging framework for you from the logging section of your
component config. You get a sensible root level, a console/stdout sink that is always on, an optional
size-rotated file sink, per-logger level overrides, and a structured one-JSON-object-per-line sink
that is the default on Kubernetes. Your component then logs through its language’s normal facility —
Log4j2/SLF4J, the stdlib logging module, tracing, or the exported TypeScript logger.
There is no gg.logging() accessor. Logging is configured by the framework when you build your
EdgeCommons instance; component code just obtains a logger and writes to it.
Where logging is configured
Section titled “Where logging is configured”Logging is driven by the logging section of the config delivered through your -c/--config source
(see the Configuration guide). The framework applies it automatically at
startup and re-applies it on hot reload — you never call the configurator yourself.
A minimal logging section looks like this (the format token is the one line that differs per SDK):
{ "logging": { "level": "INFO", "java_format": "%d{yyyy-MM-dd HH:mm:ss} [%-5p] %c{1}: %m%n" }}{ "logging": { "level": "INFO", "python_format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s" }}{ "logging": { "level": "INFO", "rust_format": "{timestamp} {level} [{component}] {target}: {message}" }}{ "logging": { "level": "INFO", "ts_format": "{timestamp} [{level}] {message}" }}If you omit the format token entirely, each SDK falls back to its own built-in default (shown in
Format tokens below) — for Java, Python, and TypeScript that is a default pattern,
while Rust uses the tracing-subscriber built-in fmt layer rather than a token template — unless
the active platform supplies a default; on Kubernetes that default is the json sink.
Levels
Section titled “Levels”level is the root logger level. The schema enum is TRACE | DEBUG | INFO | WARN | ERROR | FATAL
and defaults to INFO.
The enum is shared, but the levels are not honored uniformly — each language maps the config
string onto its own logging framework, and the extremes (TRACE, FATAL) behave differently:
| Config value | Java | Python | Rust | TypeScript |
|---|---|---|---|---|
TRACE |
honored | folds to INFO (no TRACE level) |
honored | folds to DEBUG |
DEBUG |
honored | honored | honored | honored |
INFO |
honored | honored | honored | honored |
WARN |
honored | honored (WARNING) |
honored | honored |
ERROR |
honored | honored | honored | honored |
FATAL |
honored | honored (CRITICAL) |
silences output — avoid | folds to INFO |
For the broadest cross-language consistency, stick to DEBUG, INFO, WARN, and ERROR.
Per-logger levels
Section titled “Per-logger levels”loggers maps a logger name to a level, letting you turn one noisy component up or down without
changing the root level. Keys must match ^[a-zA-Z0-9._-]+$; values are level strings from the same
enum. The keys and values are identical across all four SDKs:
{ "logging": { "level": "INFO", "loggers": { "com.example.MyApp": "DEBUG", "app.db": "WARN" } }}How the override is matched differs by SDK, but the config shape is the same:
- Java — a Log4j2
LoggerConfigper name (withadditivity=false). - Python —
logging.getLogger(name).setLevel(...)per name. - Rust —
target=leveldirectives added to thetracingEnvFilter. - TypeScript — longest dotted-prefix match of the logger name against the
loggerskeys.
Format tokens
Section titled “Format tokens”Each SDK formats text log lines using its own format token, written in its own syntax. A token from one language is meaningless to the others, and each library reads only its own key.
java_format is a Log4j2 PatternLayout.
{ "logging": { "java_format": "%d{yyyy-MM-dd HH:mm:ss} [%-5p] %-25.25c{1}(%4L): %m%n" } }Common conversion specifiers: %d{...} timestamp, %-5p level, %c{1} logger (last segment),
%L line number, %t thread, %m message, %n newline. Default when unset:
%d{yyyy-MM-dd HH:mm:ss} [%-5p] %-25.25c{1}(%4L): %m%n.
python_format is a logging.Formatter format string.
{ "logging": { "python_format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s" } }Common fields: %(asctime)s timestamp, %(levelname)s level, %(name)s logger,
%(message)s message. Default when unset:
%(asctime)s [%(levelname)s] %(name)s: %(message)s.
rust_format is a token template. Supported tokens: {timestamp}, {level}, {target},
{message}.
{ "logging": { "rust_format": "{timestamp} {level} [{component}] {target}: {message}" } }Note Rust uses {target} (not {logger}). Unlike the other SDKs, Rust has no built-in
token-template default: when rust_format is omitted (and the active platform supplies no json
default), Rust falls back to the tracing-subscriber built-in fmt layer (e.g.
2024-06-27T12:00:00.123456Z INFO target: message — note the colon after the target), not a token
template. The {timestamp}, {level}, {component}/{componentName}, {target}, and {message} tokens apply only when
rust_format is explicitly set to a non-json template.
ts_format is a token template. Supported tokens: {timestamp}, {level}, {logger},
{message}.
{ "logging": { "ts_format": "{timestamp} [{level}] {message}" } }Note TypeScript uses {logger} (not {target}). Default when unset:
{timestamp} [{level}] {message}.
Setting the format token to the literal value json (case-insensitive) switches that SDK to the
stdout-JSON sink instead of a text layout.
Console and file logging
Section titled “Console and file logging”The console/stdout sink is always on — every SDK installs it unconditionally. To also write to a
size-rotated file, add a fileLogging object. These keys are the same in all four SDKs:
{ "logging": { "level": "INFO", "fileLogging": { "enabled": true, "filePath": "/var/log/{ComponentName}.log", "maxFileSize": "10MB", "backupCount": 5 } }}enabled(required whenfileLoggingis present, boolean, defaultfalse) — turns the file sink on.filePath(string) — the active log file. Supports template variables such as{ComponentName}and{ThingName}(see the Configuration guide).maxFileSize(string, default"10MB") — rotate when the file reaches this size. Accepts units like512KB,10MB,1GB.backupCount(integer>= 0, default5) — how many rotated backups to keep.
When enabled is false (or fileLogging is omitted), only the console sink runs.
Publish logs on the UNS bus
Section titled “Publish logs on the UNS bus”Set logging.publish.enabled when a component should publish a bounded, structured log stream to the
site bus for an operator console or another consumer:
{ "logging": { "level": "INFO", "publish": { "enabled": true, "destination": "local", "minLevel": "INFO", "captureNative": true, "captureConsole": false } }}The topic is fixed and library-owned:
ecv1/{device}/{component}/log/{level}The envelope header is { "name": "log", "version": "1.0" }; the body carries
schema: "edgecommons.log.v1" plus timestamp, level, logger, message, sequence, and optional
thread, fields, error, truncated, and dropped. The log class remains reserved: ordinary
messaging.publish(...) calls to .../log/... are rejected, and only the SDK’s log publisher can use
the reserved seam.
The publisher is disabled by default because logs can be noisy. Its queue is bounded
(logging.publish.queue.maxRecords, default 1000, drop-oldest), record bodies are capped
(maxRecordBytes, default 8192), and redaction is on by default for common credential-like strings.
Use destination: "northbound" only when the configured northbound broker is the intended log sink.
captureNative captures the SDK’s logging framework where supported: Log4j2/JUL/SLF4J in Java, stdlib
logging in Python, tracing in Rust, and EdgeCommons Logger in TypeScript. captureConsole requests
stdout/stderr or console capture where the SDK/runtime can support it; it is off by default and should
not be treated as a portable substitute for using the language’s logger.
The stdout-JSON sink
Section titled “The stdout-JSON sink”For machine-readable logs (log aggregators, Kubernetes), EdgeCommons can emit one JSON object per
line to stdout instead of a text layout. Each line carries a stable field set: timestamp
(ISO-8601, UTC Z), level, logger, message, any non-empty correlation fields
(thing, pod, namespace, node), and an exception field when one is present.
The JSON sink is selected by the effective format, resolved with this precedence:
-
An explicit
*_formattoken set tojson(case-insensitive). -
Otherwise, the platform-profile default — which is
jsonon--platform KUBERNETESand unset on GREENGRASS / HOST. -
Otherwise, the SDK’s built-in text default.
So on Kubernetes you get JSON logs automatically. On any platform you can force it by setting your
language’s format token to json:
{ "logging": { "level": "INFO", "java_format": "json" } }{ "logging": { "level": "INFO", "python_format": "json" } }{ "logging": { "level": "INFO", "rust_format": "json" } }{ "logging": { "level": "INFO", "ts_format": "json" } }A few per-language details to know about the JSON field set:
- Java / Python / TypeScript put exceptions under a
thrownfield, present only when an exception/error is supplied. Python additionally emits astackfield and any callerextra=fields. - Rust preserves arbitrary structured event fields verbatim (e.g. a logged
errorfield) rather than a fixedthrownkey.
Getting a logger and emitting logs
Section titled “Getting a logger and emitting logs”Each SDK exposes logging through its native facility. The library has already configured the root logger by the time your component code runs, so you just get a logger and write to it.
import com.mbreissi.edgecommons.logging.Logger;import com.mbreissi.edgecommons.logging.LoggerFactory;
Logger log = LoggerFactory.getLogger(MyApp.class); // or LoggerFactory.getLogger("my.logger")
log.info("publish interval set to {}ms", interval); // SLF4J-style {} placeholderslog.warn("retrying after error", throwable); // (String, Throwable) overloadif (log.isDebugEnabled()) { log.debug("payload bytes: {}", payload.length);}LoggerFactory.getLogger(Class<?>) and getLogger(String) return a thin Logger facade
(trace/debug/info/warn/error plus is*Enabled() predicates) that auto-detects the
underlying framework (SLF4J ▸ Log4j2 ▸ JUL). Components may also use Log4j2 directly
(LogManager.getLogger(...)) — the library has already reconfigured the Log4j2 root logger.
import logging
log = logging.getLogger(__name__)
log.info("publish interval set to %sms", interval)log.warning("retrying after error", exc_info=True)log.debug("payload bytes: %d", len(payload))There is no EdgeCommons logger factory in Python — the library configures the root logger, so the
stdlib logging.getLogger(name) is all you need. Per-logger levels from logging.loggers apply by
name.
use tracing::{debug, info, warn};
info!(publish_interval = interval, "configuration changed");warn!(error = %err, "retrying after error");debug!(payload_bytes = payload.len(), "received payload");Components log via the tracing macros — there is no getLogger. The library installs the global
tracing-subscriber once during build; structured key/value fields (e.g. publish_interval = ...)
are preserved verbatim in the JSON sink.
import { logger, getLogger } from "@edgecommons/edgecommons";
logger.info("publish interval set to " + interval + "ms");
const dbLog = getLogger("app.db");dbLog.debug("db detail"); // honors logging.loggers["app.db"]dbLog.error("query failed", err); // (message, error?) signaturelogger is the process-wide root logger ("edgecommons"); getLogger(name) returns a named,
hierarchically-leveled, cached Logger with debug/info/warn/error methods. There is no
trace/fatal method (TRACE folds to DEBUG, FATAL to INFO).
Early-logging bootstrap
Section titled “Early-logging bootstrap”A few startup facts — “platform resolved …”, “messaging connected …” — are determined before the
logging config has been applied, so they cannot honor your configured level or format on first
emission. To avoid losing them, EdgeCommons defers and re-emits those startup lines after
config-driven logging is initialized, so they appear with your chosen level, format, and sink (text
or JSON) like everything else. This is automatic in all four SDKs; you do not need to do anything.
The practical effect is that your very first configured log lines include the platform/transport
bootstrap facts, formatted consistently with the rest of your output.
globalControl
Section titled “globalControl”globalControl (boolean, default false) is only meaningful on Java, where true routes logging
setup through GlobalLoggingManager instead of the default reconfigurer. In practice the two paths do
nearly the same thing — both replace the entire Log4j2 configuration and build a per-logger config
(each with its own appenders) for every logging.loggers entry — so the functional difference is
minimal. Python parses the flag but never acts on it, and Rust/TypeScript have no equivalent (those
SDKs always rebuild/replace the root logger regardless). You can safely leave it false.
Hot reload
Section titled “Hot reload”When your config source supports hot reload, a change to logging is re-applied live. Java, Python,
and TypeScript re-run the full configurator (level, format, and file sink all re-applied; Python and
TypeScript can swap the file writer live). Rust can only swap the level live — its format and
file layers are fixed at startup, so changing rust_format or fileLogging on Rust requires a
restart.