Skip to content

Logging

GGCommons 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 GGCommons instance; component code just obtains a logger and writes to it.

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"
}
}

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.

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.

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 LoggerConfig per name (with additivity=false).
  • Pythonlogging.getLogger(name).setLevel(...) per name.
  • Rusttarget=level directives added to the tracing EnvFilter.
  • TypeScript — longest dotted-prefix match of the logger name against the loggers keys.

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.

Setting the format token to the literal value json (case-insensitive) switches that SDK to the stdout-JSON sink instead of a text layout.

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 when fileLogging is present, boolean, default false) — 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 like 512KB, 10MB, 1GB.
  • backupCount (integer >= 0, default 5) — how many rotated backups to keep.

When enabled is false (or fileLogging is omitted), only the console sink runs.

For machine-readable logs (log aggregators, Kubernetes), GGCommons 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:

  1. An explicit *_format token set to json (case-insensitive).

  2. Otherwise, the platform-profile default — which is json on --platform KUBERNETES and unset on GREENGRASS / HOST.

  3. 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" } }

A few per-language details to know about the JSON field set:

  • Java / Python / TypeScript put exceptions under a thrown field, present only when an exception/error is supplied. Python additionally emits a stack field and any caller extra= fields.
  • Rust preserves arbitrary structured event fields verbatim (e.g. a logged error field) rather than a fixed thrown key.

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.ggcommons.logging.Logger;
import com.mbreissi.ggcommons.logging.LoggerFactory;
Logger log = LoggerFactory.getLogger(MyApp.class); // or LoggerFactory.getLogger("my.logger")
log.info("publish interval set to {}ms", interval); // SLF4J-style {} placeholders
log.warn("retrying after error", throwable); // (String, Throwable) overload
if (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.

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, GGCommons 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 (boolean, default false) is only meaningful on Java, where true switches to a manager that rebuilds the entire Log4j2 configuration (every configured logger gets its own appenders) instead of just the root and per-logger configs. Python parses the flag but never acts on it, and Rust/TypeScript have no equivalent — those SDKs always rebuild/replace the root logger regardless. Leave it false unless you specifically need Java’s whole-config rebuild.

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.