MessagePackを解析するためのライブラリ/ツール msgpack-microscope

概要

Golangで、MessagePackの解析用ライブラリとそれを使用したサンプルツールを作成しました。

github.com

ビルド済みバイナリはReleases から取得できます。

MessagePackとは

JSONのようなデータ構造を、バイナリ形式で表現したデータ形式です。 大抵の場合、JSONよりもデータサイズを減らすことができるようですが、バイナリ列のために可読性が下がっています。 公式サイトの例ですと、

{"compact":true,"schema":0}

という27byteのJSONを、

82 a7 63 6f 6d 70 61 63 74 c3 a6 73 63 68 65 6d 61 00

という18byteのバイナリに変換する例が示されています。

特徴

基本的には、インスパイア元のmsgpack-inspectとほぼ同じですが、下記が異なっています。 *1

サンプルツール msgpack2json

github.com

その名のとおり、MessagePackをJSONに変換するためのサンプルツール。 出力フォーマットは、同じ機能のツールであるmsgpack-inspectの形式をほぼ流用しています。

下記のいずれかからMessagePackのバイト列を受け取り、標準出力に解析結果を出力します。

  • 標準入力(他コマンドからパイプで繋ぐケース)
  • ファイル
  • HTTP POST

それぞれの動作サンプルについて以下に書きます。なお、いずれのケースについても下記JSON相当のMessagePackを解析した結果になります。

{"compact":true,"schema":0}

標準入力から読む

パイプでリダイレクトするケースを想定した使い方です。

$ printf "\x82\xa7compact\xc3\xa6schema\x00" | ./msgpack2json

出力結果は下記。

{"format":"fixmap", "header":"0x82", "length":2, "raw":"0x82a7636f6d70616374c3a6736368656d6100", "value":
    [
        {"key":
            {"format":"fixstr", "header":"0xa7", "raw":"0xa7636f6d70616374", "value":"compact"},
         "value":
            {"format":"true", "header":"0xc3", "raw":"0xc3", "value":true}
        },
        {"key":
            {"format":"fixstr", "header":"0xa6", "raw":"0xa6736368656d61", "value":"schema"},
         "value":
            {"format":"positive fixint", "header":"0x00", "raw":"0x00", "value":0}
        }
    ]
}

ファイルを読む

バイナリファイルを読むケースです。

$ printf "\x82\xa7compact\xc3\xa6schema\x00" > b.msgp
$ ./msgpack2json b.msgp 

出力結果は標準入力と同様なので割愛。

HTTP POST

簡単なhttp server機能も備えています。-sオプションでserver動作をし、デフォルトで8080ポートへのPOSTを待ちます。 なお、ポート番号については、-p オプションで変更することができます。

$ printf "\x82\xa7compact\xc3\xa6schema\x00" > b.msgp
$ ./msgpack2json -s &

たとえば、curlで下記のように投げることで出力結果を得ることができます。

$ curl -sS localhost:8080 -X POST --data-binary "@b.msgp"

Fluent-bitのout_httpプラグインなど、HTTP POST形式でMessagePackを投げるソフトウェアのデバッグなどにも使うことができます。

その他オプションについて

  • -e : Fluentd の EventTime Ext Format を解釈可能とする。

Fluentdでは、Ext Typeを使用して、独自のタイムスタンプ形式を定義しています。このオプションを使うことで、そのタイムスタンプ形式を解釈できるようになります。

$ printf "\xd7\x00\x5c\xda\x05\x00\x00\x00\x00\x00"| ./msgpack2json -e
{"format":"event time", "header":"0xd7", "type":0, "raw":"0xd7005cda050000000000", "value":"2019-05-14 09:00:00 +0900 JST"}
  • -r: プレーンなJSON形式で出力する

解析用でない、シンプルな形式で出力します。

$ printf "\x82\xa7compact\xc3\xa6schema\x00"|./msgpack2json -r
{"compact":true,"schema":0}

ライブラリ

