Mobius4 Logging Guide

August 5, 2026 · View on GitHub

This document describes the logging system used in Mobius4, and provides guidelines for contributors and operators.


Overview

Mobius4 uses Pino — a high-performance, JSON-first Node.js logger. Pino is chosen for its minimal overhead, which is critical for an IoT middleware platform that may handle high message volumes.

PackageRole
pinoCore logger
pino-httpExpress request/response middleware
pino-rollFile rotation (daily/hourly, by size)
pino-prettyHuman-readable dev output (devDependency)

All logging behavior is configured in config/default.json under the logging key, and can be overridden in config/local.json.


Log Levels

LevelValueWhen to use
trace10Full primitive/payload dumps (replaces JSON.stringify dumps)
debug20Flow tracing: request dispatch, MQTT messages, routing decisions
info30Important state changes: server start, resource CRUD, CSE registration
warn40Recoverable anomalies: unsupported geo type, notification delivery failure
error50Operation failures: DB errors, resource creation failures, forwarding failures
fatal60Process-level failures requiring restart: DB unreachable, port bind failure

Production recommended level: info
Development recommended level: debug


Configuration Reference

In config/default.json (override in config/local.json):

"logging": {
  "level": "info",
  "console": {
    "enabled": true,
    "pretty": false
  },
  "file": {
    "enabled": false,
    "path": "logs/mobius4.log",
    "rotate": "daily",
    "maxFiles": 14,
    "maxSize": "100m"
  },
  "http": {
    "logBody": false,
    "redactPaths": ["req.headers.authorization", "req.body.pw"]
  }
}

Options

KeyDescription
levelMinimum log level to emit. Messages below this level are dropped.
console.enabledLog to stdout.
console.prettyUse pino-pretty for human-readable output. Only active when NODE_ENV !== 'production'.
file.enabledLog to a rotating file.
file.pathLog file path. The directory is created automatically if it does not exist.
file.rotateRotation frequency: "daily" or "hourly".
file.maxFilesNumber of rotated files to retain.
file.maxSizeMax size per file before triggering a rotation (e.g. "100m").
http.redactPathsPino redact paths — values at these paths are replaced with [REDACTED] in all log output. Extend this list to protect additional sensitive fields.

Typical Configurations

Development (config/local.json)

{
  "logging": {
    "level": "debug",
    "console": { "pretty": true }
  }
}

Start with npm run dev to get colorized, human-readable output.

These two settings are the right choice for development and the wrong one for a deployment — see Throughput cost below. mobius4 logs a warning at startup whenever either is active, so a deployment that inherited them from a copied local.json says so in its own log:

WARN [logger.js] logging is configured for development, not for throughput
    costly: ["console.pretty=true (about -18% throughput)",
             "level=\"debug\" (about -7%, plus one log line per request)"]

Production (config/local.json)

{
  "logging": {
    "level": "info",
    "console": { "enabled": true, "pretty": false },
    "file": {
      "enabled": true,
      "path": "logs/mobius4.log",
      "rotate": "daily",
      "maxFiles": 30
    }
  }
}

JSON output to both stdout and a rotating file. Compatible with log shippers (Filebeat, Fluentd, etc.).

Throughput cost

The defaults in config/default.json (level: "info", console.pretty: false) are already the ones a deployment wants. The development settings above are not, and the difference is large enough to be worth stating.

Measured 2026-08-05 — concurrency 32, 5 s, RETRIEVE <container>, one instance, file logging off, same machine:

SettingRequests/svs. default
level: "warn"4,361+9%
level: "info", pretty: false — default4,013
level: "debug", pretty: false3,742−7%
level: "info", pretty: true3,288−18%
level: "debug", pretty: true2,771−31%

Two separate costs. pino-pretty runs as a stream in the request path (it has to, because the custom prettifiers are functions and functions cannot be passed to a transport worker) — that is the −18%. debug adds a second cost of its own: the HTTP binding logs one line per successful request at debug level (customLogLevel in bindings/http.js), so every request gains a log record.

NODE_ENV is not a sufficient guard. pretty is suppressed when NODE_ENV === 'production', but ecosystem.config.js sets NODE_ENV=dev unless PM2 is started with --env production, so a deployment that omits that flag keeps paying. The startup warning shown above exists for exactly that case.


Usage Patterns for Contributors

Getting a logger in a module

Each module creates a child logger with its own module context:

// At the top of the file, after other requires
const logger = require('../../logger').child({ module: 'cnt' });

The module field appears in every log line from that file, making it easy to filter.

Logging an error in a catch block

try {
    // ... operation
} catch (err) {
    logger.error({ err }, 'create_a_cnt failed');
}

Always pass the error object as { err } — Pino serializes it with stack trace automatically.

Logging with structured context (preferred over string interpolation)

// Good — fields are queryable in log aggregation systems
logger.info({ ri: res.ri, ty: res.ty }, 'resource created');

