---
title: OpenTelemetry CollectorでSyslogを受信するメモ
summary: この記事はOpenTelemetry CollectorでtcplogreceiverとGrokでSyslogを受信・解析し、OTLPへエクスポートする設定とDocker Compose例を解説します。
tags: ["syslog", "OpenTelemetry", "OpenTelemetry Collector", "Logging", "Lognroll"]
categories: ["Observability", "OpenTelemetry", "OpenTelemetry Collector"]
date: 2026-01-07T02:24:38Z
updated: 2026-01-07T02:53:34Z
---

既存システムからのログの連携がSyslog形式しかサポートされていない場合に、Syslogサーバーの用意が必要になります。
どのSyslogサーバーを使うか困ることが多いので、普段使っているOpenTelemetry CollectorでSyslogを受信する方法をメモしておきます。

OpenTelemetry Collector Contribには[syslogreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/syslogreceiver)があります。
これでもいいのですが、attributesのパースが好みではなかったので、[tcplogreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/tcplogreceiver)と[`ExtractGrokPatterns`](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/ottl/ottlfuncs#extractgrokpatterns)関数で自前でパースする方法を紹介します。かつてLogstashでGrokパターンを使ってSyslogをパースしていた経験があり、それを移植しました。

次のバージョンで動作確認しています。

```bash
$ otelcol-contrib --version
otelcol-contrib version 0.142.0
```

以下の例ではDocker Composeを使用しますが、`otelcol-contrib`のバイナリは[こちら](https://github.com/open-telemetry/opentelemetry-collector-releases/releases)からダウンロードできます。
また、ログの保存先としてOTLPサポートの軽量ログストア[Lognroll](https://github.com/categolj/lognroll)を使用していますが、他のログストアに変更しても構いません。`lognroll`のバイナリは[こちら](https://github.com/categolj/lognroll/releases/tag/tip)からダウンロードできます。

**目次**
<!-- toc -->

### OpenTelemetry Collectorの設定例

まずは`config.yaml`に以下の内容を記述します。

```yaml
receivers:
  tcplog/syslog:
    listen_address: 0.0.0.0:5514
processors:
  transform/syslog:
    error_mode: ignore
    log_statements:
    #! https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/transformprocessor#basic-config
    - context: log
      statements:
      - merge_maps(log.attributes, ExtractGrokPatterns(log.body, "(?:%{INT:syslog6587.msglen} )?<%{POSINT:syslog.pri}>(%{SPACE})?(?:%{NONNEGINT:syslog5424.ver} )?(?:%{SYSLOGTIMESTAMP:syslog.timestamp}|%{TIMESTAMP_ISO8601:syslog.timestamp}) %{DATA:syslog.hostname} %{DATA:syslog.program}(?:\\[%{POSINT:syslog.pid}\\])?(:)? %{GREEDYDATA:syslog.message}", true), "upsert") where IsString(log.body)
      - 'merge_maps(log.attributes, ExtractGrokPatterns(log.attributes["syslog.message"], "(?:%{DATA:syslog.procid}|\\-) (?:%{DATA:syslog.msgid}|\\-)? %{GREEDYDATA:syslog.message}", true), "upsert") where log.attributes["syslog5424.ver"] == "1"'
      - merge_maps(log.attributes, ExtractGrokPatterns(log.attributes["syslog.message"], "(?P<syslog_sd>\\[[^\\]\\\\]*@[^\\]\\\\]*(?:\\\\.[^\\]\\\\]*)*\\])(?P<syslog_meta>\\[meta[^\\]]*\\])?\\s+%{GREEDYDATA:syslog.message}"), "upsert")
      - 'merge_maps(log.attributes, ExtractGrokPatterns(log.attributes["syslog_sd"], "\\[%{DATA:syslog.sd_id} (?<syslog_sd_params_raw>([^\\]\\\\]|\\\\.)+)\\]", true), "upsert") where IsString(log.attributes["syslog_sd"])'
      - merge_maps(log.attributes, ParseKeyValue(log.attributes["syslog_sd_params_raw"]), "upsert") where IsString(log.attributes["syslog_sd_params_raw"])
      - 'merge_maps(log.attributes, ExtractGrokPatterns(log.attributes["syslog_meta"], "\\[%{DATA:syslog.meta_id} (?<syslog_meta_params_raw>([^\\]\\\\]|\\\\.)+)\\]", true), "upsert") where IsString(log.attributes["syslog_meta"])'
      - merge_maps(log.attributes, ParseKeyValue(log.attributes["syslog_meta_params_raw"]), "upsert") where IsString(log.attributes["syslog_meta_params_raw"])
      - set(log.attributes["syslog.message"], Trim(log.attributes["syslog.message"], "﻿"))
      - set(log.attributes["syslog.facility"], Int(log.attributes["syslog.pri"]) / 8) where IsString(log.attributes["syslog.pri"])
      - set(log.attributes["syslog.severity"], Int(log.attributes["syslog.pri"]) - (log.attributes["syslog.facility"] * 8)) where IsString(log.attributes["syslog.pri"])
      - set(log.severity_text, "FATAL") where log.attributes["syslog.severity"] == 0
      - set(log.severity_number, 21) where log.attributes["syslog.severity"] == 0
      - set(log.severity_text, "FATAL") where log.attributes["syslog.severity"] == 1
      - set(log.severity_number, 21) where log.attributes["syslog.severity"] == 1
      - set(log.severity_text, "FATAL") where log.attributes["syslog.severity"] == 2
      - set(log.severity_number, 21) where log.attributes["syslog.severity"] == 2
      - set(log.severity_text, "ERROR") where log.attributes["syslog.severity"] == 3
      - set(log.severity_number, 17) where log.attributes["syslog.severity"] == 3
      - set(log.severity_text, "WARN") where log.attributes["syslog.severity"] == 4
      - set(log.severity_number, 13) where log.attributes["syslog.severity"] == 4
      - set(log.severity_text, "INFO") where log.attributes["syslog.severity"] == 5
      - set(log.severity_number, 9) where log.attributes["syslog.severity"] == 5
      - set(log.severity_text, "INFO") where log.attributes["syslog.severity"] == 6
      - set(log.severity_number, 9) where log.attributes["syslog.severity"] == 6
      - set(log.severity_text, "DEBUG") where log.attributes["syslog.severity"] == 7
      - set(log.severity_number, 5) where log.attributes["syslog.severity"] == 7
      - replace_pattern(log.attributes["syslog.timestamp"], "(.+)([\\+\\-])(\\d+):(\\d+)$", "$$1$$2$$3$$4")
      - set(log.time, Time(log.attributes["syslog.timestamp"], "%Y-%m-%dT%H:%M:%S.%sZ")) where IsMatch(log.attributes["syslog.timestamp"], "^\\d+\\-.+Z$")
      - set(log.time, Time(log.attributes["syslog.timestamp"], "%Y-%m-%dT%H:%M:%S.%s%z")) where IsMatch(log.attributes["syslog.timestamp"], "^\\d+\\-.+\\d$")
      - set(log.time, Time(log.attributes["syslog.timestamp"], "%b %e %H:%M:%S")) where IsMatch(log.attributes["syslog.timestamp"], "^[a-zA-Z]+ .+\\d$")
      - set(log.body, log.attributes["syslog.message"])
      - delete_key(log.attributes, "syslog.message")
      - delete_key(log.attributes, "syslog_sd")
      - delete_key(log.attributes, "syslog_sd_params_raw")
      - delete_key(log.attributes, "syslog_meta")
      - delete_key(log.attributes, "syslog_meta_params_raw")
      - delete_key(log.attributes, "syslog.meta_id")
      - delete_key(log.attributes, "syslog.pri")
      - delete_key(log.attributes, "syslog.severity")
      - delete_key(log.attributes, "syslog.timestamp")
      - delete_key(log.attributes, "syslog.procid") where log.attributes["syslog.procid"] == "-"
      - delete_key(log.attributes, "syslog.msgid") where log.attributes["syslog.msgid"] == "-"
      - delete_key(log.attributes, "syslog6587.msglen")
  filter/syslog:
    error_mode: ignore
    logs:
      log_record:
      #! Example: Exclude noisy logs from "noisy-app"
      - log.attributes["syslog.program"] == "noisy-app"
  groupbyattrs/syslog:
    keys:
    - syslog.program
    - syslog.hostname
    - container_image
    - app_name
    - app
    - node
  transform/syslog-resource:
    error_mode: ignore
    log_statements:
    - context: log
      statements:
      - set(resource.attributes["service.name"], resource.attributes["syslog.program"]) where IsString(resource.attributes["syslog.program"])
      - set(resource.attributes["client.address"], resource.attributes["syslog.hostname"]) where IsString(resource.attributes["syslog.hostname"])
      - set(resource.attributes["container.image.name"], resource.attributes["container_image"]) where IsString(resource.attributes["container_image"])
      - set(resource.attributes["service.name"], resource.attributes["app_name"]) where IsString(resource.attributes["app_name"])
      - set(resource.attributes["service.name"], resource.attributes["app"]) where IsString(resource.attributes["app"])
      - set(resource.attributes["k8s.node.name"], resource.attributes["node"]) where IsString(resource.attributes["node"])
      - delete_key(resource.attributes, "syslog.program")
      - delete_key(resource.attributes, "syslog.hostname")
      - delete_key(resource.attributes, "container_image")
      - delete_key(resource.attributes, "app_name")
      - delete_key(resource.attributes, "app")
      - delete_key(resource.attributes, "node")
exporters:
  otlphttp/lognroll:
    endpoint: http://lognroll:4318
    tls:
      insecure: true
    headers:
      Authorization: Bearer changeme
    compression: gzip
  debug:
    verbosity: detailed
service:
  pipelines:
    logs/tcplog:
      receivers:
      - tcplog/syslog
      processors:
      - transform/syslog
      - filter/syslog
      - groupbyattrs/syslog
      - transform/syslog-resource
      exporters:
      - debug
      - otlphttp/lognroll
```

パイプラインは以下の順序で処理されます：

```yaml
receivers:
- tcplog/syslog        # Receive syslog

processors:
- transform/syslog     # Parse and normalize
- filter/syslog        # Filter out noisy logs
- groupbyattrs/syslog  # Group by program into separate resources
- transform/syslog-resource  # Set resource attributes

exporters:
- debug                # Debug output
- otlphttp/lognroll    # OTLP export
```

#### transform/syslog - syslog メッセージのパース

ここが設定の中核部分です。[OTTL (OpenTelemetry Transformation Language)](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/ottl/README.md)を使用して、生のsyslogメッセージを構造化データに変換します。

まず、syslog には主に2つのフォーマットがあります。

**RFC 3164 (BSD syslog)**

```
<14>Jan  6 12:00:00 myhost myapp[1234]: Hello world
```

**RFC 5424**

```
<14>1 2026-01-06T12:00:00Z myhost myapp 1234 ID001 [sd@123 key="value"] Hello world
```

これらを Grok パターンでパースします。

```yaml
- merge_maps(log.attributes, ExtractGrokPatterns(log.body,
  "(?:%{INT:syslog6587.msglen} )?<%{POSINT:syslog.pri}>(%{SPACE})?(?:%{NONNEGINT:syslog5424.ver} )?(?:%{SYSLOGTIMESTAMP:syslog.timestamp}|%{TIMESTAMP_ISO8601:syslog.timestamp}) %{DATA:syslog.hostname} %{DATA:syslog.program}(?:\\[%{POSINT:syslog.pid}\\])?(:)? %{GREEDYDATA:syslog.message}",
  true), "upsert") where IsString(log.body)
```

このパターンは RFC 3164 と RFC 5424 の両方に対応し、以下の属性を抽出します：

| 属性                 | 説明             | 例                                           |
|--------------------|----------------|---------------------------------------------|
| `syslog.pri`       | Priority 値     | `14`                                        |
| `syslog5424.ver`   | RFC 5424 バージョン | `1`                                         |
| `syslog.timestamp` | タイムスタンプ        | `Jan  6 12:00:00` or `2026-01-06T12:00:00Z` |
| `syslog.hostname`  | ホスト名           | `myhost`                                    |
| `syslog.program`   | プログラム名         | `myapp`                                     |
| `syslog.pid`       | プロセス ID        | `1234`                                      |
| `syslog.message`   | メッセージ本文        | `Hello world`                               |

RFC 5424 では `[sd_id key="value"]` 形式の構造化データを含められるため、これもパースします。

```yaml
# Extract Structured Data
- merge_maps(log.attributes, ExtractGrokPatterns(log.attributes["syslog.message"],
  "(?P<syslog_sd>\\[[^\\]\\\\]*@[^\\]\\\\]*(?:\\\\.[^\\]\\\\]*)*\\])..."), "upsert")

# Parse Key-Value pairs
- merge_maps(log.attributes, ParseKeyValue(log.attributes["syslog_sd_params_raw"]), "upsert")
  where IsString(log.attributes["syslog_sd_params_raw"])
```

これにより `[mysd@123 app="web" node="node-1"]` のようなデータが `app="web"`, `node="node-1"`として属性に展開されます。

次に、syslog の Priority 値から Facility と Severity を計算します。Priority 値は `Facility * 8 + Severity` で計算されているため、逆算します。

```yaml
- set(log.attributes["syslog.facility"], Int(log.attributes["syslog.pri"]) / 8)
- set(log.attributes["syslog.severity"], Int(log.attributes["syslog.pri"]) - (log.attributes["syslog.facility"] * 8))
```

そして、syslog の severity (0-7) を OpenTelemetry の severity_text/severity_number に変換します。

```yaml
- set(log.severity_text, "FATAL") where log.attributes["syslog.severity"] == 0  # Emergency
- set(log.severity_number, 21) where log.attributes["syslog.severity"] == 0
- set(log.severity_text, "ERROR") where log.attributes["syslog.severity"] == 3  # Error
- set(log.severity_number, 17) where log.attributes["syslog.severity"] == 3
- set(log.severity_text, "WARN") where log.attributes["syslog.severity"] == 4   # Warning
- set(log.severity_number, 13) where log.attributes["syslog.severity"] == 4
- set(log.severity_text, "INFO") where log.attributes["syslog.severity"] == 6   # Informational
- set(log.severity_number, 9) where log.attributes["syslog.severity"] == 6
- set(log.severity_text, "DEBUG") where log.attributes["syslog.severity"] == 7  # Debug
- set(log.severity_number, 5) where log.attributes["syslog.severity"] == 7
```

また、syslog のタイムスタンプ形式に応じて `log.time` を設定します。

```yaml
# ISO 8601 (UTC)
- set(log.time, Time(log.attributes["syslog.timestamp"], "%Y-%m-%dT%H:%M:%S.%sZ"))
  where IsMatch(log.attributes["syslog.timestamp"], "^\\d+\\-.+Z$")

# ISO 8601 (with timezone)
- set(log.time, Time(log.attributes["syslog.timestamp"], "%Y-%m-%dT%H:%M:%S.%s%z"))
  where IsMatch(log.attributes["syslog.timestamp"], "^\\d+\\-.+\\d$")

# BSD syslog format (Jan  6 12:00:00)
- set(log.time, Time(log.attributes["syslog.timestamp"], "%b %e %H:%M:%S"))
  where IsMatch(log.attributes["syslog.timestamp"], "^[a-zA-Z]+ .+\\d$")
```

**注意**: BSD 形式では日付が1桁の場合スペースでパディングされるため、`%d` ではなく `%e` を使用します。

最後に、パース後の中間属性を削除してログをクリーンに保ちます。

```yaml
- set(log.body, log.attributes["syslog.message"])  # Set message to body
- delete_key(log.attributes, "syslog.message")
- delete_key(log.attributes, "syslog.pri")
- delete_key(log.attributes, "syslog.severity")
- delete_key(log.attributes, "syslog.timestamp")
# ... delete other intermediate attributes
```

#### filter/syslog - ノイズの除去

特定のプログラムからのログを除外します：

```yaml
filter/syslog:
  error_mode: ignore
  logs:
    log_record:
    - log.attributes["syslog.program"] == "noisy-app"
```

#### groupbyattrs/syslog - リソースの分離

```yaml
groupbyattrs/syslog:
  keys:
  - syslog.program
  - syslog.hostname
  # ...
```

`groupbyattrs` processor は重要な役割を果たします：

* 同じ `syslog.program`、`syslog.hostname`などを持つログを同一リソースにグループ化
* `syslog.program` などを `log.attributes` から `resource.attributes` に昇格

これにより、異なるプログラムからのログが同じバッチで処理されても、リソース属性が混在しません。

次のProcessor(`transform/syslog-resource`)でリソース属性の設定を行いますが、ここの`groupbyattrs` がないと、例えば、以下の問題が発生します：

* 複数のログが同じバッチで処理される
* 各ログで `resource.attributes["service.name"]` が上書きされる
* 最後に処理されたログの値で全ログの `service.name` が決まってしまう

リソース属性に設定したいキーは全て `groupbyattrs` の `keys` に含めてください。

#### transform/syslog-resource - リソース属性の設定

```yaml
transform/syslog-resource:
  error_mode: ignore
  log_statements:
  - context: log
    statements:
    - set(resource.attributes["container.image.name"], log.attributes["container_image"])
      where IsString(log.attributes["container_image"])
    - set(resource.attributes["service.name"], resource.attributes["syslog.program"])
      where IsString(resource.attributes["syslog.program"])
    - set(resource.attributes["service.name"], log.attributes["app_name"])
      where IsString(log.attributes["app_name"])
    # ...
```

`groupbyattrs` の後に実行されるため、`syslog.program` は既に `resource.attributes` に移動しています。これを `service.name` として設定します。

また、Structured Data から抽出された `app_name`, `container_image`, `node`なども対応する[Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/)の属性にマッピングします。
繰り返しになりますが、これらのリソース属性に設定したいキーは全て `groupbyattrs` の `keys` に含めてください。

Processorの順序が重要です。

```yaml
processors:
- transform/syslog          # 1. Parse (set syslog.program in log.attributes)
- filter/syslog             # 2. Filter (evaluate log.attributes["syslog.program"])
- groupbyattrs/syslog       # 3. Group (move syslog.program to resource.attributes)
- transform/syslog-resource # 4. Set resource (use resource.attributes["syslog.program"])
```

この順序により：

- フィルタは `log.attributes` を参照できる
- リソース属性は `groupbyattrs` で分離された後に設定される

となります。

### Docker Composeファイル例

ではこの設定ファイルを使ってDocker Composeで起動してみます。以下は`compose.yaml`の例です。

```yaml
services:
  lognroll:
    image: ghcr.io/categolj/lognroll:native
    pull_policy: always
    restart: always
    ports:
    - "4318:4318"
    volumes:
    - ./data:/data
    environment:
      LOGNROLL_DB_PATH: /data/lognroll.db
      LOGNROLL_AUTH_TOKEN: changeme
  otelcol:
    image: ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.142.0
    restart: always
    command: [ "--config", "/etc/otelcol/config.yaml" ]
    ports:
    - "5514:5514"
    volumes:
    - ./config.yaml:/etc/otelcol/config.yaml:ro
    depends_on:
    - lognroll
```

次のコマンドで起動します。

```bash
docker compose up
```

`nc`コマンドを使いテストメッセージを送信します。次の例ではRFC 5424形式のメッセージを送信しています。

```bash
echo "<14>1 $(date -u +%Y-%m-%dT%H:%M:%SZ) myhost myapp 1234 ID98 [uls@0 logtype=\"access\" clustername=\"mycluster\" namespace=\"mynamespace\"] Sample app log message." | nc localhost 5514
```

OpenTelemetry Collectorのログに以下のような出力が表示されれば成功です。

```
2026-01-07T02:14:20.048Z	info	service@v0.143.0/service.go:250	Starting otelcol-contrib...	{"resource": {"service.instance.id": "ff7d6f62-ce23-43b6-a5de-d5f61e47b366", "service.name": "otelcol-contrib", "service.version": "0.143.1"}, "Version": "0.143.1", "NumCPU": 16}
2026-01-07T02:14:20.048Z	info	extensions/extensions.go:40	Starting extensions...	{"resource": {"service.instance.id": "ff7d6f62-ce23-43b6-a5de-d5f61e47b366", "service.name": "otelcol-contrib", "service.version": "0.143.1"}}
2026-01-07T02:14:20.048Z	info	adapter/receiver.go:41	Starting stanza receiver	{"resource": {"service.instance.id": "ff7d6f62-ce23-43b6-a5de-d5f61e47b366", "service.name": "otelcol-contrib", "service.version": "0.143.1"}, "otelcol.component.id": "tcplog/syslog", "otelcol.component.kind": "receiver", "otelcol.signal": "logs"}
2026-01-07T02:14:20.048Z	info	service@v0.143.0/service.go:273	Everything is ready. Begin running and processing data.	{"resource": {"service.instance.id": "ff7d6f62-ce23-43b6-a5de-d5f61e47b366", "service.name": "otelcol-contrib", "service.version": "0.143.1"}}
2026-01-07T02:14:30.149Z	info	Logs	{"resource": {"service.instance.id": "ff7d6f62-ce23-43b6-a5de-d5f61e47b366", "service.name": "otelcol-contrib", "service.version": "0.143.1"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs", "resource logs": 1, "log records": 1}
2026-01-07T02:14:30.149Z	info	ResourceLog #0
Resource SchemaURL: 
Resource attributes:
     -> client.address: Str(myhost)
     -> service.name: Str(myapp)
ScopeLogs #0
ScopeLogs SchemaURL: 
InstrumentationScope  
LogRecord #0
ObservedTimestamp: 2026-01-07 02:14:30.130747673 +0000 UTC
Timestamp: 2026-01-07 02:14:30 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(Sample app log message.)
Attributes:
     -> logtype: Str(access)
     -> clustername: Str(mycluster)
     -> syslog5424.ver: Str(1)
     -> syslog.sd_id: Str(uls@0)
     -> syslog.facility: Int(1)
     -> namespace: Str(mynamespace)
     -> syslog.msgid: Str(ID98)
     -> syslog.procid: Str(1234)
Trace ID: 
Span ID: 
Flags: 0
	{"resource": {"service.instance.id": "ff7d6f62-ce23-43b6-a5de-d5f61e47b366", "service.name": "otelcol-contrib", "service.version": "0.143.1"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs"}
```

LognrollのUI (http://localhost:4318 username: 空, password: changeme)で"View Logs"ボタンを押すとログが確認できます。

![image](https://s3.ik.am/ikam/_/1769080131819_pasted-image.png)

最後に色々なログメッセージをスクリプトで送信してみます。次の`test-syslog.sh`スクリプトを作成します。

```bash
#!/bin/bash
# Test script for sending syslog messages to otelcol

HOST="${SYSLOG_HOST:-localhost}"
PORT="${SYSLOG_PORT:-5514}"

# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

send_syslog() {
    local msg="$1"
    local desc="$2"
    echo -e "${BLUE}[Test]${NC} $desc"
    echo -e "${GREEN}>>>${NC} $msg"
    echo "$msg" | nc -w1 "$HOST" "$PORT"
    echo ""
}

echo "========================================"
echo "Syslog Test Script"
echo "Target: $HOST:$PORT"
echo "========================================"
echo ""

# RFC 3164 (BSD Syslog) format tests
echo "=== RFC 3164 (BSD Syslog) Format ==="
echo ""

send_syslog '<14>Jan  6 12:00:00 myhost myapp[1234]: This is an info message' \
    "RFC 3164 - Basic info message"

send_syslog '<11>Jan  6 12:00:01 myhost kernel: Critical kernel error occurred' \
    "RFC 3164 - Critical message (facility=1, severity=3)"

send_syslog '<134>Jan  6 12:00:02 webserver nginx[5678]: GET /api/health 200 OK' \
    "RFC 3164 - Local0 info (web server log)"

# RFC 5424 format tests
echo "=== RFC 5424 Format ==="
echo ""

send_syslog '<14>1 2026-01-06T12:00:00.123456Z myhost myapp 1234 ID001 - Hello from RFC 5424' \
    "RFC 5424 - Basic message with ISO timestamp"

send_syslog '<14>1 2026-01-06T12:00:00+09:00 myhost myapp 1234 ID002 - Message with timezone offset' \
    "RFC 5424 - Message with timezone"

send_syslog '<14>1 2026-01-06T12:00:00Z myhost myapp - - - Minimal RFC 5424 message' \
    "RFC 5424 - Minimal (no PID, no MSGID)"

# RFC 5424 with Structured Data
send_syslog '<14>1 2026-01-06T12:00:00Z myhost myapp 1234 ID003 [exampleSDID@32473 eventSource="Application" eventID="1011"] Application started successfully' \
    "RFC 5424 - With Structured Data"

send_syslog '<14>1 2026-01-06T12:00:00Z myhost myapp 1234 ID004 [mysd@12345 container_image="nginx:latest" app_name="web-frontend" node="node-1"][meta sequence="123"] Container log message' \
    "RFC 5424 - With SD and meta"

# Different severity levels
echo "=== Severity Level Tests ==="
echo ""

send_syslog '<8>Jan  6 12:00:10 myhost test[100]: Emergency - System is unusable' \
    "Severity 0 - Emergency"

send_syslog '<9>Jan  6 12:00:11 myhost test[100]: Alert - Action must be taken immediately' \
    "Severity 1 - Alert"

send_syslog '<10>Jan  6 12:00:12 myhost test[100]: Critical - Critical conditions' \
    "Severity 2 - Critical"

send_syslog '<11>Jan  6 12:00:13 myhost test[100]: Error - Error conditions' \
    "Severity 3 - Error"

send_syslog '<12>Jan  6 12:00:14 myhost test[100]: Warning - Warning conditions' \
    "Severity 4 - Warning"

send_syslog '<13>Jan  6 12:00:15 myhost test[100]: Notice - Normal but significant' \
    "Severity 5 - Notice"

send_syslog '<14>Jan  6 12:00:16 myhost test[100]: Info - Informational messages' \
    "Severity 6 - Info"

send_syslog '<15>Jan  6 12:00:17 myhost test[100]: Debug - Debug-level messages' \
    "Severity 7 - Debug"

# Test filtered message (should be filtered out by config)
echo "=== Filter Test ==="
echo ""

send_syslog '<14>Jan  6 12:00:20 myhost noisy-app[9999]: This message should be filtered out' \
    "Filtered message (syslog.program == noisy-app)"

echo "========================================"
echo "All test messages sent!"
echo "========================================"
```

次のコマンドで実行します。

```bash
chmod +x test-syslog.sh
./test-syslog.sh
```

OpenTelemetry Collectorのログに以下のような出力が表示されれば成功です。

```
2026-01-07T02:16:24.350Z	info	Logs	{"resource": {"service.instance.id": "ff7d6f62-ce23-43b6-a5de-d5f61e47b366", "service.name": "otelcol-contrib", "service.version": "0.143.1"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs", "resource logs": 4, "log records": 14}
2026-01-07T02:16:24.350Z	info	ResourceLog #0
Resource SchemaURL: 
Resource attributes:
     -> client.address: Str(webserver)
     -> service.name: Str(nginx)
ScopeLogs #0
ScopeLogs SchemaURL: 
InstrumentationScope  
LogRecord #0
ObservedTimestamp: 2026-01-07 02:16:24.253552555 +0000 UTC
Timestamp: 2026-01-06 12:00:02 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(GET /api/health 200 OK)
Attributes:
     -> syslog.facility: Int(16)
     -> syslog.pid: Str(5678)
Trace ID: 
Span ID: 
Flags: 0
ResourceLog #1
Resource SchemaURL: 
Resource attributes:
     -> client.address: Str(myhost)
     -> service.name: Str(myapp)
ScopeLogs #0
ScopeLogs SchemaURL: 
InstrumentationScope  
LogRecord #0
ObservedTimestamp: 2026-01-07 02:16:24.261633882 +0000 UTC
Timestamp: 2026-01-06 12:00:00.123456 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(- Hello from RFC 5424)
Attributes:
     -> syslog.procid: Str(1234)
     -> syslog.facility: Int(1)
     -> syslog5424.ver: Str(1)
     -> syslog.msgid: Str(ID001)
Trace ID: 
Span ID: 
Flags: 0
LogRecord #1
ObservedTimestamp: 2026-01-07 02:16:24.268348627 +0000 UTC
Timestamp: 2026-01-06 03:00:00 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(- Message with timezone offset)
Attributes:
     -> syslog.facility: Int(1)
     -> syslog5424.ver: Str(1)
     -> syslog.procid: Str(1234)
     -> syslog.msgid: Str(ID002)
Trace ID: 
Span ID: 
Flags: 0
LogRecord #2
ObservedTimestamp: 2026-01-07 02:16:24.275025663 +0000 UTC
Timestamp: 2026-01-06 12:00:00 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(- Minimal RFC 5424 message)
Attributes:
     -> syslog5424.ver: Str(1)
     -> syslog.facility: Int(1)
Trace ID: 
Span ID: 
Flags: 0
LogRecord #3
ObservedTimestamp: 2026-01-07 02:16:24.281035992 +0000 UTC
Timestamp: 2026-01-06 12:00:00 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(Application started successfully)
Attributes:
     -> syslog5424.ver: Str(1)
     -> syslog.sd_id: Str(exampleSDID@32473)
     -> syslog.procid: Str(1234)
     -> syslog.facility: Int(1)
     -> eventID: Str(1011)
     -> eventSource: Str(Application)
     -> syslog.msgid: Str(ID003)
Trace ID: 
Span ID: 
Flags: 0
ResourceLog #2
Resource SchemaURL: 
Resource attributes:
     -> k8s.node.name: Str(node-1)
     -> container.image.name: Str(nginx:latest)
     -> client.address: Str(myhost)
     -> service.name: Str(web-frontend)
ScopeLogs #0
ScopeLogs SchemaURL: 
InstrumentationScope  
LogRecord #0
ObservedTimestamp: 2026-01-07 02:16:24.286272738 +0000 UTC
Timestamp: 2026-01-06 12:00:00 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(Container log message)
Attributes:
     -> syslog.procid: Str(1234)
     -> syslog5424.ver: Str(1)
     -> sequence: Str(123)
     -> syslog.msgid: Str(ID004)
     -> syslog.sd_id: Str(mysd@12345)
     -> syslog.facility: Int(1)
Trace ID: 
Span ID: 
Flags: 0
ResourceLog #3
Resource SchemaURL: 
Resource attributes:
     -> client.address: Str(myhost)
     -> service.name: Str(test)
ScopeLogs #0
ScopeLogs SchemaURL: 
InstrumentationScope  
LogRecord #0
ObservedTimestamp: 2026-01-07 02:16:24.291517067 +0000 UTC
Timestamp: 2026-01-06 12:00:10 +0000 UTC
SeverityText: FATAL
SeverityNumber: Fatal(21)
Body: Str(Emergency - System is unusable)
Attributes:
     -> syslog.pid: Str(100)
     -> syslog.facility: Int(1)
Trace ID: 
Span ID: 
Flags: 0
LogRecord #1
ObservedTimestamp: 2026-01-07 02:16:24.296908604 +0000 UTC
Timestamp: 2026-01-06 12:00:11 +0000 UTC
SeverityText: FATAL
SeverityNumber: Fatal(21)
Body: Str(Alert - Action must be taken immediately)
Attributes:
     -> syslog.facility: Int(1)
     -> syslog.pid: Str(100)
Trace ID: 
Span ID: 
Flags: 0
LogRecord #2
ObservedTimestamp: 2026-01-07 02:16:24.301921226 +0000 UTC
Timestamp: 2026-01-06 12:00:12 +0000 UTC
SeverityText: FATAL
SeverityNumber: Fatal(21)
Body: Str(Critical - Critical conditions)
Attributes:
     -> syslog.facility: Int(1)
     -> syslog.pid: Str(100)


{"@timestamp":"2026-01-07T02:16:24.354679851Z","log":{"level":"INFO","logger":"accesslog"},"process":{"pid":1,"thread":{"name":"jetty-26"}},"service":{"name":"lognroll","node":{}},"message":"kind=server method=POST url=\"http://lognroll:4318/v1/logs\" status=200 duration=3 protocol=\"HTTP/1.1\" remote=\"192.168.158.3\" user_agent=\"OpenTelemetry Collector Contrib/0.143.1 (linux/arm64)\"","kind":"server","method":"POST","url":"http://lognroll:4318/v1/logs","status":200,"duration":3,"host":"lognroll","path":"/v1/logs","remote":"192.168.158.3","protocol":"HTTP/1.1","user_agent":"OpenTelemetry Collector Contrib/0.143.1 (linux/arm64)","ecs":{"version":"8.11"}}
Trace ID: 
Span ID: 
Flags: 0
LogRecord #3
ObservedTimestamp: 2026-01-07 02:16:24.307254763 +0000 UTC
Timestamp: 2026-01-06 12:00:13 +0000 UTC
SeverityText: ERROR
SeverityNumber: Error(17)
Body: Str(Error - Error conditions)
Attributes:
     -> syslog.facility: Int(1)
     -> syslog.pid: Str(100)
Trace ID: 
Span ID: 
Flags: 0
LogRecord #4
ObservedTimestamp: 2026-01-07 02:16:24.315502132 +0000 UTC
Timestamp: 2026-01-06 12:00:14 +0000 UTC
SeverityText: WARN
SeverityNumber: Warn(13)
Body: Str(Warning - Warning conditions)
Attributes:
     -> syslog.pid: Str(100)
     -> syslog.facility: Int(1)
Trace ID: 
Span ID: 
Flags: 0
LogRecord #5
ObservedTimestamp: 2026-01-07 02:16:24.319832087 +0000 UTC
Timestamp: 2026-01-06 12:00:15 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(Notice - Normal but significant)
Attributes:
     -> syslog.pid: Str(100)
     -> syslog.facility: Int(1)
Trace ID: 
Span ID: 
Flags: 0
LogRecord #6
ObservedTimestamp: 2026-01-07 02:16:24.323949959 +0000 UTC
Timestamp: 2026-01-06 12:00:16 +0000 UTC
SeverityText: INFO
SeverityNumber: Info(9)
Body: Str(Info - Informational messages)
Attributes:
     -> syslog.facility: Int(1)
     -> syslog.pid: Str(100)
Trace ID: 
Span ID: 
Flags: 0
LogRecord #7
ObservedTimestamp: 2026-01-07 02:16:24.328798788 +0000 UTC
Timestamp: 2026-01-06 12:00:17 +0000 UTC
SeverityText: DEBUG
SeverityNumber: Debug(5)
Body: Str(Debug - Debug-level messages)
Attributes:
     -> syslog.facility: Int(1)
     -> syslog.pid: Str(100)
Trace ID: 
Span ID: 
Flags: 0
	{"resource": {"service.instance.id": "ff7d6f62-ce23-43b6-a5de-d5f61e47b366", "service.name": "otelcol-contrib", "service.version": "0.143.1"}, "otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs"}
```

リソース属性が変わるごとにLogRecordのかたまりが分かれていることが確認できます。また、`noisy-app`からのログは出力されていないことも確認できます。

LognrollのUIでも次のようにログメッセージが確認できます。

![image](https://s3.ik.am/ikam/_/1769080158987_pasted-image.png)

Lognrollはbodyの全文検索や、attributesやresource attributesでのフィルタリングも可能です。

![image](https://s3.ik.am/ikam/_/1769080177051_pasted-image.png)

---

以上で、OpenTelemetry Collectorを使用したsyslogメッセージの受信、パース、正規化、フィルタリング、リソース属性の設定、およびOTLPでのエクスポートの一連の流れを解説しました。
この設定を基にカスタマイズしてSyslogログの受信環境が構築できるでしょう。
