진단 로그 남기고 읽기
이 튜토리얼에서는 Modbus SDK의 진단 로그를 두 가지 방법으로 켭니다. 먼저 예제 프로그램이 API로 로그를 켜고 끕니다. 다음으로 환경 변수로 헤드리스 시뮬레이터의 로그를 켜고, 응답하지 않는 장치에 보낸 요청을 로그에서 추적합니다.
- API로 로그를 켜고 상태의
written,dropped값을 확인합니다. request_id로 한 요청의 레코드를 모아 처리 단계를 읽습니다.- 시간 초과 레코드에서 실패한 요청의 장치와 기능 코드를 찾습니다.
준비물
섹션 제목: “준비물”- macOS 또는 Linux. C++ 예제에는 C++14 컴파일러가, C# 예제에는 .NET 8 SDK가 필요합니다.
- Modbus SDK(
include/,lib/,bin/,bindings/). 아래 명령은 SDK 폴더를MODBUS_SDK로, 예제 폴더를EXAMPLES로 가리킵니다. - 6단계는 가상 RS-485 버스에서 Modbus RTU 시험하기의 랩과 서버를 다시 사용합니다.
export MODBUS_SDK="$HOME/PDComm/pdcomm-modbus" # 실제 SDK 폴더로 바꿉니다export EXAMPLES="$HOME/PDComm/examples" # 실제 예제 폴더로 바꿉니다mkdir -p "$HOME/PDComm/logging-lab/logs" && cd "$HOME/PDComm/logging-lab"1단계: 예제 빌드하기
섹션 제목: “1단계: 예제 빌드하기”예제는 명령줄 인수로 받은 설정으로 로그를 켜고, 로컬 메모리 모델에 쓰기 요청 하나를 실행하고, 상태를 출력한 뒤 로그를 끕니다.
#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;}-
C++ 예제를 빌드합니다.
-Wl,-rpath는 실행 파일이 SDK의lib/에서 라이브러리를 찾게 합니다.터미널 창 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 -
실행합니다. 설정 JSON은
debug레벨로./logs/modbus-cpp.jsonl에 쓰라는 뜻입니다.터미널 창 ./logging-example '{"level":"debug","file":"./logs/modbus-cpp.jsonl"}'세 줄이 출력됩니다. 첫 줄은 쓰기 요청의 응답, 둘째 줄은 로그를 켠 상태, 셋째 줄은 끈 뒤의 상태입니다. 둘째 줄에서
"enabled":true,"written":5,"dropped":0,"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}
2단계: 레코드 읽기
섹션 제목: “2단계: 레코드 읽기”-
로그 파일에는 레코드 5개가 있습니다. 한 줄이 JSON 객체 하나입니다. 주요 필드만 출력합니다.
터미널 창 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 -
다섯 레코드의
request_id가 모두 1입니다. API 호출 한 번이 C ABI 진입(call_entered), 잠금 획득(lock_acquired), 서비스 처리(action_started,action_completed), 반환(call_completed) 순서로 기록되었습니다.fields의wait_us와elapsed_us로 잠금 대기와 처리 시간을 확인합니다.
3단계: C#으로 같은 일 하기
섹션 제목: “3단계: C#으로 같은 일 하기”C# 예제는 Modbus .NET 바인딩 소스를 프로젝트 참조로 빌드합니다. 외부 NuGet 패키지는 쓰지 않습니다.
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>-
빌드합니다.
PdcommModbusSdk속성에 SDK 폴더를 지정합니다.터미널 창 dotnet build "$EXAMPLES/diagnostics/modbus/LoggingExample.csproj" -c Release \-p:PdcommModbusSdk="$MODBUS_SDK" -o logging-dotnet -
실행합니다. 첫 인수는 네이티브 라이브러리의 절대 경로입니다. Linux에서는
libpdcomm_modbus_ffi.so를 지정합니다.터미널 창 dotnet logging-dotnet/LoggingExample.dll "$MODBUS_SDK/lib/libpdcomm_modbus_ffi.dylib" "$PWD/logs/modbus-csharp.jsonl"C++ 예제와 같은 세 줄이 나오고
logs/modbus-csharp.jsonl에 레코드 5개가 생깁니다.
4단계: 환경 변수로 헤드리스 시뮬레이터 로그 켜기
섹션 제목: “4단계: 환경 변수로 헤드리스 시뮬레이터 로그 켜기”코드를 바꾸지 않고 로그를 켤 때는 환경 변수를 씁니다. 이번에는 응답하지 않는 장치에 요청을 보내 실패를 기록합니다.
-
터미널 1에서 Modbus RTU 튜토리얼의 예제 폴더
vserial/로 이동해 버스를 시작합니다.터미널 창 cd "$EXAMPLES/vserial" && pdcomm-vserial run --config rs485-bus.json -
터미널 2에서 unit 1 서버를 시작합니다.
터미널 창 cd "$EXAMPLES/vserial" && cat modbus-rtu/drive1.jsonl - | "$MODBUS_SDK/bin/pdcomm-modbus-headless" -
터미널 3에서 로그를 켠 클라이언트를 시작합니다. 로그 파일은
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" -
터미널 3에 두 요청을 입력합니다. unit 1은 응답하고 unit 9는 버스에 없습니다.
{"action":"request","unit":1,"pdu":"0300000001"}{"action":"request","unit":9,"pdu":"0300000001"}첫 요청은
[3,2,5,220]을 받고, 둘째 요청은 1초 뒤{"error":"Timeout","ok":false}로 끝납니다. -
세 터미널을 정리합니다. 터미널 3과 2에서 Ctrl+D를 누르고, 터미널 1에
{"command":"stop"}을 입력합니다.
5단계: 실패한 요청 추적하기
섹션 제목: “5단계: 실패한 요청 추적하기”-
logging-lab폴더에서 레코드를 출력합니다. 이번에는fields도 함께 봅니다.터미널 창 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 -
마지막 요청의 레코드는 다음과 같습니다(
request_id와 시간 값은 실행마다 다릅니다).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"} -
레코드를 해석합니다.
io_batch의"direction": "tx","bytes": 8: 요청 프레임 8바이트를 포트에 썼습니다. 요청은 회선에 나갔습니다.timeout의"received_bytes": 0: 응답 바이트를 하나도 받지 못했습니다. 응답 일부만 온 경우와 구별됩니다.action_completed의elapsed_us: 설정한 시간 초과(1000ms)만큼 기다렸습니다.
결론적으로 SDK는 요청을 보냈고 unit 9는 응답하지 않았습니다. 실제 시스템이라면 장치 주소, 배선, 장치 전원을 확인합니다.
logging-lab폴더를 지웁니다. 로그에는 페이로드가 없지만 상대 주소와 포트 이름이 있으므로 외부로 보낼 때는 필요한 부분만 남깁니다.- 셸에 설정한
PDCOMM_LOG변수가 남아 있지 않은지 확인합니다. 이 튜토리얼은 명령 앞에만 변수를 지정했습니다.
자주 나는 오류
섹션 제목: “자주 나는 오류”| 증상 | 원인과 해결 |
|---|---|
| 로그 파일이 생기지 않음 | 값이 잘못되면 환경 설정 전체를 무시합니다. PDCOMM_LOG는 소문자 레벨 이름이어야 합니다. 상태의 configuration_errors를 확인합니다. |
Cannot configure PDComm logging 예외 |
설정 JSON이 잘못되었거나 파일을 열 수 없습니다. 폴더가 있는지 확인합니다. |
| 기대한 레코드가 빠짐 | 종료 전에 플러시하지 않았거나 레코드가 버려졌습니다. 상태의 dropped와 write_errors를 확인합니다. |
| C# 예제가 종료 코드 2로 끝남 | 인수 두 개를 주지 않았거나 네이티브 라이브러리 경로가 절대 경로가 아닙니다. |
다음 단계
섹션 제목: “다음 단계”- 진단 로그 사용하기에서 모든 설정과 수집기 연결을 확인합니다.