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.
Products that log
Section titled “Products that log”| 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.
Anatomy of a record
Section titled “Anatomy of a record”{"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. |
Follow a request with correlation IDs
Section titled “Follow a request with correlation IDs”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_idandrequest_id. - For Modbus, also match
connection_idandtransaction_idinfields; transaction IDs repeat across connections. - For SECS/GEM, match HSMS state changes and
system_bytes. monotonic_usmeasures order and gaps within one logger. To compare logs from different hosts, usetimestamp_unix_msand keep the host clocks synchronized.
Levels
Section titled “Levels”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.
Files and rotation
Section titled “Files and rotation”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.
Loss and latency
Section titled “Loss and latency”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.
What is never logged
Section titled “What is never logged”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.
Forensic samples
Section titled “Forensic samples”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.
Next steps
Section titled “Next steps”- Use diagnostic logging shows every setting and how to connect a collector.
- Follow the capture and read a diagnostic log tutorial.