LoginSignup
25
16

テキスト好きのためのDLTログ入門

Last updated at Posted at 2019-01-25

1. DLTとは?

Diagnostic Log and Traceの略。
rsyslog/jorunaldのようなログプラットフォームのこと。

AUTOSARで仕様が決められていて、GENIVIがオープンソースで実装を公開している。
このことからもわかるように、車載業界でログプラットフォームとして使われている。

ブロックチェーンの基礎技術、Distributed Ledger Technology; 分散型台帳管理のことだと思った方、ごめんなさい。
ちなみに2019/1/25時点でQiitaのdltタグは全部分散型台帳管理のことだった。。。

2. DLTのシステム構成

githubで公開されているDLTの構成図が下記。

DLT構成図

非常にシンプルな構成だ。
DLT Daemonが、ログ収集およびファイルシステムへの保存 or リモートサーバへの転送を担う。
各アプリケーションはDLT LibraryをリンクしてAPI経由でメッセージをDLT Daemonに転送する。
アプリ<->Deamon間の通信はいくつか選択肢があり、ビルドコンフィグで切り替えることができる。

  • 名前付きパイプ(FIFO)
  • Unix Domain Socket (デフォルトでは"/tmp/dlt")
  • 共有メモリ(v2.18.0時点ではまだexperimentalな機能とのこと)

ちなみにrsyslog/journaldの構成図は下記がわかりやすかった。
アプリケーションはlogger(1)コマンド、もしくはlibcのsyslog(3)を使って/dev/log経由でrsyslogデーモンにデータを送付する。
journaldがいたらdaemonの標準出力等も収集してくれるらしいが、よく知らない。

rsyslog/journald構成図
(参考:【図解/CentOS7】rsyslogの仕組みと.confの設定例 〜template, property, ruleフィルタの種類〜)

3. 動かしてみる

以降で取り上げる環境およびソースコードのバージョンは以下の通り。

  • OS : Ubuntu 16.04 (x86_64)
  • dlt-daemon: v2.18.0
  • dlt-viewer: v2.18.0

3-1. dlt-daemonのビルドと実行

といっても通常のcmakeプロジェクトなので、README通りに実行すれば良い。

$ git clone --depth=1 --branch=v2.18.0 http://github.com/GENIVI/dlt-daemon.git
$ sudo apt-get install cmake zlib1g-dev libdbus-glib-1-dev
$ mkdir build
$ cd build
$ cmake ..
$ make
$ src/daemon/dlt-daemon

3-2. ログのビューワのビルドと実行

こっちはINSTALL.txtにビルドの仕方が書かれていた。

$ git clone --depth=1 --branch=v2.18.0 http://github.com/GENIVI/dlt-viewer.git
$ sudo apt install qtdeclarative5-dev
$ mkdir build
$ cd build
$ qmake ../BuildDltViewer.pro
$ make
$ release/dlt_viewer

下記手順でDLT daemonからのデータを受信できるようにしておく。
"Config" > "ECU Add" > "IP" > localhost > OK > "Connect All ECU"

3-3. ログ出力サンプルのビルドと実行

$ cd dlt-daemon/examples/example1/
$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./dlt-example1

3-4. ログの確認

下記のように"Hello, world"のログが確認できる。

Screenshot from 2019-01-25 20-11-43.png

4. 大まかなログ出力の流れ

続いて先ほどのサンプルプログラムを例に、DLTログを出力するためのAPIを見ていく。

dlt-daemon/examples/example1/example1.c
#include <stdio.h>      /* for printf() and fprintf() */
#include <stdlib.h>     /* for atoi() and exit() */

#include <dlt.h>

DLT_DECLARE_CONTEXT(con_exa1);

int main()
{
    DLT_REGISTER_APP("EXA1", "First Example");

    DLT_REGISTER_CONTEXT(con_exa1, "CON", "First context");

    DLT_LOG(con_exa1, DLT_LOG_INFO, DLT_STRING("Hello world!"));

    usleep(1000);

    DLT_UNREGISTER_CONTEXT(con_exa1);

    DLT_UNREGISTER_APP();
}

順に内部を見ていく。

