vague memory

うろ覚えを無くしていこうともがき苦しむ人の備忘録

Datadog Logs 特殊文字をキーに含むJSONを処理する


経緯

Datadog の Log Management で Log Facets を作成する際に以下のエラーが発生しました。

Something wrong happened while creating the facet:
The Facet path must contain only letters, digits, or characters -_@$.
Each segments should be separated by '.'.

対象のログはJSON形式で、 facet を作成しようとした属性は記号を含んだ物でした。
例として、以下のようなログで "Size (KB)" から facet を作成しようとすると発生します。 エラーメッセージの通りですが、特殊文字(この場合、スペースと"()") を含んでいるため facet が作成できない状態です。

{
    "title": "JSON Example",
    "ID": "0000",
    "Size (KB)": 100
}

特殊文字と言っても一概には言えず、文字種により許可・非許可のパターンがありましたので、回避した方法と諦めた点をまとめます。

前提

JSON形式

テキスト形式のログと扱いが異なり、 JSON 形式のログは Datadog により自動的に解析されます。

  • Parsing
    • Datadog automatically parses JSON-formatted logs.

  • Preprocessing
    • JSON log preprocessing comes with a default configuration that works for standard log forwarders.

"Logs" -> "Configuration" の Pipeline 設定画面の一番先頭にある Preprocessing for JSON logs が該当します。

Pipelineの先頭で強制的に処理されます。 Remap する属性名の追加・削除を行うことはできますが、処理自体の無効化はできません。

今回この前処理はデフォルトの設定のまま検証しています。
(Pipelineを設定するに当たり、この動作自体がパースの障壁になることもありますが、今回は割愛します。)

特殊文字

ログ検索時には記号はバックスラッシュ(\)を前に置いて、エスケープする必要があります。 スペースはエスケープでなく、ワイルドカード(?)で置き換えて処理させます。

  • Log Search Syntax
    • The following characters are considered special: + - = && || > < ! ( ) { } [ ] ^ " “ ” ~ * ? : \, and / require escaping with the \ character.

    • To match a single special character or space, use the ? wildcard.

上記はあくまでログ検索時の特殊文字の扱いであり、ログのパースや facet 作成時は文字種により動作が変わります。

検証に使用したログ

属性に特殊文字を含むJSON形式のログを使用します。 各属性で facet および measure が作成でき、グラフ化できる状態になる事を目的とします。

{
    "ddsource": "custom",
    "hostname": "i-012345678",
    "message": "2022-08-13T13:44:27 INFO json format log parse test",
    "service": "custom",
    "key_normal": 719,
    "key space": 24,
    "key:colon": 764,
    "key.dot": 518,
    "key,comma": 835,
    "key(parentheses)": 502,
    "key[brackets]": 14,
    "key{curly}": 1,
    "key<angle>": 11,
    "key日本語": 811
}

Datadog に取り込まれると以下のように JSON の各キーが属性としてパースされた状態になります。
この時点では特殊文字の有無での違いはありません。

回避策

本題です。回避策として利用できる手法を順に記載していきます。

(a) 特に何もせず

記号を含んでいるからといって全てが制限に引っかかるわけではなく、そのまま利用できる文字種が存在します。

特に制限の無い文字種では、filter や facet の作成が問題無く行えます。 ドット(.) の場合は、JSONの階層扱いとなるため1階層下となります。

弊害(a-1): 検索する際に filter の自動入力が効かない

Filter by @xxx から作成した場合に Tag 扱いとなる文字種があります。 作成したいのは属性値であり、Tagは作成していないのでそのままでは検索に引っかかりません。

自動入力での filter が行えないので、手動で filter を記述する必要があります。

特殊文字はバックスラッシュでのエスケープが必要です。エスケープしないと以下のエラーとなります。

Invalid search query. Try escaping these special characters with the "\" character:
+ - = && || > < ! ( ) { } [ ] " “ ” * ? : \

弊害(a-2): 検索する際に filter が効かない

