Skip to content

Commit b8f4bd3

Browse files
Fix logging context propagation
1 parent 45fc2d0 commit b8f4bd3

File tree

13 files changed

+100
-59
lines changed

13 files changed

+100
-59
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ status = warn
344344
appender.console.type = Console
345345
appender.console.name = consoleLogger
346346
appender.console.layout.type = PatternLayout
347-
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %notEmpty{[%X{restateServiceMethod}]}%notEmpty{[%X{restateInvocationId}]} %c - %m%n
347+
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %notEmpty{[%X{restateInvocationTarget}]}%notEmpty{[%X{restateInvocationId}]} %c - %m%n
348348
349349
# Filter out logging during replay
350350
appender.console.filter.replay.type = ContextMapFilter
@@ -367,10 +367,12 @@ rootLogger.appenderRef.stdout.ref = consoleLogger
367367

368368
The SDK injects the following additional metadata to the logging context that can be used for filtering as well:
369369

370-
* `restateServiceMethod`: service and method, e.g. `counter.Counter/Add`.
370+
* `restateInvocationTarget`: invocation target, e.g. `counter.Counter/Add`.
371371
* `restateInvocationId`: Invocation identifier, to be used in Restate observability tools. See https://docs.restate.dev/operate/invocation#invocation-identifier.
372372
* `restateInvocationStatus`: Invocation status, can be `WAITING_START`, `REPLAYING`, `PROCESSING`, `CLOSED`.
373373

374+
When assembling fat-jars, make sure to enable merging META-INF/services files. For more info, see https://github.com/apache/logging-log4j2/issues/2099.
375+
374376
#### Tracing with OpenTelemetry
375377

376378
The SDK can generate additional tracing information on top of what Restate already publishes. See https://docs.restate.dev/operate/monitoring/tracing to configure Restate tracing.

