Skip to content

Commit 5b19736

Browse files
authored
ECS formatter for JUL (java.util.logging) (#69)
1 parent a72a28e commit 5b19736

File tree

13 files changed

+497
-21
lines changed

13 files changed

+497
-21
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ For nested structures consider prefixing with `custom.` to make sure you won't g
112112
- [Logback](logback-ecs-encoder/README.md) (default for Spring Boot)
113113
- [Log4j2](log4j2-ecs-layout/README.md)
114114
- [Log4j](log4j-ecs-layout/README.md)
115+
- [java.util.logging](jul-ecs-formatter/README.md)
115116

116117
### Step 2: Enable APM log correlation (optional)
117118
If you are using the Elastic APM Java agent,

ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ public static void serializeObjectEnd(StringBuilder builder) {
5252
}
5353

5454
public static void serializeLoggerName(StringBuilder builder, String loggerName) {
55-
builder.append("\"log.logger\":\"");
56-
JsonUtils.quoteAsString(loggerName, builder);
57-
builder.append("\",");
55+
if (loggerName != null) {
56+
builder.append("\"log.logger\":\"");
57+
JsonUtils.quoteAsString(loggerName, builder);
58+
builder.append("\",");
59+
}
5860
}
5961

6062
public static void serializeThreadName(StringBuilder builder, String threadName) {
@@ -65,6 +67,11 @@ public static void serializeThreadName(StringBuilder builder, String threadName)
6567
}
6668
}
6769