4-1. DLT_DECLARE_CONTEXT(con_exa1);

dlt-daemon/include/dlt/dlt_user_macros.h
/**
 * Create an object for a new context.
 * This macro has to be called first for every.
 * @param CONTEXT object containing information about one special logging context
 * @note To avoid the MISRA warning "Null statement is located close to other code or comments"
 *       remove the semicolon when using the macro.
 *       Example: DLT_DECLARE_CONTEXT(hContext)
 */
#define DLT_DECLARE_CONTEXT(CONTEXT) \
    DltContext CONTEXT;

なんてことはない、ただグローバル変数を定義してるだけ。
ここから、一つの実行ファイルにつき、同名のコンテキストは一箇所でだけDLT_DECLARE_CONTEXT()されなければならないことがわかる。
ちなみに他のオブジェクトファイルからこのコンテキストを使いたかったら、DLT_IMPORT_CONTEXT()を使う。

4-2. DLT_REGISTER_APP("EXA1", "First Example");

dlt-daemon/include/dlt/dlt_user_macros.h
/**
 * Register application.
 * @param APPID application id with maximal four characters
 * @param DESCRIPTION ASCII string containing description
 */
#define DLT_REGISTER_APP(APPID, DESCRIPTION) do { \
        (void)dlt_check_library_version(_DLT_PACKAGE_MAJOR_VERSION, _DLT_PACKAGE_MINOR_VERSION); \
        (void)dlt_register_app(APPID, DESCRIPTION); } while (0)

ここでは2つの関数呼び出しを行っている。
ライブラリバージョンの確認(dlt_check_library_version)と、Application IDの登録(dlt_register_app)だ。

ライブラリバージョンの確認では、ヘッダとリンクしているライブラリとの不整合がないかを確認している。
ちなみに不整合があった場合は、DLTログを送る。
本当に問題のある不整合だったら送れない気がするが。。。

ライブラリバージョンの確認をこのタイミングで行うのはなんとなく違和感がある。
普通に考えればライブラリの初期化関数でやるか、コンテキスト生成時に確認すべきだろう。
ただDLTは簡単のためかライブラリの初期化関数はない。また前述の通りDLT_DECLARE_CONTEXT()はただのグローバル変数の定義だからそんなことはできない。
コンストラクタでやる方法もなくはないが、dlt-daemonはテストを覗いてほぼCで書かれている。
そんな中で「まぁApplication IDの登録はだいたい最初に一回やるよねーだからここでも良いよねー」という妥協設計が行われたような気がする。いっそのことDLT_INITって名前の方がすっきりしたのに。

続いて呼び出されるdlt_register_appは、まぁApplication IDの登録するんだろうと思いきや、意外といろいろする。

他のAPIも含めて最初にDLT APIが呼び出された時には、dlt_initが呼び出される。
dlt_initでは、ソケットを開いたり、mutexの初期化をしたり、スレッドを2個起こしたりする。

そう、DLTは内部でスレッドを起こす。
2つのスレッドは、"dlt_receiver"と"dlt_segmented"。
"dlt_reciever"はログレベルなど設定変更をdaemonから受け取るためのスレッド。
"dlt_segmented"はSegmented Network Protocolをサポートするために必要らしい。
DLTではCANなどのネットワークに流れるデータをトレースログとして残す機能があるが、ネットワークデータが長すぎたときのために分割して遅れるようにしているらしい。
おそらくその長いネットワークデータを一生懸命分割して送り続けるスレッドと思われるが、使ったことないからよくわからない。

それらが終わるとやっとApplication IDを登録する。
でもどこに?
DLTライブラリ内で定義しているグローバル変数dlt_userである。
つまり、Application IDは実行ファイルに一つしか登録できない。

4-3. DLT_REGISTER_CONTEXT(con_exa1, "CON", "First context");

/**
 * Register context (with default log level and default trace status)
 * @param CONTEXT object containing information about one special logging context
 * @param CONTEXTID context id with maximal four characters
 * @param DESCRIPTION ASCII string containing description
 */
#define DLT_REGISTER_CONTEXT(CONTEXT, CONTEXTID, DESCRIPTION) do { \
        (void)dlt_register_context(&(CONTEXT), CONTEXTID, DESCRIPTION); } while (0)

