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 ./logsexport PDCOMM_LOG=debugexport PDCOMM_LOG_FILE=./logs/pdcomm-client.jsonl# Start your SDK application or simulator from this shell.New-Item -ItemType Directory -Force ./logs | Out-Null$env:PDCOMM_LOG = 'debug'$env:PDCOMM_LOG_FILE = './logs/pdcomm-client.jsonl'# Start your SDK application or simulator from this PowerShell.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.
Control logging through the API
Section titled “Control logging through the API”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, andflush, and throwstd::runtime_erroron failure. - In .NET,
Configureserializes an anonymous object to JSON,Statusreturns aJsonElement, and failures throwInvalidOperationException.
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:
#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;}using System.Runtime.InteropServices;if (args.Length != 2 || !Path.IsPathFullyQualified(args[0])) return 2;NativeLibrary.SetDllImportResolver(typeof(PDComm.Modbus.Endpoint).Assembly, (name, assembly, search) => name == "pdcomm_modbus_ffi" ? NativeLibrary.Load(args[0]) : IntPtr.Zero);PDComm.Modbus.DiagnosticLogging.Configure(new { enabled = true, level = "debug", file = args[1] });using (var endpoint = new PDComm.Modbus.Endpoint()) Console.WriteLine(endpoint.Call(new { action = "execute", pdu = new[] { 6, 0, 0, 0, 42 } }));PDComm.Modbus.DiagnosticLogging.Flush();Console.WriteLine(PDComm.Modbus.DiagnosticLogging.Status());PDComm.Modbus.DiagnosticLogging.Configure(new { enabled = false });Console.WriteLine(PDComm.Modbus.DiagnosticLogging.Status());return 0;To build and run the complete example, follow Capture and read a diagnostic log.
Control logging in the simulators
Section titled “Control logging in the simulators”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.
Check the status
Section titled “Check the status”{"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}levelis a number (0 for off up to 5 fortrace);level_namehas the name.sinkisfileorstderr. The file path itself isn’t shown.written,dropped,write_errors, andconfiguration_errorstell you about loss and errors.
Logged events
Section titled “Logged events”| 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.
Forward logs to a collector
Section titled “Forward logs to a collector”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:
[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[PARSER] Name pdcomm_json Format json*.jsonl*inPathalso reads the rotated.1and.2files.DBstores 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 16kis larger than the largest record (8192 bytes).ingested_atis when the collector read the record (UTC epoch seconds). The SDK’s event time stays in the record’stimestamp_unix_ms; don’t mix the two up.- Killing the collector or exceeding its storage limits can lose or duplicate records. Check
instance_idandsequenceto 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.
What to include in a support request
Section titled “What to include in a support request”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, andconnection_id. - The original error message and return code.
dropped,write_errors, andconfiguration_errorsfrom the status.- The relevant part of the JSONL log.