// Avoid — string interpolation loses structure
logger.info(`resource created: ${res.ri}`);

Log level guidelines per operation

// Server startup
logger.info({ port: config.http.port }, 'HTTP server listening');

// Incoming request (pino-http handles this automatically for HTTP)
// For MQTT, log manually:
logger.debug({ topic, originator, rqi, op }, 'mqtt request received');

// Full primitive dump (dev/trace only, never in production by default)
logger.trace({ prim: req_prim }, 'full request primitive');

// Resource operation success
logger.info({ ri, sid }, 'resource created');

// Recoverable warning
logger.warn({ geometry_type }, 'unsupported geometry type, skipping geo filter');

// Error in catch
logger.error({ err }, 'operation failed');

// Fatal — process should stop
logger.fatal({ err }, 'database unreachable, cannot start');

HTTP Request Logging

HTTP requests and responses are logged automatically by pino-http middleware in bindings/http.js. You do not need to add logging inside route handlers.

Log level mapping:

  • 5xx responses → error
  • 4xx responses → warn
  • 2xx/3xx responses → debug

Serialized fields per request:

FieldSource
req.methodHTTP method
req.urlRequest URL
req.oponeM2M operation (from X-M2M-Op header)
req.frOriginator (from X-M2M-Origin)
req.rqiRequest ID (from X-M2M-RI)
req.bodyParsed request body (available at debug level)
res.statusCodeHTTP status code
res.rsconeM2M response status code (from X-M2M-RSC)
res.bodyResponse body (captured via res.json() interceptor)

The /health and /metrics endpoints are excluded from HTTP logging.


oneM2M Primitive Logging

All oneM2M requests and responses pass through cse/reqPrim.js, which logs them at info level regardless of transport (HTTP or MQTT).

EventLevelFields
Request receivedinfoop, to, fr, rqi, pc (payload, omitted if empty)
Response sentinforsc, rqi, pc (payload, omitted if empty)
Full raw primitivetraceprim

pc is the oneM2M payload content — equivalent to the HTTP body. It is automatically omitted from the log when empty (e.g. RETRIEVE or DELETE requests).


MQTT Logging

MQTT bindings do not have middleware support, so logging is done manually in bindings/mqtt.js using a child logger:

const logger = require('../logger').child({ module: 'mqtt', binding: 'mqtt' });

Logged events:

EventLevelFields
Client connectinginfoendpoint
Subscriptions readyinfocseId
Request receiveddebugtopic, originator, rqi, op, to
Response sentdebugtopic, rsc, rqi
Full primitivedebugprim
Publish failederrorerr, topic
Reconnectingwarnendpoint
Offlineerrorendpoint

Log Output Examples

JSON (production)

{"level":30,"time":"2026-04-04T19:23:11.452+09:00","pid":1234,"cseId":"/Mobius4","module":"db","msg":"PostgreSQL connected"}
{"level":30,"time":"2026-04-04T19:23:11.502+09:00","pid":1234,"cseId":"/Mobius4","module":"http","port":7599,"msg":"HTTP server listening"}
{"level":20,"time":"2026-04-04T19:23:15.100+09:00","pid":1234,"cseId":"/Mobius4","module":"mqtt","binding":"mqtt","topic":"/oneM2M/req/C1234/Mobius4/json","originator":"C1234","op":1,"to":"Mobius","msg":"mqtt request received"}
{"level":50,"time":"2026-04-04T19:23:16.200+09:00","pid":1234,"cseId":"/Mobius4","module":"cnt","err":{"message":"duplicate key value","stack":"..."},"msg":"create_a_cnt failed"}

> **Note:** The `level` field in file logs is a numeric value (pino standard). See the [Log Levels](#log-levels) table for the mapping. Timestamps use the system's local timezone automatically.
> To view file logs in human-readable form: `cat logs/mobius4.log | npx pino-pretty`

Pretty-print (development, console.pretty: true)

[10:23:11] INFO  (db): PostgreSQL connected
[10:23:11] INFO  (http): HTTP server listening
    port: 7599
[10:23:15] DEBUG (mqtt): mqtt request received
    topic: "/oneM2M/req/C1234/Mobius4/json"
    originator: "C1234"
    op: 1

Log Aggregation

For production deployments, JSON output can be ingested by:

  • ELK Stack — Filebeat → Logstash → Elasticsearch → Kibana
  • Grafana Loki — Promtail → Loki → Grafana
  • AWS CloudWatch / GCP Logging — stdout JSON is natively captured in container environments

Filter by module to scope to a specific component, or by level to see only errors.


Adding a New Module

When creating a new file under cse/ or bindings/:

  1. Add the child logger declaration at the top:

    const logger = require('../logger').child({ module: 'my-module' });
    // or for files in cse/resources/:
    const logger = require('../../logger').child({ module: 'my-module' });
    
  2. Never use console.log, console.error, or console.warn — use the appropriate Pino level.

  3. Always pass error objects as { err } in catch blocks, not as the message string.