dlt_userにContextを登録する。
もうライブラリバージョンのチェックはしない。

4-4. DLT_LOG(con_exa1, DLT_LOG_INFO, DLT_STRING("Hello world!"));

自分の素性(Application ID, Context ID)を登録したら、やっとログを出せる。
DLT_LOGは可変長引数になっている。
第一引数がContext、第二引数がログレベル、第三引数以降がログ出力したいデータ列だ。

Cのprintfのようにフォーマットとデータを渡すのではなく、C++のiostreamのようにデータ列の中に必要だったら文字列を入れる感じだ。
ただ、C++のiostreamと違ってアプリが各データの型を明示的に書かなければならない。
うう、めんどくさい。。。。

DLT_LOG(mycontext1,DLT_LOG_INFO, DLT_CSTRING("Frame info: ,"),
                                 DLT_CSTRING("total="),DLT_UINT16(1000),DLT_CSTRING(",")
                                 DLT_CSTRING("sync="),DLT_UINT8(0),DLT_CSTRING(",")
                                 DLT_CSTRING("reem="),DLT_UINT8(0),DLT_CSTRING(",")
                                 DLT_CSTRING("valid="),DLT_UINT16(100),DLT_CSTRING(",")
                                 DLT_CSTRING("urgent="),DLT_UINT8(1))

なんでこんなことになっているのか?
それは、DLTでは転送効率を高めるため、基本もとのバイナリのままログを送信しているからだ。
バイナリをダンプしたかったらバイナリのまま送って、ビューワでデコードするという設計だ。

だからたまにsprintf等でバイナリを一生懸命hexダンプしたあとDLT_LOG呼び出してるコードを見かけるが、あんなのは愚の骨頂だ。
1byteで転送できるデータを3byteにするために、CPUとメモリを無駄遣いしている。
もちろん他の用途のためにテキストにしなければならないなら仕方ないが、少なくともDLTのためには不要だ。

    char binary[] = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7};

    char hex[1024] = {0};
    char *ptr = hex;

    ptr += sprintf (ptr, "binary:");
    for (int i = 0; i < sizeof(binary)/sizeof(binary[0]); i++)
      {
        ptr += sprintf (ptr, " %02x", binary[i]);
      }

    DLT_LOG(con_exa1, DLT_LOG_INFO,
            DLT_STRING(hex));

下記のように、バイナリ列はそのまま送ればいい。

    char binary[] = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7};

    DLT_LOG(con_exa1, DLT_LOG_INFO,
            DLT_CSTRING("binary: "),
            DLT_RAW(binary, sizeof(binary)));

なお、どのようなログをどのように出すべきか、についてはガイドラインがあるので目を通して置いたほうが良い。
上記のことは書いてないが色々書いてくれている。
ガイドラインの一番最初の項目が"Think first"だったのはウケた。
苦労してるんだなぁ。誰が書いたかしらんけど。

話をもとに戻すと、DLT_LOGはバイナリで転送するために型情報を付与しなければならず、呼び出しが面倒だ。
ただこれはGENIVIも課題に思っているらしくC++11が使える環境で、WITH_DLT_CXX11_EXTコンパイルオプションを有効にしたら、下記のように呼び出せる。

DLT_LOG_CXX(ctx, DLT_LOG_WARN, 1.0, 65);
DLT_LOG_FCN_CXX(ctx, DLT_LOG_WARN, "Test String", 145, 3.141);

C++11の可変引数テンプレートを使って実装されている。
logToDlt関数をオーバーロードすることで、独自構造体の自動出力も可能だ。

ちなみにDLT_LOG_CXXDLT_LOG_FCN_CXXとの違いはFCNの方は__PRETTY_FUNC__をプレフィックスにつける。だった。
どんどん話が脱線するけど、外部公開インターフェース名にCXXってつけるはあまり良くないと思う。インターフェースと実装がこれ以上なく癒着してしまっている。このAPIの機能自体はとても良いだけに残念。

さて、DLT_LOGの内部はどうなっているか。

