Add a logback appender that can be added to a config to export context IDs (#1165)

* Add a logback appender that exports IDs.

* Progress

* Finish

* NOTICE
This commit is contained in:
Anuraag Agrawal 2020-09-04 14:41:41 +09:00 committed by GitHub
parent 5c86bcc20d
commit d21db7b0a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 789 additions and 0 deletions

View File

@ -0,0 +1,19 @@
This product contains a modified part of Armeria, distributed by LINE:
* License:
Copyright 2015 LINE Corporation
LINE Corporation licenses this file to you under the Apache License,
version 2.0 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at:
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
* Homepage: https://armeria.dev

View File

@ -0,0 +1,54 @@
# Logback Integration
This module integrates instrumentation with Logback by injecting the trace ID and span ID from a
mounted span using a custom Logback appender.
To use it, add the module to your application's runtime classpath and add the appender to your
`logback.xml`.
**Maven**
```xml
<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-logback-1.0.0</artifactId>
<version>0.8.0-SNAPSHOT</version>
<scope>runtime</scope>
</dependency>
</dependencies>
```
**Gradle**
```kotlin
dependencies {
runtimeOnly("io.opentelemetry.instrumentation:opentelemetry-logback-1.0.0:0.8.0-SNAPSHOT")
}
```
**logback.xml**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %X{traceId} %X{spanId} %msg%n</pattern>
</encoder>
</appender>
<!-- Just wrap your logging appender, for example ConsoleAppender, with OpenTelemetryAppender -->
<appender name="OTEL" class="io.opentelemetry.instrumentation.logback.v1_0_0.OpenTelemetryAppender">
<appender-ref ref="CONSOLE" />
</appender>
...
</configuration>
```
Logging events will automatically have context information from the span context injected. The
following attributes are available for use:
- `traceId`
- `spanId`
- `traceFlags`

View File

@ -0,0 +1,7 @@
apply from: "$rootDir/gradle/instrumentation-library.gradle"
dependencies {
library group: 'ch.qos.logback', name: 'logback-classic', version: '1.0.0'
testImplementation project(':instrumentation:logback:logback-1.0.0:testing')
}

View File

@ -0,0 +1,128 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.instrumentation.logback.v1_0_0;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.LoggerContextVO;
import java.util.Map;
import org.slf4j.Marker;
final class LoggingEventWrapper implements ILoggingEvent {
private final ILoggingEvent event;
private final Map<String, String> mdcPropertyMap;
private final LoggerContextVO vo;
LoggingEventWrapper(ILoggingEvent event, Map<String, String> mdcPropertyMap) {
this.event = event;
this.mdcPropertyMap = mdcPropertyMap;
final LoggerContextVO oldVo = event.getLoggerContextVO();
if (oldVo != null) {
vo = new LoggerContextVO(oldVo.getName(), mdcPropertyMap, oldVo.getBirthTime());
} else {
vo = null;
}
}
@Override
public Object[] getArgumentArray() {
return event.getArgumentArray();
}
@Override
public Level getLevel() {
return event.getLevel();
}
@Override
public String getLoggerName() {
return event.getLoggerName();
}
@Override
public String getThreadName() {
return event.getThreadName();
}
@Override
public IThrowableProxy getThrowableProxy() {
return event.getThrowableProxy();
}
@Override
public void prepareForDeferredProcessing() {
event.prepareForDeferredProcessing();
}
@Override
public LoggerContextVO getLoggerContextVO() {
return vo;
}
@Override
public String getMessage() {
return event.getMessage();
}
@Override
public long getTimeStamp() {
return event.getTimeStamp();
}
@Override
public StackTraceElement[] getCallerData() {
return event.getCallerData();
}
@Override
public boolean hasCallerData() {
return event.hasCallerData();
}
@Override
public Marker getMarker() {
return event.getMarker();
}
@Override
public String getFormattedMessage() {
return event.getFormattedMessage();
}
@Override
public Map<String, String> getMDCPropertyMap() {
return mdcPropertyMap;
}
/**
* A synonym for {@link #getMDCPropertyMap}.
*
* @deprecated Use {@link #getMDCPropertyMap()}.
*/
@Override
@Deprecated
public Map<String, String> getMdc() {
return event.getMDCPropertyMap();
}
@Override
public String toString() {
return event.toString();
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.instrumentation.logback.v1_0_0;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import ch.qos.logback.core.spi.AppenderAttachable;
import ch.qos.logback.core.spi.AppenderAttachableImpl;
import io.opentelemetry.trace.Span;
import io.opentelemetry.trace.SpanContext;
import io.opentelemetry.trace.TracingContextUtils;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class OpenTelemetryAppender extends UnsynchronizedAppenderBase<ILoggingEvent>
implements AppenderAttachable<ILoggingEvent> {
private final AppenderAttachableImpl<ILoggingEvent> aai = new AppenderAttachableImpl<>();
@Override
protected void append(ILoggingEvent event) {
Span currentSpan = TracingContextUtils.getCurrentSpan();
if (!currentSpan.getContext().isValid()) {
aai.appendLoopOnAppenders(event);
return;
}
Map<String, String> contextData = new HashMap<>();
SpanContext spanContext = currentSpan.getContext();
contextData.put("traceId", spanContext.getTraceId().toLowerBase16());
contextData.put("spanId", spanContext.getSpanId().toLowerBase16());
contextData.put("traceFlags", spanContext.getTraceFlags().toLowerBase16());
Map<String, String> eventContext = event.getMDCPropertyMap();
if (eventContext == null) {
eventContext = contextData;
} else {
eventContext = new UnionMap<>(eventContext, contextData);
}
ILoggingEvent wrapped = new LoggingEventWrapper(event, eventContext);
aai.appendLoopOnAppenders(wrapped);
}
@Override
public void addAppender(Appender<ILoggingEvent> appender) {
aai.addAppender(appender);
}
@Override
public Iterator<Appender<ILoggingEvent>> iteratorForAppenders() {
return aai.iteratorForAppenders();
}
@Override
public Appender<ILoggingEvent> getAppender(String name) {
return aai.getAppender(name);
}
@Override
public boolean isAttached(Appender<ILoggingEvent> appender) {
return aai.isAttached(appender);
}
@Override
public void detachAndStopAllAppenders() {
aai.detachAndStopAllAppenders();
}
@Override
public boolean detachAppender(Appender<ILoggingEvent> appender) {
return aai.detachAppender(appender);
}
@Override
public boolean detachAppender(String name) {
return aai.detachAppender(name);
}
}

View File

@ -0,0 +1,211 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.instrumentation.logback.v1_0_0;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* An immutable view over two maps, with keys resolving from the first map first, or otherwise the
* second if not present in the first.
*/
final class UnionMap<K, V> extends AbstractMap<K, V> {
private final Map<K, V> first;
private final Map<K, V> second;
private int size = -1;
private Set<Entry<K, V>> entrySet;
UnionMap(Map<K, V> first, Map<K, V> second) {
this.first = first;
this.second = second;
}
@Override
public int size() {
if (size >= 0) {
return size;
}
final Map<K, V> a;
final Map<K, V> b;
if (first.size() >= second.size()) {
a = first;
b = second;
} else {
a = second;
b = first;
}
int size = a.size();
if (!b.isEmpty()) {
for (K k : b.keySet()) {
if (!a.containsKey(k)) {
size++;
}
}
}
return this.size = size;
}
@Override
public boolean isEmpty() {
return first.isEmpty() && second.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return first.containsKey(key) || second.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return first.containsValue(value) || second.containsValue(value);
}
@Override
public V get(Object key) {
final V value = first.get(key);
return value != null ? value : second.get(key);
}
@Override
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
@Override
public V remove(Object key) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
@Override
public Set<Entry<K, V>> entrySet() {
if (entrySet != null) {
return entrySet;
}
// Check for dupes first to reduce allocations on the vastly more common case where there aren't
// any.
boolean secondHasDupes = false;
for (Entry<K, V> entry : second.entrySet()) {
if (first.containsKey(entry.getKey())) {
secondHasDupes = true;
break;
}
}
final Set<Entry<K, V>> filteredSecond;
if (!secondHasDupes) {
filteredSecond = second.entrySet();
} else {
filteredSecond = new LinkedHashSet<>();
for (Entry<K, V> entry : second.entrySet()) {
if (!first.containsKey(entry.getKey())) {
filteredSecond.add(entry);
}
}
}
return entrySet =
Collections.unmodifiableSet(new ConcatenatedSet<>(first.entrySet(), filteredSecond));
}
// Member sets must be deduped by caller.
private static final class ConcatenatedSet<T> extends AbstractSet<T> {
private final Set<T> first;
private final Set<T> second;
private final int size;
ConcatenatedSet(Set<T> first, Set<T> second) {
this.first = first;
this.second = second;
size = first.size() + second.size();
}
@Override
public int size() {
return size;
}
@Override
public boolean add(T t) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
throw new UnsupportedOperationException();
}
@Override
public boolean addAll(Collection<? extends T> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean retainAll(Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
final Iterator<T> firstItr = first.iterator();
final Iterator<T> secondItr = second.iterator();
@Override
public boolean hasNext() {
return firstItr.hasNext() || secondItr.hasNext();
}
@Override
public T next() {
if (firstItr.hasNext()) {
return firstItr.next();
}
return secondItr.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.instrumentation.logback.v1_0_0
class LogbackTest extends AbstractLogbackTest {
}

View File

@ -0,0 +1,92 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.instrumentation.logback.v1_0_0
import spock.lang.Specification
class UnionMapTest extends Specification {
def "maps"() {
when:
def union = new UnionMap(first, second)
then:
union['cat'] == 'meow'
union['dog'] == 'bark'
union['foo'] == 'bar'
union['hello'] == 'world'
union['giraffe'] == null
!union.isEmpty()
union.size() == 4
union.containsKey('cat')
union.containsKey('dog')
union.containsKey('foo')
union.containsKey('hello')
!union.containsKey('giraffe')
def set = union.entrySet()
!set.isEmpty()
set.size() == 4
def copy = new ArrayList(set)
copy.size() == 4
where:
first | second
[cat: 'meow', dog: 'bark'] | [foo: 'bar', hello: 'world']
// Overlapping entries in second does not affect the union.
[cat: 'meow', dog: 'bark'] | [foo: 'bar', hello: 'world', cat: 'moo']
}
def "both empty"() {
when:
def union = new UnionMap(Collections.emptyMap(), Collections.emptyMap())
then:
union.isEmpty()
union.size() == 0
union['cat'] == null
def set = union.entrySet()
set.isEmpty()
set.size() == 0
def copy = new ArrayList(set)
copy.size() == 0
}
def "one empty"() {
when:
def union = new UnionMap(first, second)
then:
!union.isEmpty()
union.size() == 1
union['cat'] == 'meow'
union['dog'] == null
def set = union.entrySet()
!set.isEmpty()
set.size() == 1
def copy = new ArrayList(set)
copy.size() == 1
where:
first | second
[cat: 'meow'] | Collections.emptyMap()
Collections.emptyMap() | [cat: 'meow']
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright The OpenTelemetry Authors
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<configuration>
<appender name="LIST" class="ch.qos.logback.core.read.ListAppender" />
<appender name="OTEL" class="io.opentelemetry.instrumentation.logback.v1_0_0.OpenTelemetryAppender">
<appender-ref ref="LIST" />
</appender>
<logger name="test">
<level value="info" />
<appender-ref ref="OTEL" />
</logger>
</configuration>

View File

@ -0,0 +1,31 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply from: "$rootDir/gradle/java.gradle"
dependencies {
compileOnly project(":instrumentation:logback:logback-1.0.0:library")
api project(':testing-common')
api group: 'ch.qos.logback', name: 'logback-classic', version: '1.0.0'
implementation deps.guava
implementation deps.groovy
implementation deps.opentelemetryApi
implementation deps.spock
}

View File

@ -0,0 +1,101 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.instrumentation.logback.v1_0_0
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.read.ListAppender
import io.opentelemetry.auto.test.utils.TraceUtils
import io.opentelemetry.trace.Span
import io.opentelemetry.trace.TracingContextUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import spock.lang.Shared
import spock.lang.Specification
abstract class AbstractLogbackTest extends Specification {
private static final Logger logger = LoggerFactory.getLogger("test")
@Shared
ListAppender<ILoggingEvent> listAppender
def setupSpec() {
ch.qos.logback.classic.Logger logbackLogger = (ch.qos.logback.classic.Logger) logger
listAppender = (logbackLogger.getAppender("OTEL") as OpenTelemetryAppender)
.getAppender("LIST") as ListAppender<ILoggingEvent>
}
def setup() {
listAppender.list.clear()
}
def "no ids when no span"() {
when:
logger.info("log message 1")
logger.info("log message 2")
def events = listAppender.list
then:
events.size() == 2
events[0].message == "log message 1"
events[0].getMDCPropertyMap().get("traceId") == null
events[0].getMDCPropertyMap().get("spanId") == null
events[0].getMDCPropertyMap().get("traceFlags") == null
events[1].message == "log message 2"
events[1].getMDCPropertyMap().get("traceId") == null
events[1].getMDCPropertyMap().get("spanId") == null
events[1].getMDCPropertyMap().get("traceFlags") == null
}
def "ids when span"() {
when:
Span span1
TraceUtils.runUnderTrace("test") {
span1 = TracingContextUtils.currentSpan
logger.info("log message 1")
}
logger.info("log message 2")
Span span2
TraceUtils.runUnderTrace("test 2") {
span2 = TracingContextUtils.currentSpan
logger.info("log message 3")
}
def events = listAppender.list
then:
events.size() == 3
events[0].message == "log message 1"
events[0].getMDCPropertyMap().get("traceId") == span1.context.traceId.toLowerBase16()
events[0].getMDCPropertyMap().get("spanId") == span1.context.spanId.toLowerBase16()
events[0].getMDCPropertyMap().get("traceFlags") == span1.context.traceFlags.toLowerBase16()
events[1].message == "log message 2"
events[1].getMDCPropertyMap().get("traceId") == null
events[1].getMDCPropertyMap().get("spanId") == null
events[1].getMDCPropertyMap().get("traceFlags") == null
events[2].message == "log message 3"
events[2].getMDCPropertyMap().get("traceId") == span2.context.traceId.toLowerBase16()
events[2].getMDCPropertyMap().get("spanId") == span2.context.spanId.toLowerBase16()
events[2].getMDCPropertyMap().get("traceFlags") == span2.context.traceFlags.toLowerBase16()
}
}

View File

@ -121,6 +121,8 @@ include ':instrumentation:lettuce:lettuce-4.0'
include ':instrumentation:lettuce:lettuce-5.0'
include ':instrumentation:lettuce:lettuce-5.1'
include ':instrumentation:log4j:log4j-2.13.2:library'
include ':instrumentation:logback:logback-1.0.0:library'
include ':instrumentation:logback:logback-1.0.0:testing'
include ':instrumentation:mongo:mongo-3.1'
include ':instrumentation:mongo:mongo-3.7'
include ':instrumentation:mongo:mongo-async-3.3'