Skip to content

Commit f880f27

Browse files
authored
Add replay-safe logging (#295)
1 parent 6f996d1 commit f880f27

8 files changed

Lines changed: 963 additions & 1 deletion

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 `createReplaySafeLogger` to suppress orchestration log output during replay ([#295](https://github.com/microsoft/durabletask-java/pull/295)).
23
* Add `getParentInstance()` API to `TaskOrchestrationContext` for discovering parent orchestration info ([#284](https://github.com/microsoft/durabletask-java/pull/284))
34

45
## v1.9.0

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,30 @@ result += ctx.callActivity("SayHello", "Seattle", String.class).await();
1616
return result;
1717
```
1818

19+
### Replay-safe orchestration logging
20+
21+
Orchestrator code re-executes while rebuilding state from history. Wrap an existing
22+
`java.util.logging.Logger` to suppress log output during those replay segments:
23+
24+
```java
25+
Logger logger = ctx.createReplaySafeLogger(
26+
Logger.getLogger(MyOrchestration.class.getName()));
27+
28+
logger.info(() -> "Starting orchestration " + ctx.getInstanceId());
29+
String result = ctx.callActivity("ProcessItem", input, String.class).await();
30+
logger.info(() -> "Activity returned: " + result);
31+
```
32+
33+
In Azure Functions, pass `ExecutionContext.getLogger()` instead of creating a named
34+
logger so the output retains its invocation ID and normal host routing:
35+
36+
```java
37+
Logger logger = ctx.createReplaySafeLogger(executionContext.getLogger());
38+
```
39+
40+
Replay-safe logging suppresses calls made while replaying; it does not guarantee
41+
exactly-once log delivery across failed or retried live orchestration turns.
42+
1943
### Reliable fan-out / fan-in orchestration pattern
2044

2145
```java
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.microsoft.durabletask;
4+
5+
import java.util.Objects;
6+
import java.util.ResourceBundle;
7+
import java.util.function.BooleanSupplier;
8+
import java.util.function.Supplier;
9+
import java.util.logging.Filter;
10+
import java.util.logging.Handler;
11+
import java.util.logging.Level;
12+
import java.util.logging.LogRecord;
13+
import java.util.logging.Logger;
14+
15+
/**
16+
* A {@link Logger} wrapper that suppresses log emission while an orchestration is replaying.
17+
*
18+
* <p>The {@code client} module targets Java 8, but a few of the overrides below wrap
19+
* {@link Logger} methods that were introduced in Java 9:
20+
* <ul>
21+
* <li>{@link #log(Level, Throwable, Supplier)}</li>
22+
* <li>{@link #logp(Level, String, String, Throwable, Supplier)}</li>
23+
* <li>{@link #logrb(Level, String, String, ResourceBundle, String, Throwable)}</li>
24+
* </ul>
25+
* These overrides make the wrapper replay-safe when it runs on a Java 9+ runtime. On a Java 8
26+
* runtime the class still loads and every Java 8 {@link Logger} method remains replay-safe; the
27+
* Java 9+ overrides are simply unreachable, because a Java 8-compiled caller cannot resolve those
28+
* signatures through a {@link Logger}-typed reference and the JDK never routes into them
29+
* internally.
30+
*/
31+
final class ReplaySafeLogger extends Logger {
32+
private final Logger delegate;
33+
private final BooleanSupplier isReplaying;
34+
35+
ReplaySafeLogger(Logger delegate, BooleanSupplier isReplaying) {
36+
super(Objects.requireNonNull(delegate, "delegate").getName(), null);
37+
this.delegate = delegate;
38+
this.isReplaying = Objects.requireNonNull(isReplaying, "isReplaying");
39+
}
40+
41+
@Override
42+
public boolean isLoggable(Level level) {
43+
return this.delegate.isLoggable(level);
44+
}
45+
46+
@Override
47+
public void log(LogRecord record) {
48+
if (!this.isReplaying.getAsBoolean()) {
49+
this.delegate.log(record);
50+
}
51+
}
52+
53+
@Override
54+
public void log(Level level, String message) {
55+
if (!this.isReplaying.getAsBoolean()) {
56+
this.delegate.log(level, message);
57+
}
58+
}
59+
60+
@Override
61+
public void log(Level level, Supplier<String> messageSupplier) {
62+
if (!this.isReplaying.getAsBoolean()) {
63+
this.delegate.log(level, messageSupplier);
64+
}
65+
}
66+
67+
@Override
68+
public void log(Level level, String message, Object parameter) {
69+
if (!this.isReplaying.getAsBoolean()) {
70+
this.delegate.log(level, message, parameter);
71+
}
72+
}
73+
74+
@Override
75+
public void log(Level level, String message, Object[] parameters) {
76+
if (!this.isReplaying.getAsBoolean()) {
77+
this.delegate.log(level, message, parameters);
78+
}
79+
}
80+
81+
@Override
82+
public void log(Level level, String message, Throwable thrown) {
83+
if (!this.isReplaying.getAsBoolean()) {
84+
this.delegate.log(level, message, thrown);
85+
}
86+
}
87+
88+
@Override
89+
public void log(Level level, Throwable thrown, Supplier<String> messageSupplier) {
90+
if (!this.isReplaying.getAsBoolean()) {
91+
this.delegate.log(level, thrown, messageSupplier);
92+
}
93+
}
94+
95+
@Override
96+
public void logp(
97+
Level level,
98+
String sourceClass,
99+
String sourceMethod,
100+
String message) {
101+
if (!this.isReplaying.getAsBoolean()) {
102+
this.delegate.logp(level, sourceClass, sourceMethod, message);
103+
}
104+
}
105+
106+
@Override
107+
public void logp(
108+
Level level,
109+
String sourceClass,
110+
String sourceMethod,
111+
Supplier<String> messageSupplier) {
112+
if (!this.isReplaying.getAsBoolean()) {
113+
this.delegate.logp(level, sourceClass, sourceMethod, messageSupplier);
114+
}
115+
}
116+
117+
@Override
118+
public void logp(
119+
Level level,
120+
String sourceClass,
121+
String sourceMethod,
122+
String message,
123+
Object parameter) {
124+
if (!this.isReplaying.getAsBoolean()) {
125+
this.delegate.logp(level, sourceClass, sourceMethod, message, parameter);
126+
}
127+
}
128+
129+
@Override
130+
public void logp(
131+
Level level,
132+
String sourceClass,
133+
String sourceMethod,
134+
String message,
135+
Object[] parameters) {
136+
if (!this.isReplaying.getAsBoolean()) {
137+
this.delegate.logp(level, sourceClass, sourceMethod, message, parameters);
138+
}
139+
}
140+
141+
@Override
142+
public void logp(
143+
Level level,
144+
String sourceClass,
145+
String sourceMethod,
146+
String message,
147+
Throwable thrown) {
148+
if (!this.isReplaying.getAsBoolean()) {
149+
this.delegate.logp(level, sourceClass, sourceMethod, message, thrown);
150+
}
151+
}
152+
153+
@Override
154+
public void logp(
155+
Level level,
156+
String sourceClass,
157+
String sourceMethod,
158+
Throwable thrown,
159+
Supplier<String> messageSupplier) {
160+
if (!this.isReplaying.getAsBoolean()) {
161+
this.delegate.logp(level, sourceClass, sourceMethod, thrown, messageSupplier);
162+
}
163+
}
164+
165+
@Override
166+
public void logrb(
167+
Level level,
168+
String sourceClass,
169+
String sourceMethod,
170+
String bundleName,
171+
String message) {
172+
if (!this.isReplaying.getAsBoolean()) {
173+
this.delegate.logrb(level, sourceClass, sourceMethod, bundleName, message);
174+
}
175+
}
176+
177+
@Override
178+
public void logrb(
179+
Level level,
180+
String sourceClass,
181+
String sourceMethod,
182+
String bundleName,
183+
String message,
184+
Object parameter) {
185+
if (!this.isReplaying.getAsBoolean()) {
186+
this.delegate.logrb(level, sourceClass, sourceMethod, bundleName, message, parameter);
187+
}
188+
}
189+
190+
@Override
191+
public void logrb(
192+
Level level,
193+
String sourceClass,
194+
String sourceMethod,
195+
String bundleName,
196+
String message,
197+
Object[] parameters) {
198+
if (!this.isReplaying.getAsBoolean()) {
199+
this.delegate.logrb(level, sourceClass, sourceMethod, bundleName, message, parameters);
200+
}
201+
}
202+
203+
@Override
204+
public void logrb(
205+
Level level,
206+
String sourceClass,
207+
String sourceMethod,
208+
ResourceBundle bundle,
209+
String message,
210+
Object... parameters) {
211+
if (!this.isReplaying.getAsBoolean()) {
212+
this.delegate.logrb(level, sourceClass, sourceMethod, bundle, message, parameters);
213+
}
214+
}
215+
216+
@Override
217+
public void logrb(
218+
Level level,
219+
String sourceClass,
220+
String sourceMethod,
221+
String bundleName,
222+
String message,
223+
Throwable thrown) {
224+
if (!this.isReplaying.getAsBoolean()) {
225+
this.delegate.logrb(level, sourceClass, sourceMethod, bundleName, message, thrown);
226+
}
227+
}
228+
229+
@Override
230+
public void logrb(
231+
Level level,
232+
String sourceClass,
233+
String sourceMethod,
234+
ResourceBundle bundle,
235+
String message,
236+
Throwable thrown) {
237+
if (!this.isReplaying.getAsBoolean()) {
238+
this.delegate.logrb(level, sourceClass, sourceMethod, bundle, message, thrown);
239+
}
240+
}
241+
242+
@Override
243+
public String getName() {
244+
return this.delegate.getName();
245+
}
246+
247+
@Override
248+
public ResourceBundle getResourceBundle() {
249+
return this.delegate.getResourceBundle();
250+
}
251+
252+
@Override
253+
public String getResourceBundleName() {
254+
return this.delegate.getResourceBundleName();
255+
}
256+
257+
@Override
258+
public void setResourceBundle(ResourceBundle bundle) {
259+
this.delegate.setResourceBundle(bundle);
260+
}
261+
262+
@Override
263+
public Filter getFilter() {
264+
return this.delegate.getFilter();
265+
}
266+
267+
@Override
268+
public void setFilter(Filter filter) {
269+
this.delegate.setFilter(filter);
270+
}
271+
272+
@Override
273+
public Level getLevel() {
274+
return this.delegate.getLevel();
275+
}
276+
277+
@Override
278+
public void setLevel(Level level) {
279+
this.delegate.setLevel(level);
280+
}
281+
282+
@Override
283+
public Handler[] getHandlers() {
284+
return this.delegate.getHandlers();
285+
}
286+
287+
@Override
288+
public void addHandler(Handler handler) {
289+
this.delegate.addHandler(handler);
290+
}
291+
292+
@Override
293+
public void removeHandler(Handler handler) {
294+
this.delegate.removeHandler(handler);
295+
}
296+
297+
@Override
298+
public Logger getParent() {
299+
return this.delegate.getParent();
300+
}
301+
302+
@Override
303+
public void setParent(Logger parent) {
304+
this.delegate.setParent(parent);
305+
}
306+
307+
@Override
308+
public boolean getUseParentHandlers() {
309+
return this.delegate.getUseParentHandlers();
310+
}
311+
312+
@Override
313+
public void setUseParentHandlers(boolean useParentHandlers) {
314+
this.delegate.setUseParentHandlers(useParentHandlers);
315+
}
316+
}

client/src/main/java/com/microsoft/durabletask/TaskOrchestrationContext.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
import java.util.Arrays;
1111
import java.util.List;
1212
import java.util.Map;
13+
import java.util.Objects;
1314
import java.util.UUID;
15+
import java.util.logging.Logger;
1416
import javax.annotation.Nonnull;
1517

1618
/**
@@ -62,6 +64,28 @@ public interface TaskOrchestrationContext {
6264
*/
6365
boolean getIsReplaying();
6466

67+
/**
68+
* Creates a logger that suppresses output while this orchestration is replaying.
69+
* <p>
70+
* The returned logger does not own {@code logger} or any of its handlers. Replay-safe logging suppresses replay
71+
* output but does not guarantee exactly-once delivery across failed or retried live orchestration turns.
72+
* <p>
73+
* {@link Logger#isLoggable} continues to report the supplied logger's level state during replay. Use
74+
* supplier-based logging methods to avoid expensive message construction while replaying.
75+
* <p>
76+
* Automatic source-class and source-method inference is not preserved by all JUL convenience methods. Use
77+
* {@link Logger#logp} when explicit source metadata is required.
78+
*
79+
* @param logger the configured logger to wrap
80+
* @return a logger that emits through {@code logger} only when not replaying
81+
* @throws NullPointerException if {@code logger} is {@code null}
82+
*/
83+
default Logger createReplaySafeLogger(Logger logger) {
84+
return new ReplaySafeLogger(
85+
Objects.requireNonNull(logger, "logger"),
86+
this::getIsReplaying);
87+
}
88+
6589
/**
6690
* Gets the version of the orchestration that this context represents.
6791
*

0 commit comments

Comments
 (0)