/**
 * Send log message with variable list of messages (intended for verbose mode)
 * @param CONTEXT object containing information about one special logging context
 * @param LOGLEVEL the log level of the log message
 * @param ARGS variable list of arguments
 * @note To avoid the MISRA warning "The comma operator has been used outside a for statement"
 *       use a semicolon instead of a comma to separate the ARGS.
 *       Example: DLT_LOG(hContext, DLT_LOG_INFO, DLT_STRING("Hello world"); DLT_INT(123));
 */
#ifdef _MSC_VER
/* DLT_LOG is not supported by MS Visual C++ */
/* use function interface instead            */
#else
#   define DLT_LOG(CONTEXT, LOGLEVEL, ARGS ...) \
    do { \
        DltContextData log_local; \
        int dlt_local; \
        dlt_local = dlt_user_log_write_start(&CONTEXT, &log_local, LOGLEVEL); \
        if (dlt_local == DLT_RETURN_TRUE) \
        { \
            ARGS; \
            (void)dlt_user_log_write_finish(&log_local); \
        } \
    } while (0)
#endif

log_localなるローカル変数を定義して、それを使ってwrite_start()->ARGS->write_finish()としている。
ARGSは型情報付きのデータを送信用バッファにコピーしている。
では送信用バッファはいつ確保されるか?
これは予想通りdlt_log_write_startでcallocしていた。
デフォルトDLT_USER_BUF_MAX_SIZE(1390byte)のバッファがここで確保される。

4-5. DLT_UNREGISTER_CONTEXT(con_exa1);

ここまできたらもうあとは消化試合みたいなものだ。

dlt-daemon/include/dlt/dlt_user_macros.h
/**
 * Unregister context.
 * @param CONTEXT object containing information about one special logging context
 */
#define DLT_UNREGISTER_CONTEXT(CONTEXT) do { \
        (void)dlt_unregister_context(&(CONTEXT)); } while (0)

名前の通りContextを使えないようにしている。

4-6. DLT_UNREGISTER_APP()

dlt-daemon/include/dlt/dlt_user_macros.h
/**
 * Unregister application.
 */
#define DLT_UNREGISTER_APP() do { \
        (void)dlt_unregister_app(); } while (0)

こちらも同じくApplicationの登録解除している。

あれ?threadの回収は?
探してみたらatexitハンドラで回収してた。
もうそれならOSに任せたら良くない?

というかregister I/F作るときにunregister I/Fを用意するのは良い心がけだと思うけど、DLTで一旦登録解除してまた使うユースケースが想像できないな。。。

5. DLTログの構造

続いては、出力したログを見るときのためにDLTログのバイナリフォーマットを確認していく。

ちょっと意外なことにファイルヘッダはない。
メッセージごとにヘッダとペイロードがあり、メッセージの数だけそれが繰り返される。
例えば複数のDLTファイルを単純にcatしたら、それはれっきとしたDLTファイルだ。
ペイロードは、型情報→データが繰り返し入っている。
Screenshot from 2019-02-05 22-22-18.png

5-1. メッセージヘッダの構造

メッセージヘッダは4つの構造体からなる。
この内ふたつは必須だが、残りの2つはオプションで、動作モードによってあったりなかったりする。
このあたりの構造を理解するためには、dlt_file_read_header()の実装が一番わかりやすい。

  • DltStorageHeader (16byte)
    • マジックワード"DLT0x01" (4byte)
    • unix time (8byte)
    • ECU ID (4byte)
  • DltStandardHeader (4byte)
    • ヘッダに関する情報のビットフィールド (1byte)
      • ここを見ることで後続のオプションヘッダフィールドの有無がわかる
    • メッセージID (1byte)
    • ペイロード長 (2byte)
  • DltStandardHeaderExtra (12byte)
    • ECU ID (4byte)
    • Session ID (4byte)
    • システム起動からのタイムスタンプ(0.1ms単位)(4byte)
  • DltExtendedHeader (10byte)
    • メッセージタイプに関する情報のビットフィールド(1byte)
    • ペイロードに含まれるデータの数(1byte)
    • Applicatiopn ID(4byte)
    • Context ID (4byte)

片方がオプションとはいえ、ECU IDが二回あるのは謎。

