Capture and read a diagnostic log
In this tutorial you turn on the Modbus SDK diagnostic log in two ways. First, an example program turns logging on and off through the API. Then you turn on logging for the headless simulator with environment variables and trace a request to a device that never answers.
- Turn on logging through the API and check the
writtenanddroppedcounts. - Group the records of one request by
request_idand read its processing steps. - Find the device and function code of a failed request in the timeout record.
What you need
Section titled “What you need”- macOS or Linux, a C++14 compiler for the C++ example, and the .NET 8 SDK for the C# example.
- The Modbus SDK (
include/,lib/,bin/,bindings/). The commands below find the SDK folder throughMODBUS_SDKand the examples folder throughEXAMPLES. - For step 4, the lab and server from Test Modbus RTU on a virtual RS-485 bus.
export MODBUS_SDK="$HOME/PDComm/pdcomm-modbus" # replace with your SDK folderexport EXAMPLES="$HOME/PDComm/examples" # replace with your examples foldermkdir -p "$HOME/PDComm/logging-lab/logs" && cd "$HOME/PDComm/logging-lab"Step 1: Build the example
Section titled “Step 1: Build the example”The example turns on logging with the configuration from its command-line argument, runs one write request against the local memory model, prints the status, and turns logging off.
#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;}-
Build the C++ example.
-Wl,-rpathlets the program find the library in the SDK’slib/folder.터미널 창 c++ -std=c++14 -I "$MODBUS_SDK/include" "$EXAMPLES/diagnostics/modbus/logging.cpp" \-L "$MODBUS_SDK/lib" -lpdcomm_modbus_ffi -Wl,-rpath,"$MODBUS_SDK/lib" -o logging-example -
Run it. The configuration JSON asks for
debugrecords in./logs/modbus-cpp.jsonl.터미널 창 ./logging-example '{"level":"debug","file":"./logs/modbus-cpp.jsonl"}'The program prints three lines: the reply to the write request, the status with logging on, and the status after it turned logging off. In the second line, check
"enabled":true,"written":5,"dropped":0, and"write_errors":0.{"ok":true,"result":{"response":[6,0,0,0,42],"source":"local","status":"success"}}{"build_id":"…","configuration_errors":0,"dropped":0,"enabled":true,…,"sink":"file","write_errors":0,"written":5}{"build_id":"…","configuration_errors":0,"dropped":0,"enabled":false,…,"level_name":"off",…,"written":5}
Step 2: Read the records
Section titled “Step 2: Read the records”-
The log file holds five records, one JSON object per line. Print the main fields:
터미널 창 python3 -c 'import json,sys; [print(r["sequence"], r["level"], r["component"], r["event"], r["request_id"]) for r in map(json.loads, open(sys.argv[1]))]' logs/modbus-cpp.jsonl1 debug ffi call_entered 12 debug ffi lock_acquired 13 debug service action_started 14 debug service action_completed 15 debug ffi call_completed 1 -
All five records share
request_id1. One API call was logged as it entered the C ABI (call_entered), took the endpoint lock (lock_acquired), ran in the service (action_started,action_completed), and returned (call_completed).wait_usandelapsed_usinfieldsshow the lock wait and the processing time.
Step 3: Do the same in C#
Section titled “Step 3: Do the same in C#”The C# example builds the Modbus .NET binding source as a project reference, without any external NuGet package.
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;<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable><TreatWarningsAsErrors>true</TreatWarningsAsErrors></PropertyGroup> <ItemGroup><ProjectReference Include="$(PdcommModbusSdk)/bindings/dotnet/PDComm.Modbus/PDComm.Modbus.csproj" /></ItemGroup></Project>-
Build it, passing the SDK folder in the
PdcommModbusSdkproperty:터미널 창 dotnet build "$EXAMPLES/diagnostics/modbus/LoggingExample.csproj" -c Release \-p:PdcommModbusSdk="$MODBUS_SDK" -o logging-dotnet -
Run it. The first argument is the absolute path of the native library; on Linux, pass
libpdcomm_modbus_ffi.so.터미널 창 dotnet logging-dotnet/LoggingExample.dll "$MODBUS_SDK/lib/libpdcomm_modbus_ffi.dylib" "$PWD/logs/modbus-csharp.jsonl"You get the same three lines as from the C++ example, and five records in
logs/modbus-csharp.jsonl.
Step 4: Turn on headless simulator logging with environment variables
Section titled “Step 4: Turn on headless simulator logging with environment variables”To turn on logging without changing code, use environment variables. This time you send a request to a device that doesn’t answer, so the log records a failure.
-
In terminal 1, go to the
vserial/examples folder from the Modbus RTU tutorial and start the bus:터미널 창 cd "$EXAMPLES/vserial" && pdcomm-vserial run --config rs485-bus.json -
In terminal 2, start the unit 1 server:
터미널 창 cd "$EXAMPLES/vserial" && cat modbus-rtu/drive1.jsonl - | "$MODBUS_SDK/bin/pdcomm-modbus-headless" -
In terminal 3, start a client with logging on. The log file goes to
logging-lab/logs.터미널 창 cd "$EXAMPLES/vserial"cat modbus-rtu/master.jsonl - | PDCOMM_LOG=debug \PDCOMM_LOG_FILE="$HOME/PDComm/logging-lab/logs/rtu-client.jsonl" "$MODBUS_SDK/bin/pdcomm-modbus-headless" -
Type two requests into terminal 3. Unit 1 answers; unit 9 isn’t on the bus.
{"action":"request","unit":1,"pdu":"0300000001"}{"action":"request","unit":9,"pdu":"0300000001"}The first request returns
[3,2,5,220], and the second ends after one second with{"error":"Timeout","ok":false}. -
Clean up the three terminals: press Ctrl+D in terminals 3 and 2, and type
{"command":"stop"}in terminal 1.
Step 5: Trace the failed request
Section titled “Step 5: Trace the failed request”-
From the
logging-labfolder, print the records, this time with theirfields:터미널 창 cd "$HOME/PDComm/logging-lab"python3 -c 'import json,sys; [print(r["sequence"], r["level"], r["component"], r["event"], r["request_id"], json.dumps(r["fields"])) for r in map(json.loads, open(sys.argv[1]))]' logs/rtu-client.jsonl -
The records of the last request look like this (the
request_idand times differ from run to run):9 debug service action_started 7 {"action": "request", "role": "client", "timeout_ms": 1000, "transport": "rtu", "unit": 9}10 debug serial io_batch 7 {"baud": 19200, "bytes": 8, "direction": "tx", "timestamp_us": 19916, "transport": "Rtu"}11 error serial timeout 7 {"function": 3, "received_bytes": 0, "unit": 9}12 error service action_completed 7 {"action": "request", "elapsed_us": 1003520, "error_code": "Timeout", "success": false, "transport": "rtu"} -
Interpret them:
io_batchwith"direction": "tx"and"bytes": 8: the 8-byte request frame was written to the port, so the request left for the line.timeoutwith"received_bytes": 0: not a single reply byte arrived, which rules out a partial reply.elapsed_usinaction_completed: the client waited for the full configured timeout (1000 ms).
So the SDK sent the request and unit 9 never answered. In a real system you would now check the device address, the wiring, and the device’s power.
Clean up
Section titled “Clean up”- Delete the
logging-labfolder. The logs contain no payloads, but they do contain peer addresses and port names, so share only the parts you need. - Make sure
PDCOMM_LOGisn’t set in your shell. This tutorial set the variables for single commands only.
Common errors
Section titled “Common errors”| Symptom | Cause and fix |
|---|---|
| No log file appears | One invalid value makes the library ignore the whole environment configuration. PDCOMM_LOG must be a lowercase level name. Check configuration_errors in the status. |
The exception Cannot configure PDComm logging |
The configuration JSON is invalid or the file can’t be opened. Check that the folder exists. |
| Expected records are missing | The program exited without flushing, or records were dropped. Check dropped and write_errors in the status. |
| The C# example exits with code 2 | It didn’t get two arguments, or the native library path isn’t absolute. |
Next steps
Section titled “Next steps”- See every setting and how to connect a collector in Use diagnostic logging.