examples/src/main/java/my/restate/sdk/examples/Counter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public Long get(ObjectContext ctx) {
4343

4444
@Handler
4545
public CounterUpdateResult getAndAdd(ObjectContext ctx, Long request) {
46-
LOG.info("Invoked get and add with " + request);
46+
LOG.info("Invoked get and add with {}", request);
4747

4848
long currentValue = ctx.get(TOTAL).orElse(0L);
4949
long newValue = currentValue + request;

examples/src/main/kotlin/my/restate/sdk/examples/CounterKt.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import dev.restate.sdk.http.vertx.RestateHttpEndpointBuilder
1515
import dev.restate.sdk.kotlin.KtSerdes
1616
import dev.restate.sdk.kotlin.ObjectContext
1717
import kotlinx.serialization.Serializable
18+
import org.apache.logging.log4j.LogManager
19+
import org.apache.logging.log4j.Logger
1820

1921
@Serializable data class CounterUpdate(var oldValue: Long, val newValue: Long)
2022

@@ -23,6 +25,7 @@ class CounterKt {
2325

2426
companion object {
2527
private val TOTAL = StateKey.of<Long>("total", KtSerdes.json())
28+
private val LOG: Logger = LogManager.getLogger(CounterKt::class.java)
2629
}
2730

2831
@Handler
@@ -44,6 +47,7 @@ class CounterKt {
4447

4548
@Handler
4649
suspend fun getAndAdd(ctx: ObjectContext, value: Long): CounterUpdate {
50+
LOG.info("Invoked get and add with $value")
4751
val currentValue = ctx.get(TOTAL) ?: 0L
4852
val newValue = currentValue + value
4953
ctx.set(TOTAL, newValue)

examples/src/main/resources/log4j2.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ status = warn
55
appender.console.type = Console
66
appender.console.name = consoleLogger
77
appender.console.layout.type = PatternLayout
8-
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %notEmpty{[%X{restateServiceMethod}]}%notEmpty{[%X{restateInvocationId}]} %c - %m%n
8+
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %notEmpty{[%X{restateInvocationTarget}]}%notEmpty{[%X{restateInvocationId}]} %c - %m%n
99

1010
# Filter out logging during replay
1111
appender.console.filter.replay.type = ContextMapFilter

sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/Component.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import dev.restate.sdk.common.syscalls.*
1717
import kotlin.coroutines.CoroutineContext
1818
import kotlinx.coroutines.CoroutineScope
1919
import kotlinx.coroutines.Dispatchers
20+
import kotlinx.coroutines.asContextElement
2021
import kotlinx.coroutines.launch
2122
import org.apache.logging.log4j.LogManager
2223

@@ -122,7 +123,10 @@ private constructor(
122123
) {
123124
val ctx: Context = ContextImpl(syscalls)
124125

125-
val scope = CoroutineScope(options.coroutineContext)
126+
val scope =
127+
CoroutineScope(
128+
options.coroutineContext +
129+
InvocationHandler.SYSCALLS_THREAD_LOCAL.asContextElement(syscalls))
126130
scope.launch {
127131
val serializedResult: ByteString
128132

sdk-api/src/main/java/dev/restate/sdk/Component.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,19 @@ public HandlerDefinition<Component.Options> toHandlerDefinition() {
128128
@Override
129129
public void handle(
130130
Syscalls syscalls, Component.Options options, SyscallCallback<ByteString> callback) {
131-
options.executor.execute(
131+
// Wrap the executor for setting/unsetting the thread local
132+
Executor wrapped =
133+
runnable ->
134+
options.executor.execute(
135+
() -> {
136+
SYSCALLS_THREAD_LOCAL.set(syscalls);
137+
try {
138+
runnable.run();
139+
} finally {
140+
SYSCALLS_THREAD_LOCAL.remove();
141+
}
142+
});
143+
wrapped.execute(
132144
() -> {
133145
// Any context switching, if necessary, will be done by ResolvedEndpointHandler
134146
Context ctx = new ContextImpl(syscalls);

sdk-common/src/main/java/dev/restate/sdk/common/syscalls/InvocationHandler.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
import com.google.protobuf.ByteString;
1212

1313
public interface InvocationHandler<O> {
14+
/**
15+
* Thread local to store {@link Syscalls}.
16+
*
17+
* <p>Implementations of {@link InvocationHandler} should correctly propagate this thread local in
18+
* order for logging to work correctly. Could be improved if ScopedContext <a
19+
* href="https://github.com/apache/logging-log4j2/pull/2438">will ever be introduced in
20+
* log4j2</a>.
21+
*/
22+
ThreadLocal<Syscalls> SYSCALLS_THREAD_LOCAL = new ThreadLocal<>();
1423

1524
void handle(Syscalls syscalls, O options, SyscallCallback<ByteString> callback);
1625
}

sdk-core/src/main/java/dev/restate/sdk/core/InvocationStateMachine.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class InvocationStateMachine implements InvocationFlow.InvocationProcessor {
3434
private final String componentName;
3535
private final String fullyQualifiedHandlerName;
3636
private final Span span;
37-
private final Consumer<InvocationState> transitionStateObserver;
37+
private final RestateEndpoint.LoggingContextSetter loggingContextSetter;
3838

3939
private volatile InvocationState invocationState = InvocationState.WAITING_START;
4040

@@ -64,11 +64,11 @@ class InvocationStateMachine implements InvocationFlow.InvocationProcessor {
6464
String componentName,
6565
String fullyQualifiedHandlerName,
6666
Span span,
67-
Consumer<InvocationState> transitionStateObserver) {
67+
RestateEndpoint.LoggingContextSetter loggingContextSetter) {
6868
this.componentName = componentName;
6969
this.fullyQualifiedHandlerName = fullyQualifiedHandlerName;
7070
this.span = span;
71-
this.transitionStateObserver = transitionStateObserver;
71+
this.loggingContextSetter = loggingContextSetter;
7272

7373
this.incomingEntriesStateMachine = new IncomingEntriesStateMachine();
7474
this.readyResultStateMachine = new ReadyResultStateMachine();
@@ -190,6 +190,9 @@ void onStartMessage(MessageLite msg) {
190190
Protocol.StartMessage.StateEntry::getKey,
191191
Protocol.StartMessage.StateEntry::getValue)));
192192

193+
// Tracing and logging setup
194+
this.loggingContextSetter.set(
195+
RestateEndpoint.LoggingContextSetter.INVOCATION_ID_KEY, startMessage.getDebugId());
193196
if (this.span.isRecording()) {
194197
span.addEvent(
195198
"Start", Attributes.of(Tracing.RESTATE_INVOCATION_ID, startMessage.getDebugId()));
@@ -687,7 +690,8 @@ private void transitionState(InvocationState newInvocationState) {
687690
}
688691
LOG.debug("Transitioning {} to {}", this, newInvocationState);
689692
this.invocationState = newInvocationState;
690-
this.transitionStateObserver.accept(newInvocationState);
693+
this.loggingContextSetter.set(
694+
RestateEndpoint.LoggingContextSetter.INVOCATION_STATUS_KEY, newInvocationState.toString());
691695
}
692696

693697
private void incrementCurrentIndex() {

sdk-core/src/main/java/dev/restate/sdk/core/ResolvedEndpointHandlerImpl.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ public void start() {
5757
stateMachine.startAndConsumeInput(
5858
SyscallCallback.of(
5959
request -> {
60-
// Set invocation id in logging context
61-
loggingContextSetter.setInvocationId(request.invocationId().toString());
62-
6360
// Prepare Syscalls object
6461
SyscallsInternal syscalls =
6562
this.syscallsExecutor != null
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.core;
10+
11+
import dev.restate.sdk.common.syscalls.InvocationHandler;
12+
import java.util.Collections;
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
import org.apache.logging.log4j.core.util.ContextDataProvider;
16+
17+
/**
18+
* Log4j2 {@link ContextDataProvider} inferring context from {@link
19+
* InvocationHandler#SYSCALLS_THREAD_LOCAL}.
20+
*
21+
* <p>This is used to propagate the context to the user code, such that log statements from the user
22+
* will contain the restate logging context variables.
23+
*/
24+
public class RestateContextDataProvider implements ContextDataProvider {
25+
@Override
26+
public Map<String, String> supplyContextData() {
27+
SyscallsInternal syscalls = (SyscallsInternal) InvocationHandler.SYSCALLS_THREAD_LOCAL.get();
28+
if (syscalls == null) {
29+
return Collections.emptyMap();
30+
}
31+
32+
// We can't use the immutable MapN implementation from Map.of because of
33+
// https://github.com/apache/logging-log4j2/issues/2098
34+
HashMap<String, String> m = new HashMap<>(3);
35+
m.put(
36+
RestateEndpoint.LoggingContextSetter.INVOCATION_ID_KEY,
37+
syscalls.request().invocationId().toString());
38+
m.put(
39+
RestateEndpoint.LoggingContextSetter.INVOCATION_TARGET_KEY,
40+
syscalls.getFullyQualifiedMethodName());
41+
m.put(
42+
RestateEndpoint.LoggingContextSetter.INVOCATION_STATUS_KEY,
43+
syscalls.getInvocationState().toString());
44+
return m;
45+
}
46+
}

0 commit comments

Comments
 (0)