Skip to content

Commit 064c509

Browse files
committed
feat: add logging hook, rm logging from evaluation
Signed-off-by: Todd Baert <[email protected]>
1 parent 3f5294c commit 064c509

File tree

4 files changed

+263
-7
lines changed

4 files changed

+263
-7
lines changed

src/main/java/dev/openfeature/sdk/OpenFeatureClient.java

-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
137137
hookSupport.afterHooks(type, afterHookContext, details, mergedHooks, hints);
138138
}
139139
} catch (Exception e) {
140-
log.error("Unable to correctly evaluate flag with key '{}'", key, e);
141140
if (details == null) {
142141
details = FlagEvaluationDetails.<T>builder().build();
143142
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package dev.openfeature.sdk.hooks.logging;
2+
3+
import dev.openfeature.sdk.ErrorCode;
4+
import dev.openfeature.sdk.EvaluationContext;
5+
import dev.openfeature.sdk.FlagEvaluationDetails;
6+
import dev.openfeature.sdk.Hook;
7+
import dev.openfeature.sdk.HookContext;
8+
import dev.openfeature.sdk.exceptions.OpenFeatureError;
9+
import java.util.Map;
10+
import java.util.Optional;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.slf4j.spi.LoggingEventBuilder;
13+
14+
/**
15+
* A hook for logging flag evaluations.
16+
* Useful for debugging.
17+
*/
18+
@Slf4j
19+
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED",
20+
justification = "we can ignore return values of chainables (builders) here")
21+
public class LoggingHook implements Hook<Object> {
22+
23+
static final String DOMAIN_KEY = "domain";
24+
static final String PROVIDER_NAME_KEY = "provider name";
25+
static final String FLAG_KEY_KEY = "flag key";
26+
static final String DEFAULT_VALUE_KEY = "default value";
27+
static final String EVALUATION_CONTEXT_KEY = "evaluation context";
28+
static final String ERROR_CODE_KEY = "error code";
29+
static final String ERROR_MESSAGE_KEY = "error message";
30+
static final String REASON_KEY = "reason";
31+
static final String VARIANT_KEY = "variant";
32+
static final String VALUE_KEY = "value";
33+
34+
private boolean includeEvaluationContext;
35+
36+
/**
37+
* Construct a new LoggingHook.
38+
*/
39+
public LoggingHook() {
40+
this(false);
41+
}
42+
43+
/**
44+
* Construct a new LoggingHook.
45+
* @param includeEvaluationContext include a serialized evaluation context in the log message (defaults to false)
46+
*/
47+
public LoggingHook(boolean includeEvaluationContext) {
48+
this.includeEvaluationContext = includeEvaluationContext;
49+
}
50+
51+
@Override
52+
public Optional<EvaluationContext> before(HookContext<Object> hookContext, Map<String, Object> hints) {
53+
LoggingEventBuilder builder = log.atInfo();
54+
addCommonProps(builder, hookContext);
55+
builder.log("Before stage");
56+
57+
return Optional.empty();
58+
}
59+
60+
@Override
61+
public void after(HookContext<Object> hookContext, FlagEvaluationDetails<Object> details,
62+
Map<String, Object> hints) {
63+
LoggingEventBuilder builder = log.atInfo()
64+
.addKeyValue(REASON_KEY, details.getReason())
65+
.addKeyValue(VARIANT_KEY, details.getVariant())
66+
.addKeyValue(VALUE_KEY, details.getValue());
67+
addCommonProps(builder, hookContext);
68+
builder.log("After stage");
69+
}
70+
71+
@Override
72+
public void error(HookContext<Object> hookContext, Exception error, Map<String, Object> hints) {
73+
LoggingEventBuilder builder = log.atError()
74+
.addKeyValue(ERROR_MESSAGE_KEY, error.getMessage());
75+
addCommonProps(builder, hookContext);
76+
ErrorCode errorCode = error instanceof OpenFeatureError ? ((OpenFeatureError) error).getErrorCode() : null;
77+
builder.addKeyValue(ERROR_CODE_KEY, errorCode);
78+
builder.log("Error stage", error);
79+
}
80+
81+
private void addCommonProps(LoggingEventBuilder builder, HookContext<Object> hookContext) {
82+
builder.addKeyValue(DOMAIN_KEY, hookContext.getClientMetadata().getDomain())
83+
.addKeyValue(PROVIDER_NAME_KEY, hookContext.getProviderMetadata().getName())
84+
.addKeyValue(FLAG_KEY_KEY, hookContext.getFlagKey())
85+
.addKeyValue(DEFAULT_VALUE_KEY, hookContext.getDefaultValue());
86+
87+
if (includeEvaluationContext) {
88+
builder.addKeyValue(EVALUATION_CONTEXT_KEY, hookContext.getCtx());
89+
}
90+
}
91+
}

src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java

+5-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import static org.mockito.ArgumentMatchers.any;
1313
import static org.mockito.ArgumentMatchers.argThat;
1414
import static org.mockito.Mockito.mock;
15+
import static org.mockito.Mockito.never;
1516
import static org.mockito.Mockito.spy;
1617
import static org.mockito.Mockito.times;
1718
import static org.mockito.Mockito.verify;
@@ -25,12 +26,10 @@
2526
import org.junit.jupiter.api.AfterEach;
2627
import org.junit.jupiter.api.BeforeEach;
2728
import org.junit.jupiter.api.Test;
28-
import org.mockito.ArgumentMatchers;
2929
import org.mockito.Mockito;
3030
import org.simplify4u.slf4jmock.LoggerMock;
3131
import org.slf4j.Logger;
3232

33-
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
3433
import dev.openfeature.sdk.exceptions.GeneralError;
3534
import dev.openfeature.sdk.fixtures.HookFixtures;
3635
import dev.openfeature.sdk.providers.memory.InMemoryProvider;
@@ -266,17 +265,17 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
266265
assertEquals(TestConstants.BROKEN_MESSAGE, details.getErrorMessage());
267266
}
268267