70+
public static void serializeThreadId(StringBuilder builder, long threadId) {
71+
builder.append("\"process.thread.id\":");
72+
builder.append(threadId);
73+
builder.append(",");
74+
}
6875
public static void serializeFormattedMessage(StringBuilder builder, String message) {
6976
builder.append("\"message\":\"");
7077
JsonUtils.quoteAsString(message, builder);
@@ -134,9 +141,12 @@ public static void serializeOrigin(StringBuilder builder, String fileName, Strin
134141
builder.append("\",");
135142
builder.append("\"function\":\"");
136143
JsonUtils.quoteAsString(methodName, builder);
137-
builder.append("\",");
138-
builder.append("\"file.line\":");
139-
builder.append(lineNumber);
144+
builder.append('"');
145+
if (lineNumber >= 0) {
146+
builder.append(',');
147+
builder.append("\"file.line\":");
148+
builder.append(lineNumber);
149+
}
140150
builder.append("},");
141151
}
142152

ecs-logging-core/src/test/java/co/elastic/logging/AbstractEcsLoggingTest.java

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ void testSimpleLog() throws Exception {
6161

6262
@Test
6363
void testThreadContext() throws Exception {
64-
putMdc("foo", "bar");
65-
debug("test");
66-
assertThat(getLastLogLine().get("foo").textValue()).isEqualTo("bar");
64+
if (putMdc("foo", "bar")) {
65+
debug("test");
66+
assertThat(getLastLogLine().get("foo").textValue()).isEqualTo("bar");
67+
}
6768
}
6869

6970
@Test
@@ -76,14 +77,16 @@ void testThreadContextStack() throws Exception {
7677

7778
@Test
7879
void testMdc() throws Exception {
79-
putMdc("transaction.id", "0af7651916cd43dd8448eb211c80319c");
80-
putMdc("span.id", "foo");
81-
putMdc("foo", "bar");
82-
debug("test");
83-
assertThat(getLastLogLine().get("labels.transaction.id")).isNull();
84-
assertThat(getLastLogLine().get("transaction.id").textValue()).isEqualTo("0af7651916cd43dd8448eb211c80319c");
85-
assertThat(getLastLogLine().get("span.id").textValue()).isEqualTo("foo");
86-
assertThat(getLastLogLine().get("foo").textValue()).isEqualTo("bar");
80+
if (putMdc("transaction.id", "0af7651916cd43dd8448eb211c80319c")) {
81+
putMdc("span.id", "foo");
82+
putMdc("foo", "bar");
83+
debug("test");
84+
assertThat(getLastLogLine().get("labels.transaction.id")).isNull();
85+
assertThat(getLastLogLine().get("transaction.id").textValue())
86+
.isEqualTo("0af7651916cd43dd8448eb211c80319c");
87+
assertThat(getLastLogLine().get("span.id").textValue()).isEqualTo("foo");
88+
assertThat(getLastLogLine().get("foo").textValue()).isEqualTo("bar");
89+
}
8790
}
8891

8992
@Test
@@ -113,7 +116,9 @@ void testLogOrigin() throws Exception {
113116
assertThat(getLastLogLine().get("log.origin").get("file.line").intValue()).isPositive();
114117
}
115118

116-
public abstract void putMdc(String key, String value);
119+
public boolean putMdc(String key, String value) {
120+
return false;
121+
}
117122

118123
public boolean putNdc(String message) {
119124
return false;

jul-ecs-formatter/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# ECS formatter for JUL
2+
3+
Formatter for JUL (java.util.logging) which produce ECS-compatible records. May be useful for applications which use JUL as primary logging framework (e.g. Apache Tomcat).
4+
5+
## Step 1: add dependency
6+
7+
Latest version: [![Maven Central](https://img.shields.io/maven-central/v/co.elastic.logging/jul-ecs-formatter.svg)](https://search.maven.org/search?q=g:co.elastic.logging%20AND%20a:jul-ecs-formatter)
8+
9+
Add a dependency to your application
10+
```xml
11+
<dependency>
12+
<groupId>co.elastic.logging</groupId>
13+
<artifactId>jul-ecs-formatter</artifactId>
14+
<version>${ecs-logging-java.version}</version>
15+
</dependency>
16+
```
17+
If you are not using a dependency management tool, like maven, you have to add both, `jul-ecs-formatter` and `ecs-logging-core` jars manually to the classpath. For example to the `$CATALINA_HOME/lib` directory.
18+
19+
## Step 2: use the `EcsFormatter`
20+
21+
Specify `co.elastic.logging.jul.EcsFormatter` as `formatter` for the required log handler.
22+
23+
## Example
24+
For example `$CATALINA_HOME/conf/logging.properties`
25+
26+
```properties
27+
java.util.logging.ConsoleHandler.level = FINE
28+
java.util.logging.ConsoleHandler.formatter = co.elastic.logging.jul.EcsFormatter
29+
co.elastic.logging.jul.EcsFormatter.serviceName=my-app
30+
```
31+
32+
## Layout Parameters
33+
34+
|Parameter name |Type |Default|Description|
35+
|-----------------|-------|-------|-----------|
36+
|serviceName |String | |Sets the `service.name` field so you can filter your logs by a particular service |
37+
|eventDataset |String |`${serviceName}.log`|Sets the `event.dataset` field used by the machine learning job of the Logs app to look for anomalies in the log rate. |
38+
|stackTraceAsArray|boolean|`false`|Serializes the [`error.stack_trace`](https://www.elastic.co/guide/en/ecs/current/ecs-error.html) as a JSON array where each element is in a new line to improve readability. Note that this requires a slightly more complex [Filebeat configuration](../README.md#when-stacktraceasarray-is-enabled).|
39+
|includeOrigin |boolean|`false`|If `true`, adds the [`log.origin.file.name`](https://www.elastic.co/guide/en/ecs/current/ecs-log.html), [`log.origin.file.line`](https://www.elastic.co/guide/en/ecs/current/ecs-log.html) and [`log.origin.function`](https://www.elastic.co/guide/en/ecs/current/ecs-log.html) fields. Note that JUL does not stores line number and `log.origin.file.line` will have '1' value. |
40+
41+

jul-ecs-formatter/pom.xml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<parent>
4+
<artifactId>ecs-logging-java-parent</artifactId>
5+
<groupId>co.elastic.logging</groupId>
6+
<version>0.3.1-SNAPSHOT</version>
7+
</parent>
8+
<modelVersion>4.0.0</modelVersion>
9+
10+
<properties>
11+
<parent.base.dir>${project.basedir}/..</parent.base.dir>
12+
</properties>
13+
14+
<artifactId>jul-ecs-formatter</artifactId>
15+
<name>${project.groupId}:${project.artifactId}</name>
16+
17+
<dependencies>
18+
<dependency>
19+
<groupId>${project.groupId}</groupId>
20+
<artifactId>ecs-logging-core</artifactId>
21+
<version>${project.version}</version>
22+
</dependency>
23+
<dependency>
24+
<groupId>${project.groupId}</groupId>
25+
<artifactId>ecs-logging-core</artifactId>
26+
<version>${project.version}</version>
27+
<type>test-jar</type>
28+
<scope>test</scope>
29+
</dependency>
30+
</dependencies>
31+
32+
</project>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*-
2+
* #%L
3+
* Java ECS logging
4+
* %%
5+
* Copyright (C) 2019 - 2020 Elastic and contributors
6+
* %%
7+
* Licensed to Elasticsearch B.V. under one or more contributor
8+
* license agreements. See the NOTICE file distributed with
9+
* this work for additional information regarding copyright
10+
* ownership. Elasticsearch B.V. licenses this file to you under
11+
* the Apache License, Version 2.0 (the "License"); you may
12+
* not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing,
18+
* software distributed under the License is distributed on an
19+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20+
* KIND, either express or implied. See the License for the
21+
* specific language governing permissions and limitations
22+
* under the License.
23+
* #L%
24+
*/
25+
package co.elastic.logging.jul;
26+
27+
import java.util.logging.Formatter;
28+
import java.util.logging.LogManager;
29+
import java.util.logging.LogRecord;
30+
31+
import co.elastic.logging.EcsJsonSerializer;
32+
33+
public class EcsFormatter extends Formatter {
34+
35+
private static final String UNKNOWN_FILE = "<Unknown>";
36+
37+
private boolean stackTraceAsArray;
38+
private String serviceName;
39+
private boolean includeOrigin;
40+
private String eventDataset;
41+
42+
/**
43+
* Default constructor. Will read configuration from LogManager properties.
44+
*/
45+
public EcsFormatter() {
46+
serviceName = getProperty("co.elastic.logging.jul.EcsFormatter.serviceName", null);
47+
includeOrigin = Boolean.getBoolean(getProperty("co.elastic.logging.jul.EcsFormatter.includeOrigin", "false"));
48+
stackTraceAsArray = Boolean
49+
.getBoolean(getProperty("co.elastic.logging.jul.EcsFormatter.stackTraceAsArray", "false"));
50+
eventDataset = getProperty("co.elastic.logging.jul.EcsFormatter.eventDataset", null);
51+
eventDataset = EcsJsonSerializer.computeEventDataset(eventDataset, serviceName);
52+
}
53+
54+
@Override
55+
public String format(final LogRecord record) {
56+
final StringBuilder builder = new StringBuilder();
57+
EcsJsonSerializer.serializeObjectStart(builder, record.getMillis());
58+
EcsJsonSerializer.serializeLogLevel(builder, record.getLevel().getName());
59+
EcsJsonSerializer.serializeFormattedMessage(builder, record.getMessage());
60+
EcsJsonSerializer.serializeServiceName(builder, serviceName);
61+
EcsJsonSerializer.serializeEventDataset(builder, eventDataset);
62+
EcsJsonSerializer.serializeThreadId(builder, record.getThreadID());
63+
EcsJsonSerializer.serializeLoggerName(builder, record.getLoggerName());
64+
if (includeOrigin && record.getSourceClassName() != null && record.getSourceMethodName() != null) {
65+
EcsJsonSerializer.serializeOrigin(builder, buildFileName(record.getSourceClassName()), record.getSourceMethodName(), -1);
66+
}
67+
final Throwable throwableInformation = record.getThrown();
68+
if (throwableInformation != null) {
69+
EcsJsonSerializer.serializeException(builder, throwableInformation, stackTraceAsArray);
70+
}
71+
EcsJsonSerializer.serializeObjectEnd(builder);
72+
return builder.toString();
73+
}
74+
75+
protected void setIncludeOrigin(final boolean includeOrigin) {
76+
this.includeOrigin = includeOrigin;
77+
}
78+
79+
protected void setServiceName(final String serviceName) {
80+
this.serviceName = serviceName;
81+
}
82+
83+
protected void setStackTraceAsArray(final boolean stackTraceAsArray) {
84+
this.stackTraceAsArray = stackTraceAsArray;
85+
}
86+
87+
public void setEventDataset(String eventDataset) {
88+
this.eventDataset = eventDataset;
89+
}
90+
91+
private String getProperty(final String name, final String defaultValue) {
92+
String value = LogManager.getLogManager().getProperty(name);
93+
if (value == null) {
94+
value = defaultValue;
95+
} else {
96+
value = value.trim();
97+
}
98+
return value;
99+
}
100+
101+
private String buildFileName(String className) {
102+
String result = UNKNOWN_FILE;
103+
if (className != null) {
104+
int fileNameEnd = className.indexOf('$');
105+
if (fileNameEnd < 0) {
106+
fileNameEnd = className.length();
107+
}
108+
int classNameStart = className.lastIndexOf('.');
109+
if (classNameStart < fileNameEnd) {
110+
result = className.substring(classNameStart + 1, fileNameEnd) + ".java";
111+
}
112+
}
113+
return result;
114+
}
115+
116+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ecs-logging-java
2+
Copyright 2019 - 2020 Elasticsearch B.V.

0 commit comments

Comments
 (0)