|
| 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 | + |
| 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