スペースを含む要素の検索が行なえません。 バックスラッシュのエスケープは効かず、ワイルドカードはエラーとなりました。

Oops, an error has occurred. Please retry or reload the page.

(b) ログ出力側を修正する

身も蓋もないですが、JSONキーに特殊文字を含まないようログ出力側を修正できるのであれば、それが最善策だと思います。
自分たちで開発したプログラムなどであれば良いですが、利用しているソフトウェアやサービスのログが対象だと中々簡単には行きません。

(c) Remap する

非許可文字を含む属性の facet を作成しようとすると、冒頭のエラー出力となります。

Pipeline を作成、 Remapper を使用して、対象の属性を別の名称に置き換えます。

(d) String Builder で抽出後、 Remap する

Remap 設定を追加しようとすると、属性の検索が行えずエラーが発生し、 Remap を行えない文字種が存在します。

Invalid source(s): '...'

また、カンマ(,) に関しては、複数属性指定の区切りとなるため指定できません。

これらの場合は、属性を指定する Processor は使用できません。
そこで、 ブロック構文を使用できる String builder processor で、対象の属性を別の名称に置き換えます。

String builder の名の通り string 形式で生成されるので、 measure として使用したい場合は、更に Remapper により数値型に変換します。

(e) 諦める

String Builder で抽出不可な文字種が存在します。 今回試した範囲では、 []{} がエラーとなります。また、バックスペースでのエスケープは効かないようです。

Invalid template: %{...}

こちらについては用意されている Processor で処理できる案は出ませんでした。

(f) 諦めきれない...

Grok parser で自前で解析することで抽出は可能です。

しかし、JSON形式のログであるが故、前処理が走るため、キーを直接 Grok でパースすることができません。 JSON以外の形式にする、message 属性にJSON文字列を出力するなど、ログ出力側での変更が必要になってしまうかと思います。

message 属性にJSON文字列 を出力する例

message の内容を Grok parser で処理、必要な属性を抽出し、Remapper により数値型に変換します。

  • ログ例
[
  {
    "ddsource": "custom",
    "hostname": "i-012345678",
    "message": "[{\"ddsource\":\"custom\",\"hostname\":\"i-012345678\",\"message\":\" INFO json format log parse test\",\"service\":\"custom\",\"key_normal\":719,\"key space\":24,\"key:colon\":764,\"key.dot\":518,\"key,comma\":835,\"key(parentheses)\":502,\"key[brackets]\":14,\"key{curly}\":811,\"key<angle>\":556,\"key日本語\":652}]",
    "service": "custom"
  }
]
  • Define parsing rules
rule_brackets_and_curly ^.*key\[brackets\][^0-9]*%{regex("[0-9]*"):key_brackets_grok}.*key\{curly\}[^0-9]*%{regex("[0-9]*"):key_curly_grok}.*$
rule_curly_and_brackets ^.*key\{curly\}[^0-9]*%{regex("[0-9]*"):key_curly_grok}.*key\[brackets\][^0-9]*%{regex("[0-9]*"):key_brackets_grok}.*$
  • Grok 処理結果 を Remapper により数値型に変換
{
  "key_curly_grok": "811",
  "key_brackets_grok": "14"
}

結果

そのまま利用可能な属性と、変換した属性 に対し、 facet(measure) を作成します。

facet が作成され、数値型にした事により measure としても利用可能となりました。

文字種パターン

  • (*1): tag 扱いとなるため自動入力不可
  • (*2): processorで処理せずとも利用可
文字種 変換前Key (a)filter (a)facet(measure) (c)remap (d)string builder + remap
アンダースコア key_normal △(*2) △(*2)
半角スペース key space △(*2)
コロン key:colon △(*1) △(*2)
ドット(ピリオド) key.dot △(*2) △(*2)
カンマ key,comma △(*1)
丸括弧(小括弧) key(parentheses) △(*1)
角括弧(大括弧) key[brackets] △(*1)
波括弧(中括弧) key{curly} △(*1)
山括弧 key<angle> △(*1)
マルチバイト key日本語 △(*1)