dlt-daemon/include/dlt/dlt_common.h
/**
 * The structure of the DLT file storage header. This header is used before each stored DLT message.
 */
typedef struct
{
    char pattern[DLT_ID_SIZE]; /**< This pattern should be DLT0x01 */
    uint32_t seconds;          /**< seconds since 1.1.1970 */
    int32_t microseconds;      /**< Microseconds */
    char ecu[DLT_ID_SIZE];     /**< The ECU id is added, if it is not already in the DLT message itself */
} PACKED DltStorageHeader;

/**
 * The structure of the DLT standard header. This header is used in each DLT message.
 */
typedef struct
{
    uint8_t htyp;           /**< This parameter contains several informations, see definitions below */
    uint8_t mcnt;           /**< The message counter is increased with each sent DLT message */
    uint16_t len;           /**< Length of the complete message, without storage header */
} PACKED DltStandardHeader;

/**
 * The structure of the DLT extra header parameters. Each parameter is sent only if enabled in htyp.
 */
typedef struct
{
    char ecu[DLT_ID_SIZE];       /**< ECU id */
    uint32_t seid;               /**< Session number */
    uint32_t tmsp;               /**< Timestamp since system start in 0.1 milliseconds */
} PACKED DltStandardHeaderExtra;

/**
 * The structure of the DLT extended header. This header is only sent if enabled in htyp parameter.
 */
typedef struct
{
    uint8_t msin;              /**< messsage info */
    uint8_t noar;              /**< number of arguments */
    char apid[DLT_ID_SIZE];    /**< application id */
    char ctid[DLT_ID_SIZE];    /**< context id */
} PACKED DltExtendedHeader;

ビットフィールドは下記にマスクが定義されている。

dlt-daemon/include/dlt/dlt_protocol.h
#define DLT_HTYP_UEH  0x01 /**< use extended header */
#define DLT_HTYP_MSBF 0x02 /**< MSB first */
#define DLT_HTYP_WEID 0x04 /**< with ECU ID */
#define DLT_HTYP_WSID 0x08 /**< with session ID */
#define DLT_HTYP_WTMS 0x10 /**< with timestamp */
#define DLT_HTYP_VERS 0xe0 /**< version number, 0x1 */
dlt-daemon/include/dlt/dlt_protocol.h
#define DLT_MSIN_VERB 0x01 /**< verbose */
#define DLT_MSIN_MSTP 0x0e /**< message type */
#define DLT_MSIN_MTIN 0xf0 /**< message type info */

5-2. メッセージペイロードの構造

前述のとおり、メッセージペイロードは、型情報とデータが交互に格納されている。
型情報にはまず4byteの種別データが格納されており、そこで「文字列」や「int32」などの型が判明する。
その型が文字列など可変長な場合はさらに2byteのデータ長、そしてその長さのデータが続く。
固定長の型の場合は、種別データの直後に実際のデータが続く。

このあたりの構造はdlt_message_argument_print()の実装を見るとわかりやすい。
実は型情報のビット値によっては、さらに付加情報を入れる余地もあるが、後述のdlt-converterでも無視していたので省略。

/*                                                                                                                                                                                                          
 * Definitions of types of arguments in payload.                                                                                                                                                            
 */
#define DLT_TYPE_INFO_TYLE 0x0000000f /**< Length of standard data: 1 = 8bit, 2 = 16bit, 3 = 32 bit, 4 = 64 bit, 5 = 128 bit */
#define DLT_TYPE_INFO_BOOL 0x00000010 /**< Boolean data */
#define DLT_TYPE_INFO_SINT 0x00000020 /**< Signed integer data */
#define DLT_TYPE_INFO_UINT 0x00000040 /**< Unsigned integer data */
#define DLT_TYPE_INFO_FLOA 0x00000080 /**< Float data */
#define DLT_TYPE_INFO_ARAY 0x00000100 /**< Array of standard types */
#define DLT_TYPE_INFO_STRG 0x00000200 /**< String */
#define DLT_TYPE_INFO_RAWD 0x00000400 /**< Raw data */
#define DLT_TYPE_INFO_VARI 0x00000800 /**< Set, if additional information to a variable is available */
#define DLT_TYPE_INFO_FIXP 0x00001000 /**< Set, if quantization and offset are added */
#define DLT_TYPE_INFO_TRAI 0x00002000 /**< Set, if additional trace information is added */
#define DLT_TYPE_INFO_STRU 0x00004000 /**< Struct */
#define DLT_TYPE_INFO_SCOD 0x00038000 /**< coding of the type string: 0 = ASCII, 1 = UTF-8 */

