Skip to content
Start free evaluation

Use diagnostic logging

PDComm diagnostic logging is off by default. This page shows the three ways to turn it on and how to connect a collector. For the record layout and the loss rules, see How structured diagnostic logs work.

Method Use it to
Environment variables Turn on logging for an existing program or simulator without changing code.
API Turn logging on and off, or check its status, while your program runs.
Simulator actions Change the logging of the headless, MCP, or GUI simulators while they run.

Turn on logging with environment variables

Section titled “Turn on logging with environment variables”

A library reads the environment once, when it first uses its logger. Set the variables in the shell that starts your program or simulator; a program that is already running doesn’t pick them up, so restart it.

Variable Value Default
PDCOMM_LOG One of error, warn, info, debug, trace (lowercase). Unset or off leaves logging off. Unset (off)
PDCOMM_LOG_FILE Log file path Standard error when unset
PDCOMM_LOG_MAX_BYTES Rotation size, 8192 to 1073741824 bytes 10485760
PDCOMM_LOG_FILES Files to keep, counting the active one, 1 to 16 3
PDCOMM_LOG_FORENSICS 1, true, 0, or false; only with debug or trace Off
터미널 창
mkdir -p ./logs
export PDCOMM_LOG=debug
export PDCOMM_LOG_FILE=./logs/pdcomm-client.jsonl
# Start your SDK application or simulator from this shell.

The SECS/GEM and Modbus libraries read the same variables. If one process uses both, both loggers write to and rotate the same file, so use the API instead and give each library its own file. Separate processes, such as a client, a server, and a simulator, need separate files too.

You pass the configuration as one JSON object. Every key is optional, and unknown keys are rejected.

Key Value Default
enabled true or false true
level "error", "warn", "info", "debug", "trace" "info"
file A file path, or null for standard error null
max_bytes 8192 to 1073741824 10485760
files 1 to 16 3
forensics true or false; only with debug or trace false

{"level":"debug","file":"./logs/sdk.jsonl"}, for example, turns logging on at debug and writes to a file, and {"enabled":false} turns it off. An invalid configuration leaves the previous one in effect. Configuring and flushing wait up to two seconds for the logging thread, so call them outside your communication path.

Language SECS/GEM Modbus
C pdcomm_logging_configure, pdcomm_logging_status, pdcomm_logging_flush pdcomm_modbus_logging_configure, pdcomm_modbus_logging_status, pdcomm_modbus_logging_flush
C++ DiagnosticLogging (pdcomm_logging.hpp) DiagnosticLogging (pdcomm/modbus/pdcomm_modbus_logging.hpp)
.NET DiagnosticLogging DiagnosticLogging
  • The C functions take UTF-8 JSON bytes and a length of at most 16384 bytes. When the output buffer is too small, the status function leaves it unchanged and reports the size it needs.
  • The C++ classes offer the static functions configure, status, and flush, and throw std::runtime_error on failure.
  • In .NET, Configure serializes an anonymous object to JSON, Status returns a JsonElement, and failures throw InvalidOperationException.

Return codes:

Functions Success Invalid input Buffer too small Internal error
pdcomm_logging_* 0 1 3 5
pdcomm_modbus_logging_* 0 1 4 3

This example turns on logging for the Modbus library, runs one request, prints the status, and turns logging off again:

diagnostics/modbus/logging.cpp
#include <pdcomm/modbus/pdcomm_modbus.hpp>
#include <pdcomm/modbus/pdcomm_modbus_logging.hpp>
#include <iostream>
int main(int argc, char **argv) {
if (argc != 2) return 2;
try {
pdcomm::modbus::DiagnosticLogging::configure(argv[1]);
pdcomm::modbus::Endpoint endpoint;
std::cout << endpoint.call(R"({"action":"execute","pdu":[6,0,0,0,42]})") << '\n';
pdcomm::modbus::DiagnosticLogging::flush();
std::cout << pdcomm::modbus::DiagnosticLogging::status() << '\n';
pdcomm::modbus::DiagnosticLogging::configure(R"({"enabled":false})");
std::cout << pdcomm::modbus::DiagnosticLogging::status() << '\n';
} catch (const std::exception &error) {
std::cerr << error.what() << '\n';
return 1;
}
return 0;
}

To build and run the complete example, follow Capture and read a diagnostic log.

The Modbus and OPC UA headless simulators accept these actions. Their MCP servers offer the same actions as tools with a product prefix, such as modbus_logging_configure and opcua_logging_status.

