Skip to content

Commit d3e4a98

Browse files
sekaiserFlogger Team
authored andcommitted
Extends the functionality of the Log4j2 backend by supporting ScopedLoggingContext (Grpc).
This is a functional import of #242 with changes to formatting and other minor changes to satisfy internal checks. RELNOTES=The log4j2 backend now supports `ScopedLoggingContext`. PiperOrigin-RevId: 392510036
1 parent d108885 commit d3e4a98

File tree

6 files changed

+928
-24
lines changed

6 files changed

+928
-24
lines changed

log4j2/BUILD

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ LOG4J2_BACKEND_RESOURCES = glob(["src/main/resources/**"])
1515
java_library(
1616
name = "log4j2_backend",
1717
srcs = LOG4J2_BACKEND_SRCS,
18-
javacopts = ["-source 7 -target 7"],
18+
javacopts = ["-source 8 -target 8"],
1919
resources = LOG4J2_BACKEND_RESOURCES,
2020
tags = ["maven_coordinates=com.google.flogger:flogger-log4j2-backend:${project.version}"],
2121
deps = [
2222
"//api",
2323
"//api:system_backend",
24+
"@com_google_errorprone_error_prone_annotations",
2425
"@google_bazel_common//third_party/java/log4j2",
26+
"@org_checkerframework_checker_compat_qual",
2527
],
2628
)
2729

@@ -49,7 +51,8 @@ gen_java_tests(
4951
"//api",
5052
"//api:system_backend",
5153
"//api:testing",
52-
"@google_bazel_common//third_party/java/guava",
54+
"//google:flogger",
55+
"//grpc:grpc_context",
5356
"@google_bazel_common//third_party/java/junit",
5457
"@google_bazel_common//third_party/java/log4j2",
5558
"@google_bazel_common//third_party/java/truth",

log4j2/src/main/java/com/google/common/flogger/backend/log4j2/Log4j2LogEventUtil.java

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,34 @@
2222

2323
import com.google.common.flogger.LogContext;
2424
import com.google.common.flogger.LogSite;
25+
import com.google.common.flogger.MetadataKey;
26+
import com.google.common.flogger.backend.BaseMessageFormatter;
2527
import com.google.common.flogger.backend.LogData;
2628
import com.google.common.flogger.backend.MessageUtils;
2729
import com.google.common.flogger.backend.Metadata;
30+
import com.google.common.flogger.backend.MetadataHandler;
2831
import com.google.common.flogger.backend.MetadataProcessor;
2932
import com.google.common.flogger.backend.Platform;
3033
import com.google.common.flogger.backend.SimpleMessageFormatter;
31-
import java.util.Collections;
32-
import java.util.Map;
34+
import com.google.common.flogger.context.ScopedLoggingContext;
35+
import com.google.common.flogger.context.Tags;
3336
import java.util.logging.Level;
37+
import java.util.stream.Collectors;
38+
import java.util.stream.StreamSupport;
3439
import org.apache.logging.log4j.core.LogEvent;
40+
import org.apache.logging.log4j.core.LoggerContext;
41+
import org.apache.logging.log4j.core.config.Configuration;
42+
import org.apache.logging.log4j.core.config.DefaultConfiguration;
43+
import org.apache.logging.log4j.core.impl.ContextDataFactory;
3544
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
3645
import org.apache.logging.log4j.core.time.Instant;
3746
import org.apache.logging.log4j.core.time.MutableInstant;
3847
import org.apache.logging.log4j.core.util.Throwables;
3948
import org.apache.logging.log4j.message.SimpleMessage;
49+
import org.apache.logging.log4j.util.StringMap;
4050

41-
/** Helper to format LogData.
51+
/**
52+
* Helper to format LogData.
4253
*
4354
* <p>Note: Any changes in this code should, as far as possible, be reflected in the equivalently
4455
* named log4j implementation. If the behaviour of this class starts to deviate from that of the
@@ -52,10 +63,34 @@ private Log4j2LogEventUtil() {}
5263
static LogEvent toLog4jLogEvent(String loggerName, LogData logData) {
5364
MetadataProcessor metadata =
5465
MetadataProcessor.forScopeAndLogSite(Platform.getInjectedMetadata(), logData.getMetadata());
55-
String message = SimpleMessageFormatter.getDefaultFormatter().format(logData, metadata);
66+
67+
/*
68+
* If no configuration file could be located, Log4j2 will use the DefaultConfiguration. This
69+
* will cause logging output to go to the console and the context data will be ignored. This
70+
* mechanism can be used to detect if a configuration file has been loaded (or if the default
71+
* configuration was overwritten through the means of a configuration factory) by checking the
72+
* type of the current configuration class.
73+
*
74+
* Be aware that the LoggerContext class is not part of Log4j2's public API and behavior can
75+
* change with any minor release.
76+
*
77+
* For the future we are thinking about implementing a Flogger aware Log4j2 configuration (e.g.
78+
* using a configuration builder with a custom ConfigurationFactory) to configure a formatter,
79+
* which can perhaps be installed as default if nothing else is present. Then, we would not rely
80+
* on Log4j2 internals.
81+
*/
82+
LoggerContext ctx = LoggerContext.getContext(false);
83+
Configuration config = ctx.getConfiguration();
84+
String message;
85+
if (config instanceof DefaultConfiguration) {
86+
message = SimpleMessageFormatter.getDefaultFormatter().format(logData, metadata);
87+
} else {
88+
message =
89+
BaseMessageFormatter.appendFormattedMessage(logData, new StringBuilder()).toString();
90+
}
91+
5692
Throwable thrown = metadata.getSingleValue(LogContext.Key.LOG_CAUSE);
57-
return toLog4jLogEvent(
58-
loggerName, logData, message, toLog4jLevel(logData.getLevel()), thrown);
93+
return toLog4jLogEvent(loggerName, logData, message, toLog4jLevel(logData.getLevel()), thrown);
5994
}
6095

