Skip to content

Commit be892db

Browse files
cyrille-leclerczeitlingersvrnmtiffany76opentelemetrybot
authored
New Blog Post: Collecting OpenTelemetry-compliant Java logs from files (#5600)
Co-authored-by: Gregor Zeitlinger <[email protected]> Co-authored-by: Severin Neumann <[email protected]> Co-authored-by: Tiffany Hrabusa <[email protected]> Co-authored-by: opentelemetrybot <[email protected]> Co-authored-by: Patrice Chalin <[email protected]> Co-authored-by: Severin Neumann <[email protected]>
1 parent 12730d2 commit be892db

File tree

4 files changed

+399
-0
lines changed

4 files changed

+399
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
---
2+
title: Collecting OpenTelemetry-compliant Java logs from files
3+
linkTitle: OTel-compliant Java logs from files
4+
date: 2024-12-09
5+
author: >
6+
[Cyrille Le Clerc](https://github.com/cyrille-leclerc) (Grafana Labs), [Gregor
7+
Zeitlinger](https://github.com/zeitlinger) (Grafana Labs)
8+
issue: https://github.com/open-telemetry/opentelemetry.io/issues/5606
9+
sig: Java, Specification
10+
# prettier-ignore
11+
cSpell:ignore: Clerc cust Cyrille Dotel Gregor Logback logback otlphttp otlpjson resourcedetection SLF4J stdout Zeitlinger
12+
---
13+
14+
If you want to get logs from your Java application ingested into an
15+
OpenTelemetry-compatible logs backend, the easiest and recommended way is using
16+
an OpenTelemetry protocol (OTLP) exporter. However, some scenarios require logs
17+
to be output to files or stdout due to organizational or reliability needs.
18+
19+
A common approach to centralize logs is to use unstructured logs, parse them
20+
with regular expressions, and add contextual attributes.
21+
22+
However, regular expression parsing is problematic. They become complex and
23+
fragile quickly when handling all log fields, line breaks in exceptions, and
24+
unexpected log format changes. Parsing errors are inevitable with this method.
25+
26+
## Native solution for Java logs
27+
28+
The OpenTelemetry Java Instrumentation agent and SDK now offer an easy solution
29+
to convert logs from frameworks like SLF4J/Logback or Log4j2 into OTel-compliant
30+
JSON logs on stdout with all resource and log attributes.
31+
32+
This is a true turnkey solution:
33+
34+
- No code or dependency changes, just a few configuration adjustments typical
35+
for production deployment.
36+
- No complex field mapping in the log collector. Just use the
37+
[OTLP/JSON connector](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/connector/otlpjsonconnector)
38+
to ingest the payload.
39+
- Automatic correlation between logs, traces, and metrics.
40+
41+
This blog post shows how to set up this solution step by step.
42+
43+
- In the first part, we'll show how to configure the Java application to output
44+
logs in the OTLP/JSON format.
45+
- In the second part, we'll show how to configure the OpenTelemetry Collector to
46+
ingest the logs.
47+
- Finally, we'll show a Kubernetes-specific setup to handle container logs.
48+
49+
![OTLP/JSON Architecture](otlpjson-architecture.png)
50+
51+
## Configure Java application to output OTLP/JSON logs
52+
53+
{{% alert title="Note" color="info" %}}
54+
55+
Blog post instructions can easily get outdated. In case of issues, check this
56+
[sample application deployed on Kubernetes](https://github.com/grafana/docker-otel-lgtm/tree/main/examples/java/json-logging-otlp),
57+
which is continuously updated and tested against the latest versions.
58+
59+
{{% /alert %}}
60+
61+
No code changes needed. Continue using your preferred logging library, including
62+
templated logs, mapped diagnostic context, and structured logging.
63+
64+
```java
65+
Logger logger = org.slf4j.LoggerFactory.getLogger(MyClass.class);
66+
...
67+
MDC.put("customerId", customerId);
68+
69+
logger.info("Order {} successfully placed", orderId);
70+
71+
logger.atInfo().
72+
.addKeyValue("orderId", orderId)
73+
.addKeyValue("outcome", "success")
74+
.log("placeOrder");
75+
```
76+
77+
Export the logs captured by the OTel Java instrumentation to stdout using the
78+
OTel JSON format (aka [OTLP/JSON](/docs/specs/otlp/#json-protobuf-encoding)).
79+
Configuration parameters for
80+
[Logback](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-appender-1.0/javaagent)
81+
and
82+
[Log4j](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/log4j/log4j-appender-2.17/javaagent)
83+
are optional but recommended.
84+
85+
```bash
86+
# Tested with opentelemetry-javaagent v2.10.0
87+
#
88+
# Details on -Dotel.logback-appender.* params on documentation page:
89+
# https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-appender-1.0/javaagent
90+
91+
java -javaagent:/path/to/opentelemetry-javaagent.jar \
92+
-Dotel.logs.exporter=experimental-otlp/stdout \
93+
-Dotel.instrumentation.logback-appender.experimental-log-attributes=true \
94+
-Dotel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes=true \
95+
-Dotel.instrumentation.logback-appender.experimental.capture-mdc-attributes=* \
96+
-jar /path/to/my-app.jar
97+
```
98+
99+
The `-Dotel.logs.exporter=experimental-otlp/stdout` JVM argument and the
100+
environment variable `OTEL_LOGS_EXPORTER="experimental-otlp/stdout"` can be used
101+
interchangeably.
102+
103+
{{% alert title="Note" color="info" %}}
104+
105+
The OTLP logs exporter is experimental and subject to change. Check the
106+
[Specification PR](https://github.com/open-telemetry/opentelemetry-specification/pull/4183)
107+
for the latest updates.
108+
109+
{{% /alert %}}
110+
111+
Verify that OTLP/JSON logs are outputted to stdout. The logs are in the
112+
OTLP/JSON format, with a JSON object per line. The log records are nested in the
113+
`resourceLogs` array. Example:
114+
115+
<details>
116+
<summary> <code>{"resourceLogs":[{"resource" ...}]}</code> </summary>
117+
118+
```json
119+
{
120+
"resourceLogs": [
121+
{
122+
"resource": {
123+
"attributes": [
124+
{
125+
"key": "deployment.environment.name",
126+
"value": {
127+
"stringValue": "staging"
128+
}
129+
},
130+
{
131+
"key": "service.instance.id",
132+
"value": {
133+
"stringValue": "6ad88e10-238c-4fb7-bf97-38df19053366"
134+
}
135+
},
136+
{
137+
"key": "service.name",
138+
"value": {
139+
"stringValue": "checkout"
140+
}
141+
},
142+
{
143+
"key": "service.namespace",
144+
"value": {
145+
"stringValue": "shop"
146+
}
147+
},
148+
{
149+
"key": "service.version",
150+
"value": {
151+
"stringValue": "1.1"
152+
}
153+
}
154+
]
155+
},
156+
"scopeLogs": [
157+
{
158+
"scope": {
159+
"name": "com.mycompany.checkout.CheckoutServiceServer$CheckoutServiceImpl",
160+
"attributes": []
161+
},
162+
"logRecords": [
163+
{
164+
"timeUnixNano": "1730435085776869000",
165+
"observedTimeUnixNano": "1730435085776944000",
166+
"severityNumber": 9,
167+
"severityText": "INFO",
168+
"body": {
169+
"stringValue": "Order order-12035 successfully placed"
170+
},
171+
"attributes": [
172+
{
173+
"key": "customerId",
174+
"value": {
175+
"stringValue": "customer-49"
176+
}
177+
},
178+
{
179+
"key": "thread.id",
180+
"value": {
181+
"intValue": "44"
182+
}
183+
},
184+
{
185+
"key": "thread.name",
186+
"value": {
187+
"stringValue": "grpc-default-executor-1"
188+
}
189+
}
190+
],
191+
"flags": 1,
192+
"traceId": "42de1f0dd124e27619a9f3c10bccac1c",
193+
"spanId": "270984d03e94bb8b"
194+
}
195+
]
196+
}
197+
],
198+
"schemaUrl": "https://opentelemetry.io/schemas/1.24.0"
199+
}
200+
]
201+
}
202+
```
203+
204+
</details>
205+
206+
## Configure the Collector to ingest the OTLP/JSON logs
207+
208+
{{< figure class="figure" src="otel-collector-otlpjson-pipeline.png" attr="View OTel Collector pipeline with OTelBin" attrlink="https://www.otelbin.io/s/69739d790cf279c203fc8efc86ad1a876a2fc01a" >}}
209+
210+
```yaml
211+
# tested with otelcol-contrib v0.112.0
212+
213+
receivers:
214+
filelog/otlp-json-logs:
215+
# start_at: beginning # for testing purpose, use "start_at: beginning"
216+
include: [/path/to/my-app.otlpjson.log]
217+
otlp:
218+
protocols:
219+
grpc:
220+
http:
221+
222+
processors:
223+
batch:
224+
resourcedetection:
225+
detectors: ['env', 'system']
226+
override: false
227+
228+
connectors:
229+
otlpjson:
230+
231+
service:
232+
pipelines:
233+
logs/raw_otlpjson:
234+
receivers: [filelog/otlp-json-logs]
235+
# (i) no need for processors before the otlpjson connector
236+
# Declare processors in the shared "logs" pipeline below
237+
processors: []
238+
exporters: [otlpjson]
239+
logs:
240+
receivers: [otlp, otlpjson]
241+
processors: [resourcedetection, batch]
242+
# remove "debug" for production deployments
243+
exporters: [otlphttp, debug]
244+
245+
exporters:
246+
debug:
247+
verbosity: detailed
248+
# Exporter to the OTLP backend like `otlphttp`
249+
otlphttp:
250+
```
251+
252+
Verify the logs collected by the OTel Collector by checking the output of the
253+
OTel Collector Debug exporter:
254+
255+
```log
256+
2024-11-01T10:03:31.074+0530 info Logs {"kind": "exporter", "data_type": "logs", "name": "debug", "resource logs": 1, "log records": 1}
257+
2024-11-01T10:03:31.074+0530 info ResourceLog #0
258+
Resource SchemaURL: https://opentelemetry.io/schemas/1.24.0
259+
Resource attributes:
260+
-> deployment.environment.name: Str(staging)
261+
-> service.instance.id: Str(6ad88e10-238c-4fb7-bf97-38df19053366)
262+
-> service.name: Str(checkout)
263+
-> service.namespace: Str(shop)
264+
-> service.version: Str(1.1)
265+
ScopeLogs #0
266+
ScopeLogs SchemaURL:
267+
InstrumentationScope com.mycompany.checkout.CheckoutServiceServer$CheckoutServiceImpl
268+
LogRecord #0
269+
ObservedTimestamp: 2024-11-01 04:24:45.776944 +0000 UTC
270+
Timestamp: 2024-11-01 04:24:45.776869 +0000 UTC
271+
SeverityText: INFO
272+
SeverityNumber: Info(9)
273+
Body: Str(Order order-12035 successfully placed)
274+
Attributes:
275+
-> customerId: Str(cust-12345)
276+
-> thread.id: Int(44)
277+
-> thread.name: Str(grpc-default-executor-1)
278+
Trace ID: 42de1f0dd124e27619a9f3c10bccac1c
279+
Span ID: 270984d03e94bb8b
280+
Flags: 1
281+
{"kind": "exporter", "data_type": "logs", "name": "debug"}
282+
```
283+
284+
Verify the logs in the OpenTelemetry backend.
285+
286+
After the pipeline works end-to-end, ensure production readiness:
287+
288+
- Remove the `debug` exporter from the `logs` pipeline in the OTel Collector
289+
configuration.
290+
- Disable file and console exporters in the logging framework (for example,
291+
`logback.xml`) but keep using the logging configuration to filter logs. The
292+
OTel Java agent will output JSON logs to stdout.
293+
294+
```xml
295+
<!-- tested with logback-classic v1.5.11 -->
296+
<configuration>
297+
<logger name="com.example" level="debug"/>
298+
<root level="info">
299+
<!-- No appender as the OTel Agent emits otlpjson logs through stdout -->
300+
<!--
301+
IMPORTANT enable a console appender to troubleshoot cases where
302+
logs are missing in the OTel backend
303+
-->
304+
</root>
305+
</configuration>
306+
```
307+
308+
## Configure an OpenTelemetry Collector in Kubernetes to handle container logs
309+
310+
To support Kubernetes and container specifics, add the built-in
311+
[`container`](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/pkg/stanza/docs/operators/container.md)
312+
parsing step in the pipeline without needing specific mapping configuration.
313+
314+
Replace `<<namespace>>`, `<<pod_name>>`, and `<<container_name>>` with the
315+
desired values or use a broader [glob pattern](https://pkg.go.dev/v.io/v23/glob)
316+
like `*`.
317+
318+
```yaml
319+
receivers:
320+
filelog/otlp-json-logs:
321+
# start_at: beginning # for testing purpose, use "start_at: beginning"
322+
include: [/var/log/pods/<<namespace>>_<<pod_name>>_*/<<container_name>>/]
323+
include_file_path: true
324+
operators:
325+
- type: container
326+
add_metadata_from_filepath: true
327+
328+
otlp:
329+
protocols:
330+
grpc:
331+
http:
332+
333+
processors:
334+
batch:
335+
resourcedetection:
336+
detectors: ['env', 'system']
337+
override: false
338+
339+
connectors:
340+
otlpjson:
341+
342+
service:
343+
pipelines:
344+
logs/raw_otlpjson:
345+
receivers: [filelog/otlp-json-logs]
346+
# (i) no need for processors before the otlpjson connector
347+
# Declare processors in the shared "logs" pipeline below
348+
processors: []
349+
exporters: [otlpjson]
350+
logs:
351+
receivers: [otlp, otlpjson]
352+
processors: [resourcedetection, batch]
353+
# remove "debug" for production deployments
354+
exporters: [otlphttp, debug]
355+
356+
exporters:
357+
debug:
358+
verbosity: detailed
359+
# Exporter to the OTLP backend like `otlphttp`
360+
otlphttp:
361+
```
362+
363+
## Conclusion
364+
365+
This blog post showed how to collect file-based Java logs with OpenTelemetry.
366+
The solution is easy to set up and provides a turnkey solution for converting
367+
logs from frameworks like SLF4J/Logback or Log4j2 into OTel-compliant JSON logs
368+
on stdout with all resource and log attributes. This JSON format is certainly
369+
verbose, but it generally has minimal impact on performances and offers a solid
370+
balance by providing highly contextualized logs that can be correlated with
371+
traces and metrics.
372+
373+
If any of the steps are unclear or you encounter issues, check this
374+
[sample application deployed on Kubernetes](https://github.com/grafana/docker-otel-lgtm/tree/main/examples/java/json-logging-otlp),
375+
which is continuously updated and tested against the latest versions.
376+
377+
Any feedback or questions? Reach out on
378+
[GitHub](https://github.com/open-telemetry/opentelemetry-specification/pull/4183)
379+
or on [Slack](/community/#develop-and-contribute) (`#otel-collector`).

0 commit comments

Comments
 (0)