Skip to content

Commit 8bb3067

Browse files
committed
docs: add OpenTelemetry output documentation
Add docs/otel/ with: - otel.md: how to build with --features otel or make image-otel, configure the OTLP endpoint, and a full Loki + Grafana example - loki-config.yaml: minimal single-instance Loki configuration - grafana-datasource.yaml: auto-provisioned Loki datasource - grafana-dashboard-provider.yaml: dashboard file provider config - grafana-fact-dashboard.json: example dashboard with event rate, events by type/file pie charts, and a log stream panel Assisted-by: claude-opus-4-6 <noreply@opencode.ai>
1 parent 5d2e169 commit 8bb3067

5 files changed

Lines changed: 368 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: 1
2+
3+
providers:
4+
- name: default
5+
type: file
6+
disableDeletion: true
7+
options:
8+
path: /var/lib/grafana/dashboards

docs/otel/grafana-datasource.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: 1
2+
3+
datasources:
4+
- name: Loki
5+
type: loki
6+
uid: loki
7+
access: proxy
8+
url: http://loki:3100
9+
isDefault: true
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
{
2+
"annotations": { "list": [] },
3+
"editable": true,
4+
"fiscalYearStartMonth": 0,
5+
"graphTooltip": 0,
6+
"links": [],
7+
"panels": [
8+
{
9+
"title": "Event rate",
10+
"type": "timeseries",
11+
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 0 },
12+
"datasource": { "type": "loki", "uid": "loki" },
13+
"fieldConfig": {
14+
"defaults": {
15+
"custom": {
16+
"drawStyle": "bars",
17+
"fillOpacity": 30,
18+
"stacking": { "mode": "normal" }
19+
},
20+
"color": { "mode": "palette-classic" }
21+
},
22+
"overrides": []
23+
},
24+
"options": {
25+
"legend": { "displayMode": "list", "placement": "bottom" },
26+
"tooltip": { "mode": "multi" }
27+
},
28+
"targets": [
29+
{
30+
"datasource": { "type": "loki", "uid": "loki" },
31+
"expr": "sum by (detected_level) (count_over_time({service_name=\"fact\"} [1m]))",
32+
"legendFormat": "{{detected_level}}",
33+
"refId": "A"
34+
}
35+
]
36+
},
37+
{
38+
"title": "Events by type",
39+
"type": "piechart",
40+
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
41+
"datasource": { "type": "loki", "uid": "loki" },
42+
"fieldConfig": {
43+
"defaults": {
44+
"color": { "mode": "palette-classic" },
45+
"custom": {
46+
"hideFrom": { "legend": false, "tooltip": false, "viz": false }
47+
}
48+
},
49+
"overrides": []
50+
},
51+
"options": {
52+
"displayLabels": ["percent"],
53+
"legend": { "displayMode": "list", "placement": "bottom", "showLegend": true },
54+
"tooltip": { "mode": "single", "sort": "none" },
55+
"pieType": "donut",
56+
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
57+
"sort": "desc"
58+
},
59+
"targets": [
60+
{
61+
"datasource": { "type": "loki", "uid": "loki" },
62+
"editorMode": "code",
63+
"expr": "sum by (file_event_type) (count_over_time({service_name=\"fact\"} | file_event_type != `` [$__range]))",
64+
"legendFormat": "{{file_event_type}}",
65+
"queryType": "range",
66+
"direction": "backward",
67+
"refId": "A"
68+
}
69+
]
70+
},
71+
{
72+
"title": "Events by file",
73+
"type": "piechart",
74+
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
75+
"datasource": { "type": "loki", "uid": "loki" },
76+
"fieldConfig": {
77+
"defaults": {
78+
"color": { "mode": "palette-classic" },
79+
"custom": {
80+
"hideFrom": { "legend": false, "tooltip": false, "viz": false }
81+
}
82+
},
83+
"overrides": []
84+
},
85+
"options": {
86+
"displayLabels": ["percent"],
87+
"legend": { "displayMode": "list", "placement": "bottom", "showLegend": true },
88+
"tooltip": { "mode": "single", "sort": "none" },
89+
"pieType": "donut",
90+
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
91+
"sort": "desc"
92+
},
93+
"targets": [
94+
{
95+
"datasource": { "type": "loki", "uid": "loki" },
96+
"editorMode": "code",
97+
"expr": "sum by (file_filename) (count_over_time({service_name=\"fact\"} | file_filename != `` [$__range]))",
98+
"legendFormat": "{{file_filename}}",
99+
"queryType": "range",
100+
"direction": "backward",
101+
"refId": "A"
102+
}
103+
]
104+
},
105+
{
106+
"title": "Events",
107+
"type": "logs",
108+
"gridPos": { "h": 16, "w": 24, "x": 0, "y": 16 },
109+
"datasource": { "type": "loki", "uid": "loki" },
110+
"options": {
111+
"showTime": true,
112+
"showLabels": true,
113+
"showCommonLabels": false,
114+
"wrapLogMessage": false,
115+
"prettifyLogMessage": false,
116+
"enableLogDetails": true,
117+
"sortOrder": "Descending",
118+
"dedupStrategy": "none"
119+
},
120+
"targets": [
121+
{
122+
"datasource": { "type": "loki", "uid": "loki" },
123+
"expr": "{service_name=\"fact\"}",
124+
"refId": "A"
125+
}
126+
]
127+
}
128+
],
129+
"schemaVersion": 39,
130+
"tags": ["fact", "otel"],
131+
"templating": { "list": [] },
132+
"time": { "from": "now-15m", "to": "now" },
133+
"timepicker": {},
134+
"timezone": "browser",
135+
"title": "fact - File Activity",
136+
"uid": "fact-file-activity",
137+
"version": 1
138+
}