#define DLT_TYLE_8BIT      0x00000001
#define DLT_TYLE_16BIT     0x00000002                                                                                                                                                                       
#define DLT_TYLE_32BIT     0x00000003
#define DLT_TYLE_64BIT     0x00000004
#define DLT_TYLE_128BIT    0x00000005

#define DLT_SCOD_ASCII      0x00000000
#define DLT_SCOD_UTF8       0x00008000
#define DLT_SCOD_HEX        0x00010000
#define DLT_SCOD_BIN        0x00018000

6. ログの見方

さて、ログは解析時のヒントとして出力するので、いかに効率よくログから必要な情報を取り出せるか、が肝になる。
DLTではログのフォーマットがバイナリなので、見るためにはその構造をパースするツールが必要だ。
前述のdlt-viewerを含め、オフラインでDLTログを解析するためには実は3つの方法がある。
(もし他にもっといい方法があればぜひ教えてください)

6-1. dlt-viewer(GUIモード)

さきほど"Hello, world"を確認した時に使ったビューワ。
一番使われていると思われる。
実際、実機を動かしつつログを確認するときなどはこれ一択と思われる。

フィルタ機能やエクスポート機能等、いろいろ盛り込まれているが、分割表示ができなかったりフォントサイズの設定が弱かったりと、微妙に解析中に思考が中断されることがある。

6-2. dlt-viewer(batchモード)

上記ツールにはいくつか起動オプションが用意されていて、batchモードで起動できる。

$ ./dlt_viewer -h
Usage: dlt_viewer [OPTIONS]
Options:
 -h             Print usage
 -p projectfile         Loading project file on startup (must end with .dlp)
 -l logfile     Loading logfile on startup (must end with .dlt)
 -f filterfile  Loading filterfile on startup (must end with .dlf)
 -c logfile textfile    Convert logfile file to textfile (logfile must end with .dlt)
Conversion will be done in UTF8 instead of Ascii
 -e "plugin|command|param1|..|param<n>"         Execute a plugin command with <n> parameters.
 -s or --silent         Enable silent mode without warning message boxes.

変換結果は下記のような感じ。
各メッセージが行単位で出力されるので、UNIX系コマンドラインツールとの相性がとても良い。

$ ./dlt_viewer -s -c helloworld.dlt helloworld.txt
$ cat helloworld.txt
0 2019/01/25 20:10:35.151695 240591.0105 0 ECU1 EXA1 CON 18747 log info verbose 1 Hello world!

また、未リリースだが、dlt-viewerのmasterのHEADではcsvフォーマットでの出力も対応している。
コマンドラインから使いたいという要望が増えているのかな?
計画中のv2.19.0では入るだろうが、csvが良い人はdlt-viewerだけmasterを使えばいい。

 -csv Conversion will be done in CSV format

6-3. dlt-convert(CUI)

dlt-daemonリポジトリの中にひっそりと入っている。

$ cd dlt-daemon/build/src/console
$ ./dlt-convert -h
Usage: dlt-convert [options] [commands] file1 [file2]
Read DLT files, print DLT messages as ASCII and store the messages again.
Use filters to filter DLT messages.
Use Ranges and Output file to cut DLT files.
Use two files and Output file to join DLT files.
DLT Package Version: 2.18.0 STABLE, Package Revision: v2.18.0, build on Jan 25 2019 19:08:27
-SYSTEMD -SYSTEMD_WATCHDOG -TEST -SHM

Commands:
  -h            Usage
  -a            Print DLT file; payload as ASCII
  -x            Print DLT file; payload as hex
  -m            Print DLT file; payload as hex and ASCII
  -s            Print DLT file; only headers
  -o filename   Output messages in new DLT file