6196
static LogEvent toLog4jLogEvent(String loggerName, RuntimeException error, LogData badData) {
@@ -71,21 +106,6 @@ private static LogEvent toLog4jLogEvent(
71106
String message,
72107
org.apache.logging.log4j.Level level,
73108
Throwable thrown) {
74-
// The Mapped Diagnostic Context (MDC) allows to include additional metadata into logs which
75-
// are written from the current thread.
76-
//
77-
// Example:
78-
// MDC.put("user.id", userId);
79-
// // do business logic that triggers logs
80-
// MDC.clear();
81-
//
82-
// By using '%X{key}' in the ConversionPattern of an appender this data can be included in the
83-
// logs.
84-
//
85-
// We could include this data here by doing 'MDC.getContext()', but we don't want to encourage
86-
// people using the log4j specific MDC. Instead this should be supported by a LoggingContext and
87-
// usage of Flogger tags.
88-
Map<String, String> mdcProperties = Collections.emptyMap();
89109

90110
LogSite logSite = logData.getLogSite();
91111
StackTraceElement locationInfo =
@@ -105,7 +125,7 @@ private static LogEvent toLog4jLogEvent(
105125
.setThrown(thrown != null ? Throwables.getRootCause(thrown) : null)
106126
.setIncludeLocation(true)
107127
.setSource(locationInfo)
108-
.setContextMap(mdcProperties)
128+
.setContextData(createContextMap(logData))
109129
.build();
110130
}
111131

@@ -180,4 +200,63 @@ private static void appendLogData(LogData data, StringBuilder out) {
180200
out.append("\n method: ").append(data.getLogSite().getMethodName());
181201
out.append("\n line number: ").append(data.getLogSite().getLineNumber());
182202
}
203+
204+
private static final MetadataHandler<MetadataKey.KeyValueHandler> HANDLER =
205+
MetadataHandler.builder(
206+
(MetadataHandler.ValueHandler<Object, MetadataKey.KeyValueHandler>)
207+
(key, value, kvh) -> {
208+
if (key.getClass().equals(LogContext.Key.TAGS.getClass())) {
209+
processTags(key, value, kvh);
210+
} else {
211+
// In theory a user can define a custom tag and use it as a MetadataKey. Those
212+
// keys shall be treated in the same way as LogContext.Key.TAGS when used as a
213+
// MetadataKey. Might be removed if visibility of MetadataKey#clazz changes.
214+
if (value instanceof Tags) {
215+
processTags(key, value, kvh);
216+
} else {
217+
ValueQueue.appendValues(key.getLabel(), value, kvh);
218+
}
219+
}
220+
})
221+
.setDefaultRepeatedHandler(
222+
(key, values, kvh) -> values.forEachRemaining(v -> kvh.handle(key.getLabel(), v)))
223+
.build();
224+
225+
private static void processTags(
226+
MetadataKey<Object> key, Object value, MetadataKey.KeyValueHandler kvh) {
227+
ValueQueue valueQueue = ValueQueue.appendValueToNewQueue(value);
228+
// Unlike single metadata (which is usually formatted as a single value), tags are always
229+
// formatted as a list.
230+
// Given the tags: tags -> foo=[bar], it will be formatted as tags=[foo=bar].
231+
ValueQueue.appendValues(
232+
key.getLabel(),
233+
valueQueue.size() == 1
234+
? StreamSupport.stream(valueQueue.spliterator(), false).collect(Collectors.toList())
235+
: valueQueue,
236+
kvh);
237+
}
238+
239+
/**
240+
* We do not support {@code MDC.getContext()} and {@code NDC.getStack()} and we do not make any
241+
* attempt to merge Log4j2 context data with Flogger's context data. Instead, users should use the
242+
* {@link ScopedLoggingContext}.
243+
*
244+
* <p>Flogger's {@link ScopedLoggingContext} allows to include additional metadata and tags into
245+
* logs which are written from current thread. This context data will be added to the log4j2
246+
* event.
247+
*/
248+
private static StringMap createContextMap(LogData logData) {
249+
MetadataProcessor metadataProcessor =
250+
MetadataProcessor.forScopeAndLogSite(Platform.getInjectedMetadata(), logData.getMetadata());
251+
252+
StringMap contextData = ContextDataFactory.createContextData(metadataProcessor.keyCount());
253+
metadataProcessor.process(
254+
HANDLER,
255+
(key, value) ->
256+
contextData.putValue(key, ValueQueue.maybeWrap(value, contextData.getValue(key))));
257+
258+
contextData.freeze();
259+
260+
return contextData;
261+
}
183262
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* Copyright (C) 2021 The Flogger Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.common.flogger.backend.log4j2;
18+
19+
import static com.google.common.flogger.util.Checks.checkNotNull;
20+
21+
import com.google.common.flogger.MetadataKey;
22+
import com.google.common.flogger.context.Tags;
23+
import java.util.Iterator;
24+
import java.util.LinkedList;
25+
import java.util.List;
26+
import java.util.Objects;
27+
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
28+
29+
/**
30+
* A simple FIFO queue linked-list implementation designed to store multiple metadata values in a
31+
* StringMap. There are two aspects worth pointing out:
32+
*
33+
* <p>First, it is expected that a value queue always contains at least a single item. You cannot
34+
* add null references to the queue and you cannot create an empty queue.
35+
*
36+
* <p>Second, it is expected to access the contents of the value queue via an iterator only. Hence
37+
* we do not provide a method for taking the first item in the value queue..
38+
*
39+
* <p>Metadata values in Flogger always have unique keys, but those keys can have the same label.
40+
* Because Log4j2 uses a {@code String} keyed map, we would risk clashing of values if we just used
41+
* the label to store each value directly. This class lets us store a list of values for a single
42+
* label while being memory efficient in the common case where each label really does only have one
43+
* value.
44+
*/
45+
final class ValueQueue implements Iterable<Object> {
46+
// Since the number of elements is almost never above 1 or 2, a LinkedList saves space.
47+
private final List<Object> values = new LinkedList<>();
48+
49+
private ValueQueue() {}
50+
51+
static ValueQueue newQueue(Object item) {
52+
checkNotNull(item, "item");
53+
ValueQueue valueQueue = new ValueQueue();
54+
valueQueue.put(item);
55+
return valueQueue;
56+
}
57+
58+
static Object maybeWrap(Object value, @NullableDecl Object existingValue) {
59+
checkNotNull(value, "value");
60+
if (existingValue == null) {
61+
return value;
62+
} else {
63+
// This should only rarely happen, so a few small allocations seems acceptable.
64+
ValueQueue existingQueue =
65+
existingValue instanceof ValueQueue
66+
? (ValueQueue) existingValue
67+
: ValueQueue.newQueue(existingValue);
68+
existingQueue.put(value);
69+
return existingQueue;
70+
}
71+
}
72+
73+
static void appendValues(String label, Object valueOrQueue, MetadataKey.KeyValueHandler kvh) {
74+
if (valueOrQueue instanceof ValueQueue) {
75+
for (Object value : (ValueQueue) valueOrQueue) {
76+
emit(label, value, kvh);
77+
}
78+
} else {
79+
emit(label, valueOrQueue, kvh);
80+
}
81+
}
82+
83+
/**
84+
* Helper method for creating and initializing a value queue with a non-nullable value. If value
85+
* is an instance of Tags, each tag will be added to the value queue.
86+
*/
87+
static ValueQueue appendValueToNewQueue(Object value) {
88+
ValueQueue valueQueue = new ValueQueue();
89+
ValueQueue.emit(null, value, (k, v) -> valueQueue.put(v));
90+
return valueQueue;
91+
}
92+
93+
/**
94+
* Emits a metadata label/value pair to a given {@code KeyValueHandler}, handling {@code Tags}
95+
* values specially.
96+
*
97+
* <p>Tags are key-value mappings which cannot be modified or replaced. If you add the tag mapping
98+
* {@code "foo" -> true} and later add {@code "foo" -> false}, you get "foo" mapped to both true
99+
* and false. This is very deliberate since the key space for tags is global and the risk of two
100+
* bits of code accidentally using the same tag name is real (e.g. you add "id=xyz" to a scope,
101+
* but you see "id=abcd" because someone else added "id=abcd" in a context you weren't aware of).
102+
*
103+
* <p>Given three tag mappings:
104+
* <ul>
105+
* <li>{@code "baz"} (no value)
106+
* <li>{@code "foo" -> true}
107+
* <li>{@code "foo" -> false}
108+
* </ul>
109+
*
110+
* the value queue is going to store the mappings as:
111+
* <pre>{@code
112+
* tags=[baz, foo=false, foo=true]
113+
* }</pre>
114+
*
115+
* <p>Reusing the label 'tags' is intentional as this allows us to store the flatten tags in
116+
* Log4j2's ContextMap.
117+
*/
118+
static void emit(String label, Object value, MetadataKey.KeyValueHandler kvh) {
119+
if (value instanceof Tags) {
120+
// Flatten tags to treat them as keys or key/value pairs, e.g. tags=[baz=bar, baz=bar2, foo]
121+
((Tags) value)
122+
.asMap()
123+
.forEach(
124+
(k, v) -> {
125+
if (v.isEmpty()) {
126+
kvh.handle(label, k);
127+
} else {
128+
for (Object obj : v) {
129+
kvh.handle(label, k + "=" + obj);
130+
}
131+
}
132+
});
133+
} else {
134+
kvh.handle(label, value);
135+
}
136+
}
137+
138+
@Override
139+
public Iterator<Object> iterator() {
140+
return values.iterator();
141+
}
142+
143+
void put(Object item) {
144+
checkNotNull(item, "item");
145+
values.add(item);
146+
}
147+
148+
int size() {
149+
return values.size();
150+
}
151+
152+
/**
153+
* Returns a string representation of the contents of the specified value queue.
154+
*
155+
* <ul>
156+
* <li>If the value queue is empty, the method returns an empty string.
157+
* <li>If the value queue contains a single element {@code a}, this method returns {@code
158+
* a.toString()}.
159+
* <li>Otherwise, the contents of the queue are formatted like a {@code List}.
160+
* </ul>
161+
*/
162+
@Override
163+
public String toString() {
164+
// This case shouldn't actually happen unless you use the value queue for storing emitted values
165+
if (values.isEmpty()) {
166+
return "";
167+
}
168+
// Consider using MessageUtils.safeToString() here.
169+
if (values.size() == 1) {
170+
return values.get(0).toString();
171+
}
172+
return values.toString();
173+
}
174+
175+
@Override
176+
public boolean equals(Object o) {
177+
if (this == o) {
178+
return true;
179+
}
180+
if (o == null || getClass() != o.getClass()) {
181+
return false;
182+
}
183+
ValueQueue that = (ValueQueue) o;
184+
return values.equals(that.values);
185+
}
186+
187+
@Override
188+
public int hashCode() {
189+
return Objects.hashCode(values);
190+
}
191+
}

0 commit comments

Comments
 (0)