Skip to content

Commit 792bab3

Browse files
Add clj-otel-adapter-logback module
This commit adds a new module `clj-otel-adapter-logback`, which provides: - A Logback appender that emits log records - A Logback appender that adds Otel context data to Logback MDC. This appender is designed to be used to wrap other appenders, such as the log record emitter appender described above. This is an alternative to OpenTelemetry artifacts `io.opentelemetry.instrumentation/opentelemetry-logback-appender-1.0` and `io.opentelemetry.instrumentation/opentelemetry-logback-mdc-1.0` `clj-otel-adapter-logback` has the same configuration options as the OpenTelemetry counterparts. To use with the OpenTelemetry agent, the agent's Logback instrumentation must be disabled to prevent conflicts. Use these JVM flags: -Dotel.instrumentation.logback-appender.enabled=false -Dotel.instrumentation.logback-mdc.enabled=false `clj-otel-adapter-log4j` provides the same functionality as OpenTelemetry artifacts, but is bound context aware. Using this appender ensures the correct context is attached to each log event by default, when the application uses current or bound context.
1 parent d0f0169 commit 792bab3

File tree

8 files changed

+566
-2
lines changed

8 files changed

+566
-2
lines changed

build.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ clojure -A:deps -T:build help/doc"
4444
"clj-otel-instrumentation-resources"
4545
"clj-otel-instrumentation-runtime-telemetry-java8"
4646
"clj-otel-instrumentation-runtime-telemetry-java17"
47-
"clj-otel-adapter-log4j"])
47+
"clj-otel-adapter-log4j"
48+
"clj-otel-adapter-logback"])
4849