Options:
  -v            Verbose mode
  -c            Count number of messages
  -f filename   Enable filtering of messages
  -b number     First messages to be handled
  -e number     Last message to be handled
  -w            Follow dlt file while file is increasing

こちらもテキストへの変換ができる。
こちらも各メッセージが行単位で出力されるので、UNIX系コマンドラインツールとの相性がとても良い。
出力がデフォルト標準出力というところも、良い。

$ ./dlt-convert -a helloworld.dlt 
0 2019/01/25 20:10:35.151695 2405910105 000 ECU1 EXA1 CON- log info V 1 [Hello world!]

さらに機能を削っているため、依存関係も少なく軽量なので、見つけた時は「これだ!」と思った。

が。。

大変残念な欠点があった。
1つのログの中に改行がある場合、とたんに使いづらくなってしまうのだ。

具体的に出力結果を見てもらったほうが早いと思う。

複数行ログのdlt_viewerの出力
$ ./dlt_viewer -s -c multiline.dlt multiline.dlt
$ cat multiline.dlt 
0 2019/01/25 20:39:39.349801 242335.2082 0 ECU1 EXA1 CON 19788 log info verbose 1 1st line 2nd line 3rd line
複数行ログのdlt-convertの出力
$ ./dlt-convert -a multiline.dlt 
0 2019/01/25 20:39:39.349801 2423352082 000 ECU1 EXA1 CON- log info V 1 [1st line
2nd line
3rd line]

こうなってしまうとパースしづらい。。。
もちろん[]で囲んでくれているから、対処しようはあるのだが、微妙な感じは拭えない。

意外と行志向なDLTでは、一つのメッセージ内に複数行の文字列を入れられることをあまり想定していないと思われる。
とはいえ、シリアル出力用のログと共有していたりして、実際にはそのようなケースも見受けられる。
実はこの問題は結構根が深くて、dlt-viewerのGUIモードで表示しているときにも見えなくなったりする。
Screenshot from 2019-01-25 21-23-51.png

せっかく種々のコストを払って出したログ、ビューワで見えてないなんてことは避けたい。
複数行のログを無くせれば良いんだろうが、実際問題ログを解析するときにそんなことを言っても仕方がない。
そういう意味でもオフラインデバッグのときは、dlt_viewerをbatchモードで動かすのが一番お気に入りだ。

おまけ

今回DLTの調査をしていて、WITH_DLT_FATAL_LOG_TRAPというcmakeオプションを知った。
ヘルプによると、 "Set to ON to enable DLT_LOG_FATAL trap (trigger segv inside dlt-user library)"とのこと。
trapして何してくれるのかなーと実装を確認してみると。。。

dlt-daemon/src/lib/dlt_user.c
#ifdef DLT_FATAL_LOG_RESET_ENABLE
#   define DLT_LOG_FATAL_RESET_TRAP(LOGLEVEL) \
    do {                                   \
        if (LOGLEVEL == DLT_LOG_FATAL) {   \
            int *p = NULL;                 \
            *p = 0;                        \
        }                                  \
    } while (0)
#else /* DLT_FATAL_LOG_RESET_ENABLE */
#   define DLT_LOG_FATAL_RESET_TRAP(LOGLEVEL)
#endif /* DLT_FATAL_LOG_RESET_ENABLE */

NULLポインタを参照しても死ねないシステムがあるとも知らないで。。。┐(´д`)┌ヤレヤレ

おわりに

なんか思ってた以上に長文になってしまった。
DLTについての日本語の情報は皆無なので、誰かの役に立てばいいな。

ただ、ログのプラットフォームはシステム構成の影響を受けやすく、都合に合わせて独自拡張を加えるケースも結構あるので、そこは気をつけてください。
DLTデーモンが動いているのが別システムだからUnixDomainSocketでもFIFOでもない通信を使ったり、動的メモリ確保がダメだからcallocではなくスタックや自前アロケータから取ってきたりなど。

逆に詳しい方はぜひコメント・ご指摘ください。

参考

GENIVI Diagnostic Log and Trace
AUTOSAR DLT 4.3.1 specification
github dlt-daemon
github dlt-viewer

25
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
16