Skip to content

Commit 5fc5891

Browse files
committed
initial commit
1 parent b546be9 commit 5fc5891

10 files changed

Lines changed: 918 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## Unreleased
2+
* Add replay-safe logging: `createReplaySafeLogger(String)`, `createReplaySafeLogger(Class<?>)`, `getReplaySafeLoggerFactory()`, and `getLoggerFactory()` on `TaskOrchestrationContext`. Wraps SLF4J loggers to suppress log output during orchestration replay. Requires `slf4j-api >= 2.0.0` (new transitive dependency).
23

34
## v1.9.0
45
* Fix entity locking deserialization and add Jackson support for EntityInstanceId/EntityMetadata ([#281](https://github.com/microsoft/durabletask-java/pull/281))

client/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,17 @@ dependencies {
5252
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}"
5353
implementation "io.opentelemetry:opentelemetry-api:${openTelemetryVersion}"
5454
implementation "io.opentelemetry:opentelemetry-context:${openTelemetryVersion}"
55+
api "org.slf4j:slf4j-api:2.0.16"
5556

5657
testImplementation "io.opentelemetry:opentelemetry-sdk:${openTelemetryVersion}"
5758
testImplementation "io.opentelemetry:opentelemetry-sdk-trace:${openTelemetryVersion}"
5859
testImplementation "io.opentelemetry:opentelemetry-sdk-testing:${openTelemetryVersion}"
5960
testImplementation project(':azuremanaged')
6061
testImplementation "com.azure:azure-core:${azureCoreVersion}"
6162
testImplementation "com.azure:azure-identity:${azureIdentityVersion}"
63+
testImplementation 'org.mockito:mockito-core:5.21.0'
64+
testImplementation 'org.mockito:mockito-junit-jupiter:5.21.0'
65+
testImplementation "org.slf4j:slf4j-simple:2.0.16"
6266
}
6367

6468
compileJava {
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.microsoft.durabletask;
4+
5+
import org.slf4j.Logger;
6+
import org.slf4j.Marker;
7+
import org.slf4j.event.Level;
8+
import org.slf4j.spi.LoggingEventBuilder;
9+
import org.slf4j.spi.NOPLoggingEventBuilder;
10+
11+
/**
12+
* An SLF4J {@link Logger} wrapper that suppresses log output during orchestration replay.
13+
*
14+
* <p>Traditional logging methods ({@code info}, {@code debug}, etc.) and the SLF4J 2.x fluent API
15+
* ({@code atInfo()}, {@code atDebug()}, etc.) are both gated on
16+
* {@link TaskOrchestrationContext#getIsReplaying()}. The {@code isXxxEnabled()} family of methods
17+
* always passes through to the underlying logger unchanged.
18+
*
19+
* <p>This mirrors the {@code ReplaySafeLogger} nested class in the modern .NET
20+
* {@code TaskOrchestrationContext}.
21+
*/
22+
final class ReplaySafeLogger implements Logger {
23+
24+
private final TaskOrchestrationContext context;
25+
private final Logger inner;
26+
27+
ReplaySafeLogger(TaskOrchestrationContext context, Logger inner) {
28+
Helpers.throwIfArgumentNull(context, "context");
29+
Helpers.throwIfArgumentNull(inner, "inner");
30+
this.context = context;
31+
this.inner = inner;
32+
}
33+
34+
// -----------------------------------------------------------------------
35+
// getName — always pass through
36+
// -----------------------------------------------------------------------
37+
38+
@Override
39+
public String getName() {
40+
return inner.getName();
41+
}
42+
43+
// -----------------------------------------------------------------------
44+
// isXxxEnabled — always pass through (matches .NET IsEnabled behavior)
45+
// -----------------------------------------------------------------------
46+
47+
@Override public boolean isTraceEnabled() { return inner.isTraceEnabled(); }
48+
@Override public boolean isTraceEnabled(Marker marker) { return inner.isTraceEnabled(marker); }
49+
@Override public boolean isDebugEnabled() { return inner.isDebugEnabled(); }
50+
@Override public boolean isDebugEnabled(Marker marker) { return inner.isDebugEnabled(marker); }
51+
@Override public boolean isInfoEnabled() { return inner.isInfoEnabled(); }
52+
@Override public boolean isInfoEnabled(Marker marker) { return inner.isInfoEnabled(marker); }
53+
@Override public boolean isWarnEnabled() { return inner.isWarnEnabled(); }
54+
@Override public boolean isWarnEnabled(Marker marker) { return inner.isWarnEnabled(marker); }
55+
@Override public boolean isErrorEnabled() { return inner.isErrorEnabled(); }
56+
@Override public boolean isErrorEnabled(Marker marker) { return inner.isErrorEnabled(marker); }
57+
58+
// -----------------------------------------------------------------------
59+
// SLF4J 2.x fluent API — gate via makeLoggingEventBuilder
60+
//
61+
// atInfo(), atDebug(), atWarn(), atError(), atTrace(), atLevel() are
62+
// default methods on Logger that all delegate to makeLoggingEventBuilder().
63+
// They bypass the traditional info/debug/warn/error/trace methods
64+
// entirely, so we MUST override this single entry point to prevent
65+
// fluent-API calls from escaping replay safety.
66+
// -----------------------------------------------------------------------
67+
68+
@Override
69+
public LoggingEventBuilder makeLoggingEventBuilder(Level level) {
70+
if (context.getIsReplaying()) {
71+
return NOPLoggingEventBuilder.singleton();
72+
}
73+
return inner.makeLoggingEventBuilder(level);
74+
}
75+
76+
// -----------------------------------------------------------------------
77+
// trace — gated on !isReplaying
78+
// -----------------------------------------------------------------------
79+
80+
@Override public void trace(String msg) { if (!context.getIsReplaying()) inner.trace(msg); }
81+
@Override public void trace(String format, Object arg) { if (!context.getIsReplaying()) inner.trace(format, arg); }
82+
@Override public void trace(String format, Object arg1, Object arg2) { if (!context.getIsReplaying()) inner.trace(format, arg1, arg2); }
83+
@Override public void trace(String format, Object... arguments) { if (!context.getIsReplaying()) inner.trace(format, arguments); }
84+
@Override public void trace(String msg, Throwable t) { if (!context.getIsReplaying()) inner.trace(msg, t); }
85+
@Override public void trace(Marker marker, String msg) { if (!context.getIsReplaying()) inner.trace(marker, msg); }
86+
@Override public void trace(Marker marker, String format, Object arg) { if (!context.getIsReplaying()) inner.trace(marker, format, arg); }
87+
@Override public void trace(Marker marker, String format, Object arg1, Object arg2) { if (!context.getIsReplaying()) inner.trace(marker, format, arg1, arg2); }
88+
@Override public void trace(Marker marker, String format, Object... argArray) { if (!context.getIsReplaying()) inner.trace(marker, format, argArray); }
89+
@Override public void trace(Marker marker, String msg, Throwable t) { if (!context.getIsReplaying()) inner.trace(marker, msg, t); }
90+
91+
// -----------------------------------------------------------------------
92+
// debug — gated on !isReplaying
93+
// -----------------------------------------------------------------------
94+
95+
@Override public void debug(String msg) { if (!context.getIsReplaying()) inner.debug(msg); }
96+
@Override public void debug(String format, Object arg) { if (!context.getIsReplaying()) inner.debug(format, arg); }
97+
@Override public void debug(String format, Object arg1, Object arg2) { if (!context.getIsReplaying()) inner.debug(format, arg1, arg2); }
98+
@Override public void debug(String format, Object... arguments) { if (!context.getIsReplaying()) inner.debug(format, arguments); }
99+
@Override public void debug(String msg, Throwable t) { if (!context.getIsReplaying()) inner.debug(msg, t); }
100+
@Override public void debug(Marker marker, String msg) { if (!context.getIsReplaying()) inner.debug(marker, msg); }
101+
@Override public void debug(Marker marker, String format, Object arg) { if (!context.getIsReplaying()) inner.debug(marker, format, arg); }
102+
@Override public void debug(Marker marker, String format, Object arg1, Object arg2) { if (!context.getIsReplaying()) inner.debug(marker, format, arg1, arg2); }
103+
@Override public void debug(Marker marker, String format, Object... arguments) { if (!context.getIsReplaying()) inner.debug(marker, format, arguments); }
104+
@Override public void debug(Marker marker, String msg, Throwable t) { if (!context.getIsReplaying()) inner.debug(marker, msg, t); }
105+
106+
// -----------------------------------------------------------------------
107+
// info — gated on !isReplaying
108+
// -----------------------------------------------------------------------
109+
110+
@Override public void info(String msg) { if (!context.getIsReplaying()) inner.info(msg); }
111+
@Override public void info(String format, Object arg) { if (!context.getIsReplaying()) inner.info(format, arg); }
112+
@Override public void info(String format, Object arg1, Object arg2) { if (!context.getIsReplaying()) inner.info(format, arg1, arg2); }
113+
@Override public void info(String format, Object... arguments) { if (!context.getIsReplaying()) inner.info(format, arguments); }
114+
@Override public void info(String msg, Throwable t) { if (!context.getIsReplaying()) inner.info(msg, t); }
115+
@Override public void info(Marker marker, String msg) { if (!context.getIsReplaying()) inner.info(marker, msg); }
116+
@Override public void info(Marker marker, String format, Object arg) { if (!context.getIsReplaying()) inner.info(marker, format, arg); }
117+
@Override public void info(Marker marker, String format, Object arg1, Object arg2) { if (!context.getIsReplaying()) inner.info(marker, format, arg1, arg2); }
118+
@Override public void info(Marker marker, String format, Object... arguments) { if (!context.getIsReplaying()) inner.info(marker, format, arguments); }
119+
@Override public void info(Marker marker, String msg, Throwable t) { if (!context.getIsReplaying()) inner.info(marker, msg, t); }
120+
121+
// -----------------------------------------------------------------------
122+
// warn — gated on !isReplaying
123+
// -----------------------------------------------------------------------
124+
125+
@Override public void warn(String msg) { if (!context.getIsReplaying()) inner.warn(msg); }
126+
@Override public void warn(String format, Object arg) { if (!context.getIsReplaying()) inner.warn(format, arg); }
127+
@Override public void warn(String format, Object arg1, Object arg2) { if (!context.getIsReplaying()) inner.warn(format, arg1, arg2); }
128+
@Override public void warn(String format, Object... arguments) { if (!context.getIsReplaying()) inner.warn(format, arguments); }
129+
@Override public void warn(String msg, Throwable t) { if (!context.getIsReplaying()) inner.warn(msg, t); }
130+
@Override public void warn(Marker marker, String msg) { if (!context.getIsReplaying()) inner.warn(marker, msg); }
131+
@Override public void warn(Marker marker, String format, Object arg) { if (!context.getIsReplaying()) inner.warn(marker, format, arg); }
132+
@Override public void warn(Marker marker, String format, Object arg1, Object arg2) { if (!context.getIsReplaying()) inner.warn(marker, format, arg1, arg2); }
133+
@Override public void warn(Marker marker, String format, Object... arguments) { if (!context.getIsReplaying()) inner.warn(marker, format, arguments); }
134+
@Override public void warn(Marker marker, String msg, Throwable t) { if (!context.getIsReplaying()) inner.warn(marker, msg, t); }
135+
136+
// -----------------------------------------------------------------------
137+
// error — gated on !isReplaying
138+
// -----------------------------------------------------------------------
139+
140+
@Override public void error(String msg) { if (!context.getIsReplaying()) inner.error(msg); }
141+
@Override public void error(String format, Object arg) { if (!context.getIsReplaying()) inner.error(format, arg); }
142+
@Override public void error(String format, Object arg1, Object arg2) { if (!context.getIsReplaying()) inner.error(format, arg1, arg2); }
143+
@Override public void error(String format, Object... arguments) { if (!context.getIsReplaying()) inner.error(format, arguments); }
144+
@Override public void error(String msg, Throwable t) { if (!context.getIsReplaying()) inner.error(msg, t); }
145+
@Override public void error(Marker marker, String msg) { if (!context.getIsReplaying()) inner.error(marker, msg); }
146+
@Override public void error(Marker marker, String format, Object arg) { if (!context.getIsReplaying()) inner.error(marker, format, arg); }
147+
@Override public void error(Marker marker, String format, Object arg1, Object arg2) { if (!context.getIsReplaying()) inner.error(marker, format, arg1, arg2); }
148+
@Override public void error(Marker marker, String format, Object... arguments) { if (!context.getIsReplaying()) inner.error(marker, format, arguments); }
149+
@Override public void error(Marker marker, String msg, Throwable t) { if (!context.getIsReplaying()) inner.error(marker, msg, t); }
150+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.microsoft.durabletask;
4+
5+
import org.slf4j.ILoggerFactory;
6+
import org.slf4j.Logger;
7+
8+
/**
9+
* An {@link ILoggerFactory} that produces replay-safe loggers backed by the context's
10+
* underlying {@link TaskOrchestrationContext#getLoggerFactory() logger factory}.
11+
*
12+
* <p>Mirrors the {@code ReplaySafeLoggerFactoryImpl} nested class in the modern .NET
13+
* {@code TaskOrchestrationContext}.
14+
*/
15+
final class ReplaySafeLoggerFactory implements ILoggerFactory {
16+
17+
private final TaskOrchestrationContext context;
18+
19+
ReplaySafeLoggerFactory(TaskOrchestrationContext context) {
20+
Helpers.throwIfArgumentNull(context, "context");
21+
this.context = context;
22+
}
23+
24+
/**
25+
* Returns the factory underlying this wrapper. Used by {@link ReplaySafeLoggers#unwrap}
26+
* to walk past nested replay-safe wrappers (wrapper-context delegation pattern).
27+
*/
28+
ILoggerFactory underlying() {
29+
return context.getLoggerFactory();
30+
}
31+
32+
@Override
33+
public Logger getLogger(String name) {
34+
return new ReplaySafeLogger(context, ReplaySafeLoggers.unwrap(context).getLogger(name));
35+
}
36+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.microsoft.durabletask;
4+
5+
import org.slf4j.ILoggerFactory;
6+
7+
/**
8+
* Utility class for unwrapping nested {@link ReplaySafeLoggerFactory} instances.
9+
*
10+
* <p>Mirrors {@code TaskOrchestrationContext.GetUnwrappedLoggerFactory()} in the modern .NET SDK.
11+
* When a wrapper context delegates {@code getLoggerFactory()} to
12+
* {@code inner.getReplaySafeLoggerFactory()}, the returned factory is already a
13+
* {@link ReplaySafeLoggerFactory}. Without unwrapping, loggers produced from it would be
14+
* double-wrapped with redundant replay-safe checks.
15+
*/
16+
final class ReplaySafeLoggers {
17+
18+
private static final int MAX_UNWRAP_DEPTH = 10;
19+
20+
private ReplaySafeLoggers() {
21+
// utility class
22+
}
23+
24+
/**
25+
* Returns the underlying {@link ILoggerFactory} for the given context, walking past any
26+
* nested {@link ReplaySafeLoggerFactory} wrappers.
27+
*
28+
* @param context the orchestration context whose logger factory to unwrap
29+
* @return the first non-replay-safe-wrapped factory in the chain
30+
* @throws IllegalStateException if more than {@value #MAX_UNWRAP_DEPTH} levels of wrapping
31+
* are encountered (cycle protection)
32+
*/
33+
static ILoggerFactory unwrap(TaskOrchestrationContext context) {
34+
Helpers.throwIfArgumentNull(context, "context");
35+
ILoggerFactory factory = context.getLoggerFactory();
36+
int depth = 0;
37+
while (factory instanceof ReplaySafeLoggerFactory) {
38+
if (++depth > MAX_UNWRAP_DEPTH) {
39+
throw new IllegalStateException(
40+
"Maximum unwrap depth exceeded while resolving the underlying ILoggerFactory. " +
41+
"Ensure the wrapper's getLoggerFactory() delegates to the inner context's " +
42+
"getReplaySafeLoggerFactory() (e.g., 'inner.getReplaySafeLoggerFactory()'), " +
43+
"not 'this.getReplaySafeLoggerFactory()'.");
44+
}
45+
factory = ((ReplaySafeLoggerFactory) factory).underlying();
46+
}
47+
return factory;
48+
}
49+
}

0 commit comments

Comments
 (0)