콘텐츠로 이동
무료 평가 시작

진단 로그 남기고 읽기

이 튜토리얼에서는 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"

예제는 명령줄 인수로 받은 설정으로 로그를 켜고, 로컬 메모리 모델에 쓰기 요청 하나를 실행하고, 상태를 출력한 뒤 로그를 끕니다.

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;
}
  1. 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
  2. 실행합니다. 설정 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}
  1. 로그 파일에는 레코드 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.jsonl
    1 debug ffi call_entered 1
    2 debug ffi lock_acquired 1
    3 debug service action_started 1
    4 debug service action_completed 1
    5 debug ffi call_completed 1
  2. 다섯 레코드의 request_id가 모두 1입니다. API 호출 한 번이 C ABI 진입(call_entered), 잠금 획득(lock_acquired), 서비스 처리(action_started, action_completed), 반환(call_completed) 순서로 기록되었습니다. fields의 wait_us와 elapsed_us로 잠금 대기와 처리 시간을 확인합니다.

C# 예제는 Modbus .NET 바인딩 소스를 프로젝트 참조로 빌드합니다. 외부 NuGet 패키지는 쓰지 않습니다.

diagnostics/modbus/Program.cs
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;
  1. 빌드합니다. PdcommModbusSdk 속성에 SDK 폴더를 지정합니다.

    터미널 창
    dotnet build "$EXAMPLES/diagnostics/modbus/LoggingExample.csproj" -c Release \
    -p:PdcommModbusSdk="$MODBUS_SDK" -o logging-dotnet
  2. 실행합니다. 첫 인수는 네이티브 라이브러리의 절대 경로입니다. 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. 터미널 1에서 Modbus RTU 튜토리얼의 예제 폴더 vserial/로 이동해 버스를 시작합니다.

    터미널 창
    cd "$EXAMPLES/vserial" && pdcomm-vserial run --config rs485-bus.json
  2. 터미널 2에서 unit 1 서버를 시작합니다.

    터미널 창
    cd "$EXAMPLES/vserial" && cat modbus-rtu/drive1.jsonl - | "$MODBUS_SDK/bin/pdcomm-modbus-headless"
  3. 터미널 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"
  4. 터미널 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}로 끝납니다.

  5. 세 터미널을 정리합니다. 터미널 3과 2에서 Ctrl+D를 누르고, 터미널 1에 {"command":"stop"}을 입력합니다.

  1. 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
  2. 마지막 요청의 레코드는 다음과 같습니다(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"}
  3. 레코드를 해석합니다.

    • 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로 끝남 인수 두 개를 주지 않았거나 네이티브 라이브러리 경로가 절대 경로가 아닙니다.