{"action":"logging_configure","config":{"enabled":true,"level":"debug","file":"./logs/modbus.jsonl"}}
{"action":"logging_status"}
{"action":"logging_flush"}
{"action":"logging_configure","config":{"enabled":false}}

logging_configure and logging_status return the status object, and logging_flush returns {"flushed":true}. An invalid configuration returns one of these errors:

Error Cause
Unknown logging option An unknown key.
Invalid logging level level isn’t one of the five levels.
Logging limits are out of range max_bytes or files is out of range.
Forensics requires debug or trace logging forensics with a lower level.
Invalid logging file file is empty or not a string.
Cannot open logging sink The file can’t be opened. Check the folder and its permissions.

In the OPC UA simulator GUI, open the Trace & Diagnostics tab, select Diagnostic log (off by default), enter a file path, and click Apply logging. The line below shows the state and the written, dropped, and write errors counts.

{"build_id":"…","configuration_errors":0,"dropped":0,"enabled":true,"files":3,"forensic_sample_errors":0,"forensic_sample_interval_ms":1000,"forensic_samples":0,"forensics":false,"instance_id":"55856-1791996289686444000","level":4,"level_name":"debug","max_bytes":10485760,"queue_capacity":1024,"record_limit":8192,"schema_version":1,"sink":"file","write_errors":0,"written":5}
  • level is a number (0 for off up to 5 for trace); level_name has the name.
  • sink is file or stderr. The file path itself isn’t shown.
  • written, dropped, write_errors, and configuration_errors tell you about loss and errors.
Product component Main event values
SECS/GEM ffi call_completed (status and elapsed time)
SECS/GEM hsms start, stop, state_changed, received, transaction_failed, protocol_error, transport_error
SECS/GEM hsms_transport socket_read, socket_write
SECS/GEM secs1 received, sent, send_failed, fault
Modbus ffi call_entered, lock_acquired, call_completed
Modbus service action_started, action_completed
Modbus tcp_client connected, submitted, socket_write, socket_read, completed, failed
Modbus tcp_server connection_accepted, request_received, response_queued, socket_write, connection_closed
Modbus serial io_batch, timeout
OPC UA simulator service action_started, action_completed

The fields of a Modbus failed event include the error category (error_type), whether it timed out, the pending requests, how much of a reply arrived (rx_partial_bytes), and the longest polling gap (max_poll_gap_us). Use them to tell a missing reply from a partial one, or from slow SDK processing.

The logs are JSON Lines files, so collectors such as Fluent Bit, Vector, or the OpenTelemetry Collector read them as they are. The SDK doesn’t need a collector to run and doesn’t prescribe a storage backend. This Fluent Bit configuration reads the rotating files and writes them to standard output:

fluent-bit.conf
[SERVICE]
Flush 1
Parsers_File parsers.conf
[INPUT]
Name tail
Path /var/log/pdcomm/*.jsonl*
Tag pdcomm
Parser pdcomm_json
DB /var/lib/fluent-bit/pdcomm-offsets.db
DB.Sync Full
DB.Compare_Filename Off
Refresh_Interval 1
Rotate_Wait 30
Mem_Buf_Limit 10MB
Buffer_Chunk_Size 16k
Buffer_Max_Size 16k
Skip_Long_Lines Off
Read_From_Head On
[OUTPUT]
Name stdout
Match pdcomm
Format json_lines
Json_date_key ingested_at
Json_date_format double
parsers.conf
[PARSER]
Name pdcomm_json
Format json
  • *.jsonl* in Path also reads the rotated .1 and .2 files. DB stores the read positions, so restarting the collector neither duplicates nor skips records; deleting that file makes Fluent Bit read from the beginning again.
  • Buffer_Max_Size 16k is larger than the largest record (8192 bytes).
  • ingested_at is when the collector read the record (UTC epoch seconds). The SDK’s event time stays in the record’s timestamp_unix_ms; don’t mix the two up.
  • Killing the collector or exceeding its storage limits can lose or duplicate records. Check instance_id and sequence to find them.

If you store logs in Grafana Loki, use only low-cardinality fields such as protocol and level as labels, and query ever-growing values such as request_id, connection_id, and sequence as JSON fields (Loki labels). Compare the Fluent Bit settings with the Tail input documentation for the version you run.

When you report a problem, include the following, but leave out raw customer data:

  • The SDK version and build_id, the operating system, and the CPU architecture.
  • When it failed, and the instance_id, request_id, and connection_id.
  • The original error message and return code.
  • dropped, write_errors, and configuration_errors from the status.
  • The relevant part of the JSONL log.