4950
(def ^:private demo-project-paths
5051
["examples/common/anonymise-app"

clj-otel-adapter-log4j/src/steffan_westcott/clj_otel/adapter/log4j.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
(ns steffan-westcott.clj-otel.adapter.log4j
2-
"Appender and context data provider for Log4j."
2+
"Appender and context data provider for Log4j. Has the same configuration and
3+
functionality provided by OpenTelemetry Java instrumentation versions, but
4+
is bound context aware."
35
(:require [steffan-westcott.clj-otel.api.baggage :as baggage]
46
[steffan-westcott.clj-otel.api.logs.log-record :as log-record]
57
[steffan-westcott.clj-otel.api.trace.span :as span]

clj-otel-adapter-logback/deps.edn

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
;!zprint {:width 140}
2+
3+
{:paths ["src" "resources" "target/classes" "java"]
4+
:deps {org.clojure/clojure {:mvn/version "1.10.3"}
5+
com.github.steffan-westcott/clj-otel-api {:local/root "../clj-otel-api"}}
6+
:aliases {:release {:override-deps {com.github.steffan-westcott/clj-otel-api {:mvn/version "0.2.11"}}}
7+
:snapshot {:override-deps {com.github.steffan-westcott/clj-otel-api {:mvn/version "0.2.11-SNAPSHOT"}}}
8+
:provided {:extra-deps {ch.qos.logback/logback-classic {:mvn/version "1.5.21"}
9+
net.logstash.logback/logstash-logback-encoder {:mvn/version "9.0"}}}}}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package steffan_westcott.clj_otel.adapter.logback;
2+
3+
import ch.qos.logback.classic.spi.ILoggingEvent;
4+
import ch.qos.logback.core.UnsynchronizedAppenderBase;
5+
import clojure.java.api.Clojure;
6+
import clojure.lang.IFn;
7+
8+
import java.util.Arrays;
9+
import java.util.Collections;
10+
import java.util.Set;
11+
import java.util.concurrent.ConcurrentLinkedQueue;
12+
import java.util.concurrent.locks.ReadWriteLock;
13+
import java.util.concurrent.locks.ReentrantReadWriteLock;
14+
import java.util.stream.Collectors;
15+
16+
public class CljOtelAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
17+
18+
/**
19+
* If true, CljOtelAppender instances are initialized.
20+
*/
21+
public static volatile boolean initialized = false;
22+
23+
/**
24+
* Delayed emits while appenders remain uninitialized.
25+
*/
26+
public static final ConcurrentLinkedQueue<Object> delayedEmits = new ConcurrentLinkedQueue<>();
27+
28+
/**
29+
* Protects access to <code>initialized</code> and <code>delayedEmits</code>.
30+
*/
31+
public static final ReadWriteLock lock = new ReentrantReadWriteLock();
32+
33+
/**
34+
* If true, include <code>:source</code> location where log record occurred (default: false)
35+
*/
36+
public boolean captureCodeAttributes;
37+
38+
/**
39+
* If true, include thread data (default: false).
40+
*/
41+
public boolean captureExperimentalAttributes;
42+
43+
/**
44+
* If true, include Logback markers as attributes (default: false).
45+
*/
46+
public boolean captureMarkerAttribute;
47+
48+
/**
49+
* If true, include Logback KeyValuePairs as attributes (default: false).
50+
*/
51+
public boolean captureKeyValuePairAttributes;
52+
53+
/**
54+
* If true, include logger context properties as attributes (default: false)
55+
*/
56+
public boolean captureLoggerContext;
57+
58+
/**
59+
* If true, include message arguments as attributes (default: false)
60+
*/
61+
public boolean captureArguments;
62+
63+
/**
64+
* If true, include Logstash markers as attributes (default: false)
65+
*/
66+
public boolean captureLogstashMarkerAttributes;
67+
68+
/**
69+
* If true, include structured Logstash markers as attributes (default: false)
70+
*/
71+
public boolean captureLogstashStructuredArguments;
72+
73+
/**
74+
* if true, include all MDC as attributes (default: false)
75+
*/
76+
public boolean captureAllMdcAttributes;
77+
78+
/**
79+
* Set of keys of MDC to include as attributes, if <code>allMdcAttrs</code> is false (default: no keys)
80+
*/
81+
public Set<String> captureMdcAttributes = Collections.emptySet();
82+
83+
/**
84+
* If true, set log record event name as value of <code>event.name</code> attribute (default: false)
85+
*/
86+
public boolean captureEventName = false;
87+
88+
private static final String NAMESPACE = "steffan-westcott.clj-otel.adapter.logback";
89+
private final IFn append;
90+
91+
public CljOtelAppender() {
92+
Clojure.var("clojure.core", "require").invoke(Clojure.read(NAMESPACE));
93+
append = Clojure.var(NAMESPACE, "append");
94+
}
95+
96+
/**
97+
* Appends a <code>ILoggingEvent</code> by emitting a log record. If
98+
* `CljOtelAppender` instances have been initialized, the log record is emitted
99+
* immediately (but not necessarily exported). Otherwise, the log record is added
100+
* to a queue of delayed emits.
101+
*
102+
* @param event ILoggingEvent to append
103+
*/
104+
@Override
105+
protected void append(ILoggingEvent event) {
106+
append.invoke(this, event);
107+
}
108+
109+
private static Set<String> trimmedSet(String string) {
110+
return (string == null) ? Collections.emptySet() :
111+
Arrays.stream(string.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toSet());
112+
}
113+
114+
public void setCaptureExperimentalAttributes(boolean captureExperimentalAttributes) {
115+
this.captureExperimentalAttributes = captureExperimentalAttributes;
116+
}
117+
118+
public void setCaptureCodeAttributes(boolean captureCodeAttributes) {
119+
this.captureCodeAttributes = captureCodeAttributes;
120+
}
121+
122+
public void setCaptureMarkerAttribute(boolean captureMarkerAttribute) {
123+
this.captureMarkerAttribute = captureMarkerAttribute;
124+
}
125+
126+
public void setCaptureKeyValuePairAttributes(boolean captureKeyValuePairAttributes) {
127+
this.captureKeyValuePairAttributes = captureKeyValuePairAttributes;
128+
}
129+
130+
public void setCaptureLoggerContext(boolean captureLoggerContext) {
131+
this.captureLoggerContext = captureLoggerContext;
132+
}
133+
134+
public void setCaptureArguments(boolean captureArguments) {
135+
this.captureArguments = captureArguments;
136+
}
137+
138+
public void setCaptureLogstashMarkerAttributes(boolean captureLogstashMarkerAttributes) {
139+
this.captureLogstashMarkerAttributes = captureLogstashMarkerAttributes;
140+
}
141+
142+
public void setCaptureLogstashStructuredArguments(boolean captureLogstashStructuredArguments) {
143+
this.captureLogstashStructuredArguments = captureLogstashStructuredArguments;
144+
}
145+
146+
public void setCaptureMdcAttributes(String attributes) {
147+
captureMdcAttributes = trimmedSet(attributes);
148+
captureAllMdcAttributes = captureMdcAttributes.size() == 1 && captureMdcAttributes.contains("*");
149+
}
150+
151+
public void setCaptureEventName(boolean captureEventName) {
152+
this.captureEventName = captureEventName;
153+
}
154+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package steffan_westcott.clj_otel.adapter.logback;
2+
3+
import ch.qos.logback.classic.spi.ILoggingEvent;
4+
import ch.qos.logback.core.Appender;
5+
import ch.qos.logback.core.UnsynchronizedAppenderBase;
6+
import ch.qos.logback.core.spi.AppenderAttachable;
7+
import ch.qos.logback.core.spi.AppenderAttachableImpl;
8+
import clojure.java.api.Clojure;
9+
import clojure.lang.IFn;
10+
11+
import java.util.Iterator;
12+
13+
/**
14+
* Adds clj-otel bound context aware data to Logback MDC for nested appenders.
15+
* Provides trace ID, span ID, trace flags and baggage data.
16+
*/
17+
public class CljOtelMdcAppender extends UnsynchronizedAppenderBase<ILoggingEvent> implements AppenderAttachable<ILoggingEvent> {
18+
19+
private static final String NAMESPACE = "steffan-westcott.clj-otel.adapter.logback";
20+
21+
private final AppenderAttachableImpl<ILoggingEvent> aai = new AppenderAttachableImpl<>();
22+
private final IFn assoc_context_data;
23+
24+
public CljOtelMdcAppender() {
25+
Clojure.var("clojure.core", "require").invoke(Clojure.read(NAMESPACE));
26+
assoc_context_data = Clojure.var(NAMESPACE, "assoc-context-data!");
27+
}
28+
29+
@Override
30+
protected void append(ILoggingEvent event) {
31+
assoc_context_data.invoke(event);
32+
aai.appendLoopOnAppenders(event);
33+
}
34+
35+
@Override
36+
public void addAppender(Appender<ILoggingEvent> appender) {
37+
aai.addAppender(appender);
38+
}
39+
40+
@Override
41+
public Iterator<Appender<ILoggingEvent>> iteratorForAppenders() {
42+
return aai.iteratorForAppenders();
43+
}
44+
45+
@Override
46+
public Appender<ILoggingEvent> getAppender(String name) {
47+
return aai.getAppender(name);
48+
}
49+
50+
@Override
51+
public boolean isAttached(Appender<ILoggingEvent> appender) {
52+
return aai.isAttached(appender);
53+
}
54+
55+
@Override
56+
public void detachAndStopAllAppenders() {
57+
aai.detachAndStopAllAppenders();
58+
}
59+
60+
@Override
61+
public boolean detachAppender(Appender<ILoggingEvent> appender) {
62+
return aai.detachAppender(appender);
63+
}
64+
65+
@Override
66+
public boolean detachAppender(String name) {
67+
return aai.detachAppender(name);
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Args = --initialize-at-build-time=io.opentelemetry.api.logs.Severity

0 commit comments

Comments
 (0)