Skip to content
Start free evaluation

How structured diagnostic logs work

PDComm writes opt-in diagnostic logs that you turn on when you investigate a communication problem. Logging is off by default. When it’s on, each line is one JSON object (JSON Lines, or JSONL), which you can read yourself and which collectors such as Fluent Bit process as is.

Product How you control the log
SECS/GEM SDK (pdcomm_ffi) Environment variables, C pdcomm_logging_*, C++ pdcomm::DiagnosticLogging, .NET PDComm.DiagnosticLogging
Modbus SDK (pdcomm_modbus_ffi) Environment variables, C pdcomm_modbus_logging_*, C++ pdcomm::modbus::DiagnosticLogging, .NET PDComm.Modbus.DiagnosticLogging
Modbus simulator Environment variables, and the logging_configure, logging_status, and logging_flush actions of the headless and MCP front ends
OPC UA simulator Environment variables, the same headless and MCP actions, and the diagnostic log setting in the GUI

Each library has its own logger. If one process uses both the SECS/GEM and the Modbus library, each keeps its own status counters, instance_id, and sequence, so configure each logger to write to its own file.

{"arch":"aarch64","build_id":"…","component":"ffi","event":"call_entered","fields":{"endpoint_id":1,"input_bytes":39},"instance_id":"55856-1791996289686444000","level":"debug","monotonic_us":286,"os":"macos","pid":55856,"protocol":"modbus","request_id":1,"schema_version":1,"sdk_version":"0.1.0","sequence":1,"thread":"ThreadId(2)","timestamp_unix_ms":1791996289686}

Keys appear in alphabetical order.

Field Meaning
schema_version The record format version, currently 1.
timestamp_unix_ms When the event happened, in Unix epoch milliseconds (UTC).
monotonic_us Microseconds since the logger was created. Compare it only within one logger.
sequence Counts up from 1 per logger. Use it to find lost or duplicated records.
instance_id Identifies the logger instance (<pid>-<start nanoseconds>).
pid, thread Process ID and thread.
sdk_version, build_id, os, arch Library version, build identifier, operating system, and CPU architecture.
level error, warn, info, debug, or trace.
protocol, component, event Where the event happened (protocol such as secsgem or modbus, then component) and its name.
request_id Groups the records of one API call.
fields Event-specific values.

One request produces several records. A Modbus TCP client request, for example, logs call_entered, lock_acquired, action_started, submitted, socket_write, socket_read, then completed or failed, and finally call_completed.

  • Group the records of one request by instance_id and request_id.
  • For Modbus, also match connection_id and transaction_id in fields; transaction IDs repeat across connections.
  • For SECS/GEM, match HSMS state changes and system_bytes.
  • monotonic_us measures order and gaps within one logger. To compare logs from different hosts, use timestamp_unix_ms and keep the host clocks synchronized.

From least to most detail: error, warn, info, debug, trace. Use info for everyday operation, debug to investigate communication, and trace for short, very detailed observations. Most send and receive events are recorded at debug.

File output rotates by size. When the next record would push the file past max_bytes (10 MiB by default), the logger renames the file to .1 and shifts older files along. files (3 by default) counts the active file, so the defaults keep client.jsonl, client.jsonl.1, and client.jsonl.2. On Unix, new files are readable and writable by the owner only (0600).

Without a file, records go to standard error. The standard output of the headless simulators and MCP servers carries replies only, so logs never mix with them.

Logging must never slow communication down, so it follows these rules:

  • Records pass through a queue of 1024 entries to a writer thread. When the queue is full, records are dropped.
  • If a protocol thread can’t take the logger lock immediately, it drops the record instead of waiting.
  • A record larger than 8192 bytes, including its newline, is dropped.

The status counts dropped records in dropped, failed writes in write_errors, and rejected configurations in configuration_errors. If any of these is not zero, don’t conclude that an event didn’t happen just because it’s missing from the log. Records still in the queue or the operating system cache when a process crashes are not guaranteed, so flush before the process exits.

Logs never contain payloads such as frames, PDUs, or message bodies, only their sizes (body_bytes, pdu_bytes, bytes). They never contain secret keys, credentials, or the whole environment. They do contain peer addresses and listening addresses, so check that before you send logs elsewhere.

With forensics on (requires debug or trace), the logger records a process resource sample (resource_sample) about once a second, and every internal stage that takes 20 ms or longer as slow_stage. Use them to tell whether a slow response comes from SDK processing, the peer device, or operating system scheduling.