269-
@Specification(number="1.4.11", text="In the case of abnormal execution, the client SHOULD log an informative error message.")
268+
@Specification(number="1.4.11", text="Methods, functions, or operations on the client SHOULD NOT write log messages.")
270269
@Test void log_on_error() throws NotImplementedException {
271270
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
272271
Client c = api.getClient();
273272
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);
274273

275274
assertEquals(Reason.ERROR.toString(), result.getReason());
276-
Mockito.verify(logger).error(
277-
ArgumentMatchers.contains("Unable to correctly evaluate flag with key"),
275+
Mockito.verify(logger, never()).error(
276+
any(String.class),
278277
any(),
279-
ArgumentMatchers.isA(FlagNotFoundError.class));
278+
any());
280279
}
281280

282281
@Specification(number="1.2.2", text="The client interface MUST define a metadata member or accessor, containing an immutable domain field or accessor of type string, which corresponds to the domain value supplied during client creation. In previous drafts, this property was called name. For backwards compatibility, implementations should consider name an alias to domain.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package dev.openfeature.sdk.hooks.logging;
2+
3+
import static org.mockito.ArgumentMatchers.any;
4+
import static org.mockito.ArgumentMatchers.anyString;
5+
import static org.mockito.ArgumentMatchers.argThat;
6+
import static org.mockito.ArgumentMatchers.contains;
7+
import static org.mockito.Mockito.mock;
8+
import static org.mockito.Mockito.never;
9+
import static org.mockito.Mockito.verify;
10+
import static org.mockito.Mockito.when;
11+
12+
import dev.openfeature.sdk.ClientMetadata;
13+
import dev.openfeature.sdk.ErrorCode;
14+
import dev.openfeature.sdk.EvaluationContext;
15+
import dev.openfeature.sdk.FlagEvaluationDetails;
16+
import dev.openfeature.sdk.FlagValueType;
17+
import dev.openfeature.sdk.HookContext;
18+
import dev.openfeature.sdk.ImmutableContext;
19+
import dev.openfeature.sdk.Metadata;
20+
import dev.openfeature.sdk.exceptions.GeneralError;
21+
import lombok.SneakyThrows;
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
import org.simplify4u.slf4jmock.LoggerMock;
25+
import org.slf4j.Logger;
26+
import org.slf4j.spi.LoggingEventBuilder;
27+
28+
public class LoggingHookTest {
29+
30+
private static final String FLAG_KEY = "some-key";
31+
private static final String DEFAULT_VALUE = "default";
32+
private static final String DOMAIN = "some-domain";
33+
private static final String PROVIDER_NAME = "some-provider";
34+
private static final String REASON = "some-reason";
35+
private static final String VALUE = "some-value";
36+
private static final String VARIANT = "some-variant";
37+
private static final String ERROR_MESSAGE = "some fake error!";
38+
private static final ErrorCode ERROR_CODE = ErrorCode.GENERAL;
39+
40+
private HookContext<Object> hookContext;
41+
private LoggingEventBuilder mockBuilder;
42+
private Logger logger;
43+
44+
@BeforeEach
45+
void each() {
46+
47+
// create a fake hook context
48+
hookContext = HookContext.builder().flagKey(FLAG_KEY).defaultValue(DEFAULT_VALUE)
49+
.clientMetadata(new ClientMetadata() {
50+
@Override
51+
public String getDomain() {
52+
return DOMAIN;
53+
}
54+
}).providerMetadata(new Metadata() {
55+
@Override
56+
public String getName() {
57+
return PROVIDER_NAME;
58+
}
59+
}).type(FlagValueType.BOOLEAN).ctx(new ImmutableContext()).build();
60+
61+
// mock logging
62+
logger = mock(Logger.class);
63+
mockBuilder = mock(LoggingEventBuilder.class);
64+
when(mockBuilder.addKeyValue(anyString(), anyString())).thenReturn(mockBuilder);
65+
when(logger.atInfo()).thenReturn(mockBuilder);
66+
when(logger.atError()).thenReturn(mockBuilder);
67+
LoggerMock.setMock(LoggingHook.class, logger);
68+
}
69+
70+
@SneakyThrows
71+
@Test
72+
void beforeLogsAllPropsExceptEvaluationContext() {
73+
LoggingHook hook = new LoggingHook();
74+
hook.before(hookContext, null);
75+
76+
verify(logger).atInfo();
77+
verifyCommonProps(mockBuilder);
78+
verify(mockBuilder, never()).addKeyValue(anyString(), any(EvaluationContext.class));
79+
verify(mockBuilder).log(argThat((String s) -> s.contains("Before")));
80+
}
81+
82+
@SneakyThrows
83+
@Test
84+
void beforeLogsAllPropsAndEvaluationContext() {
85+
LoggingHook hook = new LoggingHook(true);
86+
hook.before(hookContext, null);
87+
88+
verify(logger).atInfo();
89+
verifyCommonProps(mockBuilder);
90+
verify(mockBuilder).addKeyValue(contains(LoggingHook.EVALUATION_CONTEXT_KEY), any(EvaluationContext.class));
91+
verify(mockBuilder).log(argThat((String s) -> s.contains("Before")));
92+
}
93+
94+
@SneakyThrows
95+
@Test
96+
void afterLogsAllPropsExceptEvaluationContext() {
97+
LoggingHook hook = new LoggingHook();
98+
FlagEvaluationDetails<Object> details = FlagEvaluationDetails.builder().reason(REASON).variant(VARIANT).value(VALUE).build();
99+
hook.after(hookContext, details, null);
100+
101+
verify(logger).atInfo();
102+
verifyAfterProps(mockBuilder);
103+
verifyCommonProps(mockBuilder);
104+
verify(mockBuilder, never()).addKeyValue(anyString(), any(EvaluationContext.class));
105+
verify(mockBuilder).log(argThat((String s) -> s.contains("After")));
106+
}
107+
108+
@SneakyThrows
109+
@Test
110+
void afterLogsAllPropsAndEvaluationContext() {
111+
LoggingHook hook = new LoggingHook(true);
112+
FlagEvaluationDetails<Object> details = FlagEvaluationDetails.builder().reason(REASON).variant(VARIANT).value(VALUE).build();
113+
hook.after(hookContext, details, null);
114+
115+
verify(logger).atInfo();
116+
verifyAfterProps(mockBuilder);
117+
verifyCommonProps(mockBuilder);
118+
verify(mockBuilder).addKeyValue(contains(LoggingHook.EVALUATION_CONTEXT_KEY), any(EvaluationContext.class));
119+
verify(mockBuilder).log(argThat((String s) -> s.contains("After")));
120+
}
121+
122+
@SneakyThrows
123+
@Test
124+
void errorLogsAllPropsExceptEvaluationContext() {
125+
LoggingHook hook = new LoggingHook();
126+
GeneralError error = new GeneralError(ERROR_MESSAGE);
127+
hook.error(hookContext, error, null);
128+
129+
verify(logger).atError();
130+
verifyCommonProps(mockBuilder);
131+
verifyErrorProps(mockBuilder);
132+
verify(mockBuilder, never()).addKeyValue(anyString(), any(EvaluationContext.class));
133+
verify(mockBuilder).log(argThat((String s) -> s.contains("Error")), any(Exception.class));
134+
}
135+
136+
@SneakyThrows
137+
@Test
138+
void errorLogsAllPropsAndEvaluationContext() {
139+
LoggingHook hook = new LoggingHook(true);
140+
GeneralError error = new GeneralError(ERROR_MESSAGE);
141+
hook.error(hookContext, error, null);
142+
143+
verify(logger).atError();
144+
verifyCommonProps(mockBuilder);
145+
verifyErrorProps(mockBuilder);
146+
verify(mockBuilder).addKeyValue(contains(LoggingHook.EVALUATION_CONTEXT_KEY), any(EvaluationContext.class));
147+
verify(mockBuilder).log(argThat((String s) -> s.contains("Error")), any(Exception.class));
148+
}
149+
150+
private void verifyCommonProps(LoggingEventBuilder mockBuilder) {
151+
verify(mockBuilder).addKeyValue(LoggingHook.DOMAIN_KEY, DOMAIN);
152+
verify(mockBuilder).addKeyValue(LoggingHook.FLAG_KEY_KEY, FLAG_KEY);
153+
verify(mockBuilder).addKeyValue(LoggingHook.PROVIDER_NAME_KEY, PROVIDER_NAME);
154+
verify(mockBuilder).addKeyValue(LoggingHook.DEFAULT_VALUE_KEY, DEFAULT_VALUE);
155+
}
156+
157+
private void verifyAfterProps(LoggingEventBuilder mockBuilder) {
158+
verify(mockBuilder).addKeyValue(LoggingHook.REASON_KEY, REASON);
159+
verify(mockBuilder).addKeyValue(LoggingHook.VARIANT_KEY, VARIANT);
160+
verify(mockBuilder).addKeyValue(LoggingHook.VALUE_KEY, VALUE);
161+
}
162+
163+
private void verifyErrorProps(LoggingEventBuilder mockBuilder) {
164+
verify(mockBuilder).addKeyValue(LoggingHook.ERROR_CODE_KEY, ERROR_CODE);
165+
verify(mockBuilder).addKeyValue(LoggingHook.ERROR_MESSAGE_KEY, ERROR_MESSAGE);
166+
}
167+
}

0 commit comments

Comments
 (0)