docs/otel/loki-config.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
auth_enabled: false
2+
3+
server:
4+
http_listen_port: 3100
5+
6+
common:
7+
instance_addr: 127.0.0.1
8+
path_prefix: /tmp/loki
9+
storage:
10+
filesystem:
11+
chunks_directory: /tmp/loki/chunks
12+
rules_directory: /tmp/loki/rules
13+
replication_factor: 1
14+
ring:
15+
kvstore:
16+
store: inmemory
17+
18+
schema_config:
19+
configs:
20+
- from: 2020-10-24
21+
store: tsdb
22+
object_store: filesystem
23+
schema: v13
24+
index:
25+
prefix: index_
26+
period: 24h

docs/otel/otel.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# `fact` OpenTelemetry output
2+
3+
`fact` can optionally push file activity events as
4+
[OTLP](https://opentelemetry.io/docs/specs/otlp/) log records over
5+
HTTP (protobuf encoding) to any OTLP-compatible endpoint such as
6+
[Grafana Loki](https://grafana.com/oss/loki/),
7+
[Grafana Alloy](https://grafana.com/docs/alloy/), or a standard
8+
[OpenTelemetry Collector](https://opentelemetry.io/docs/collector/).
9+
10+
This feature is gated behind the `otel` Cargo feature flag and is not
11+
included in the default build.
12+
13+
## Building with OTel support
14+
15+
### Local build
16+
17+
```sh
18+
cargo build --release --features otel
19+
```
20+
21+
### Container image
22+
23+
```sh
24+
make image-otel
25+
```
26+
27+
This invokes the regular `image` target with `CARGO_ARGS=--features otel`.
28+
If you use podman instead of docker, set `DOCKER=podman`:
29+
30+
```sh
31+
make image-otel DOCKER=podman
32+
```
33+
34+
## Configuration
35+
36+
The OTel output requires a single setting: the OTLP HTTP endpoint URL
37+
where log records will be pushed. It can be configured in three ways
38+
(later entries override earlier ones):
39+
40+
| Method | Example |
41+
|---|---|
42+
| YAML config file | `otel:` block with `endpoint:` key (see below) |
43+
| Environment variable | `FACT_OTEL_ENDPOINT=http://loki:3100/otlp/v1/logs` |
44+
| CLI flag | `--otel-endpoint http://loki:3100/otlp/v1/logs` |
45+
46+
YAML example:
47+
48+
```yaml
49+
otel:
50+
endpoint: http://loki:3100/otlp/v1/logs
51+
```
52+
53+
The endpoint value is the full OTLP HTTP logs URL exposed by your
54+
backend. Common values:
55+
56+
| Backend | Endpoint |
57+
|---|---|
58+
| Grafana Loki | `http://<host>:3100/otlp/v1/logs` |
59+
| OTel Collector | `http://<host>:4318/v1/logs` |
60+
61+
When no endpoint is configured the OTel client is idle and consumes no
62+
resources.
63+
64+
## Technical details
65+
66+
- Events are serialized as structured OTLP `LogRecord` attributes
67+
using the native OpenTelemetry `AnyValue` map format.
68+
- Transport is HTTP with binary protobuf encoding.
69+
- Records are batched automatically by the OpenTelemetry SDK before
70+
export. Batch parameters can be tuned via standard
71+
[OTLP environment variables](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#batch-log-record-processor)
72+
such as `OTEL_BLRP_SCHEDULE_DELAY` and
73+
`OTEL_BLRP_MAX_EXPORT_BATCH_SIZE`.
74+
- The OTLP resource `service.name` is set to `fact`.
75+
- All records are emitted with severity `Info`.
76+
- The endpoint supports hot-reload: sending `SIGHUP` to reload the
77+
configuration will reconnect the OTel client to a new endpoint
78+
without restarting the process.
79+
80+
## Example: pushing events to Loki with Grafana
81+
82+
This walkthrough sets up a minimal Loki instance with Grafana for
83+
visualization, then runs `fact` with the OTel output pointing at
84+
Loki's OTLP ingestion endpoint.
85+
86+
All commands below use `docker` but work identically with `podman`.
87+
88+
### 1. Create a network
89+
90+
```sh
91+
docker network create fact-loki
92+
```
93+
94+
### 2. Start Loki
95+
96+
This directory contains a minimal Loki configuration in
97+
[loki-config.yaml](loki-config.yaml). Mount it into the container:
98+
99+
```sh
100+
docker run -d --name loki \
101+
--network fact-loki \
102+
-p 3100:3100 \
103+
-v ./docs/otel/loki-config.yaml:/etc/loki/config.yaml:ro,z \
104+
docker.io/grafana/loki:3.7.3
105+
```
106+
107+
Loki exposes its OTLP ingestion endpoint at `/otlp/v1/logs` on port
108+
3100.
109+
110+
### 3. Start Grafana
111+
112+
Grafana is pre-configured with a Loki datasource
113+
([grafana-datasource.yaml](grafana-datasource.yaml)) and a small
114+
dashboard ([grafana-fact-dashboard.json](grafana-fact-dashboard.json))
115+
via provisioning files:
116+
117+
```sh
118+
docker run -d --name grafana \
119+
--network fact-loki \
120+
-p 3000:3000 \
121+
-e GF_AUTH_ANONYMOUS_ENABLED=true \
122+
-e GF_AUTH_ANONYMOUS_ORG_ROLE=Admin \
123+
-v ./docs/otel/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/loki.yaml:ro,z \
124+
-v ./docs/otel/grafana-dashboard-provider.yaml:/etc/grafana/provisioning/dashboards/default.yaml:ro,z \
125+
-v ./docs/otel/grafana-fact-dashboard.json:/var/lib/grafana/dashboards/fact.json:ro,z \
126+
docker.io/grafana/grafana:13.1.0
127+
```
128+
129+
### 4. Run `fact`
130+
131+
#### Option A: container image
132+
133+
Build the OTel image first (if you haven't already):
134+
135+
```sh
136+
make image-otel
137+
```
138+
139+
Then run it on the same network so it can reach Loki by name:
140+
141+
```sh
142+
docker run --rm -it \
143+
--privileged \
144+
--network fact-loki \
145+
-e FACT_OTEL_ENDPOINT=http://loki:3100/otlp/v1/logs \
146+
-e FACT_PATHS='/etc:/etc/**/*' \
147+
-e FACT_HOST_MOUNT=/host \
148+
-v /:/host:ro \
149+
"$(make image-name)"
150+
```
151+
152+
#### Option B: local binary
153+
154+
Since the binary runs on the host, reach Loki via the published port:
155+
156+
```sh
157+
cargo build --release --features otel
158+
sudo -E FACT_OTEL_ENDPOINT=http://localhost:3100/otlp/v1/logs \
159+
./target/release/fact -p /etc
160+
```
161+
162+
### 5. View events in Grafana
163+
164+
Open <http://localhost:3000/dashboards> and select the **fact - File
165+
Activity** dashboard. It contains four panels:
166+
167+
- **Event rate** — a time series showing the number of events per
168+
minute.
169+
- **Events by type** — a donut chart breaking down events by type
170+
(open, creation, chmod, etc.).
171+
- **Events by file** — a donut chart breaking down events by filename.
172+
- **Events** — a log stream of all file activity events with full
173+
attribute details.
174+
175+
You can also query events directly in **Explore**
176+
(<http://localhost:3000/explore>) using LogQL, for example:
177+
178+
```logql
179+
{service_name="fact"}
180+
```
181+
182+
### Cleanup
183+
184+
```sh
185+
docker rm -f loki grafana
186+
docker network rm fact-loki
187+
```

0 commit comments

Comments
 (0)