基本的には、Decode関数にMessagePackの[]byteを与えることで、MPObjectに変換し、そのMPObjectの中身をゴニョゴニョして解析する形になります。

type MPObject struct {
    FirstByte byte        /* どのフォーマットかを示すbyte */
    FormatName  string  /* フォーマット名称 */
    ExtType   int8       /* Ext フォーマットの場合、どのTypeかを示す。それ以外のフォーマットの場合は0。*/
    Length    uint32  /* length を備えるフォーマットの場合のみ。*/
    DataStr   string  /* 各フォーマットのデータを文字列にしたもの */
    Raw       []byte /* FirstByteからデータ本体までのbyteスライス。ArrayやMapの場合は、その要素も含む。 */
    Child     []*MPObject /* ArrayやMap フォーマットの場合、その各要素が保存される。*/
}

import

import "github.com/nokute78/msgpack-microscope/pkg/msgpack"

サンプルコード

簡単なサンプルコードは下記です。

package main

import (
    "bytes"
    "fmt"
    "github.com/nokute78/msgpack-microscope/pkg/msgpack"
    "os"
)

func showMsgPack(obj *msgpack.MPObject) {
    switch {
    case msgpack.IsMap(obj.FirstByte) || msgpack.IsArray(obj.FirstByte):
        for _, v := range obj.Child {
            showMsgPack(v)
        }
    default:
        fmt.Fprintf(os.Stdout, "%s\n", obj)
    }
}

func main() {
    /* {"compact":true,"schema":0} in JSON */
    msgp := []byte{0x82, 0xa7, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0xc3, 0xa6, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x00}

    ret, err := msgpack.Decode(bytes.NewBuffer(msgp))
    if err != nil {
        os.Exit(-1)
    }
    showMsgPack(ret)
}

出力結果は、下記のようになります。

fixstr(0xa7): val="compact"
true(0xc3): val=true
fixstr(0xa6): val="schema"
positive fixint(0x00): val=0

独自のExt Typeを定義する

MessagePackには、Ext Typeを用いることでユーザーが独自フォーマットを定義することができます。 例としては、FluentdのEventTimeがあります。

本ライブラリでは、下記のstruct埋め、RegisterExt関数に食わせることで、ユーザー定義のフォーマットを追加することができます。

type ExtFormat struct {
    FirstByte  byte  /* どのフォーマットかを示すbyte。0xd4-0xd8, 0xc7-0xc9のいずれか。 */
    ExtType    int8 /* 種別を示す。正の整数。負の値は予約されているため、通常は使わない。*/
    TypeName   string /* Type 名称 */
    DecodeFunc func([]byte) string /* []]byte列を変換するための関数 */
}

FluentdのEventTimeの場合の例は下記のようになります。

func extEventTimeV1(b []byte) string {
    if len(b) != 8 {
        return ""
    }
    var sec int32
    if binary.Read(bytes.NewReader(b[:4]), binary.BigEndian, &sec) != nil {
        return ""
    }
    var nsec int32
    if binary.Read(bytes.NewReader(b[4:]), binary.BigEndian, &nsec) != nil {
        return ""
    }
    return fmt.Sprintf("%v", time.Unix(int64(sec), int64(nsec)))
}

// RegisterFluentdEventTime registers Fluentd ext timestamp format.
// https://github.com/fluent/fluentd/wiki/Forward-Protocol-Specification-v1 */
func RegisterFluentdEventTime() {
    msgpack.RegisterExt(&msgpack.ExtFormat{FirstByte: msgpack.FixExt8Format, ExtType: 0, TypeName: "event time", DecodeFunc: extEventTimeV1})
    msgpack.RegisterExt(&msgpack.ExtFormat{FirstByte: msgpack.Ext8Format, ExtType: 0, TypeName: "event time", DecodeFunc: extEventTimeV1})
}

*1:ext typeの独自定義については、msgpack-inspectもrubyファイルの追加読み込みで対応可能。

*2:但し、明示的に有効化する必要あり。

*3:Length と異なるサイズの要素を含む配列など