Skip to content

Commit 6f7c96d

Browse files
PerfectSlayermcculls
authored andcommitted
feat(context): Introduce context helpers API
1 parent 0da31f6 commit 6f7c96d

File tree

4 files changed

+407
-3
lines changed

4 files changed

+407
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package datadog.context;
2+
3+
import static java.lang.Math.max;
4+
import static java.util.Arrays.copyOfRange;
5+
import static java.util.Objects.requireNonNull;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import java.util.function.BinaryOperator;
10+
11+
/**
12+
* Static helpers to manipulate context collections.
13+
*
14+
* <p>Typical usages include:
15+
*
16+
* <pre>{@code
17+
* // Finding a context value from multiple sources:
18+
* Span span = findFirst(spanKey, message, request, CURRENT)
19+
* // Find all context values from different sources:
20+
* List<Error> errors = findAll(errorKey, message, request, CURRENT)
21+
* // Capture multiple contexts in a single one:
22+
* Context aggregate = combine(message, request, CURRENT)
23+
* // Combine multiple contexts into a single one using custom merge rules:
24+
* Context combined = combine(
25+
* (current, next) -> {
26+
* var metric = current.get(metricKey);
27+
* var nextMetric = next.get(metricKey);
28+
* return current.with(metricKey, metric.add(nextMetric));
29+
* }, message, request, CURRENT);
30+
* }</pre>
31+
*
32+
* where {@link #CURRENT} denotes a carrier with the current context.
33+
*/
34+
public final class ContextHelpers {
35+
/** A helper object carrying the {@link Context#current()} context. */
36+
public static final Object CURRENT = new Object();
37+
38+
private ContextHelpers() {}
39+
40+
/**
41+
* Find the first context value from given context carriers.
42+
*
43+
* @param key The key used to store the value.
44+
* @param carriers The carrier to get context and value from.
45+
* @param <T> The type of the value to look for.
46+
* @return The first context value found, {@code null} if not found.
47+
*/
48+
public static <T> T findFirst(ContextKey<T> key, Object... carriers) {
49+
requireNonNull(key, "key cannot be null");
50+
for (Object carrier : carriers) {
51+
requireNonNull(carrier, "carrier cannot be null");
52+
Context context = carrier == CURRENT ? Context.current() : Context.from(carrier);
53+
T value = context.get(key);
54+
if (value != null) {
55+
return value;
56+
}
57+
}
58+
return null;
59+
}
60+
61+
/**
62+
* Find all the context values from the given context carriers.
63+
*
64+
* @param key The key used to store the value.
65+
* @param carriers The carriers to get context and value from.
66+
* @param <T> The type of the values to look for.
67+
* @return A list of all values found, in context order.
68+
*/
69+
public static <T> List<T> findAll(ContextKey<T> key, Object... carriers) {
70+
requireNonNull(key, "key cannot be null");
71+
List<T> values = new ArrayList<>(carriers.length);
72+
for (Object carrier : carriers) {
73+
requireNonNull(carrier, "carrier cannot be null");
74+
Context context = carrier == CURRENT ? Context.current() : Context.from(carrier);
75+
T value = context.get(key);
76+
if (value != null) {
77+
values.add(value);
78+
}
79+
}
80+
return values;
81+
}
82+
83+
/**
84+
* Combine contexts and their values, keeping the first founds.
85+
*
86+
* @param contexts The contexts to combine.
87+
* @return A context containing all the values from all the given context, keeping the first value
88+
* found for a given key.
89+
*/
90+
public static Context combine(Context... contexts) {
91+
return combine(ContextHelpers::combineKeepingFirst, contexts);
92+
}
93+
94+
/**
95+
* Combine multiple contexts into a single one.
96+
*
97+
* @param combiner The context combiner, taking already combined context as first parameter, any
98+
* following one as second parameter, and returning the combined context.
99+
* @param contexts The contexts to combine.
100+
* @return The combined context.
101+
*/
102+
public static Context combine(BinaryOperator<Context> combiner, Context... contexts) {
103+
requireNonNull(combiner, "combiner cannot be null");
104+
Context result = new IndexedContext(new Object[0]);
105+
for (Context context : contexts) {
106+
requireNonNull(context, "context cannot be null");
107+
result = combiner.apply(result, context);
108+
}
109+
return result;
110+
}
111+
112+
private static Context combineKeepingFirst(Context current, Context next) {
113+
if (!(current instanceof IndexedContext)) {
114+
throw new IllegalStateException("Left context is supposed to be an IndexedContext");
115+
}
116+
IndexedContext currentIndexed = (IndexedContext) current;
117+
if (next instanceof EmptyContext) {
118+
return current;
119+
} else if (next instanceof SingletonContext) {
120+
SingletonContext nextSingleton = (SingletonContext) next;
121+
// Check if the single next value is already define in current so next context can be skipped
122+
if (nextSingleton.index < currentIndexed.store.length
123+
&& currentIndexed.store[nextSingleton.index] != null) {
124+
return current;
125+
}
126+
// Always store next value otherwise
127+
Object[] store =
128+
copyOfRange(
129+
currentIndexed.store, 0, max(currentIndexed.store.length, nextSingleton.index + 1));
130+
store[nextSingleton.index] = nextSingleton.value;
131+
return new IndexedContext(store);
132+
} else if (next instanceof IndexedContext) {
133+
IndexedContext nextIndexed = (IndexedContext) next;
134+
// Don't prematurely allocate store. Only allocate if:
135+
// * nextIndexed has more values that currentIndexed,
136+
// so the additional values will always be kept
137+
// * nextIndexed has values that currentIndexed do not have
138+
Object[] store = null;
139+
// Allocate store if nextIndexed has more elements than currentIndexed
140+
if (nextIndexed.store.length > currentIndexed.store.length) {
141+
store = copyOfRange(currentIndexed.store, 0, nextIndexed.store.length);
142+
}
143+
// Apply nextIndexed values if not set in currentIndexed
144+
for (int i = 0; i < currentIndexed.store.length; i++) {
145+
Object nextValue = nextIndexed.store[i];
146+
if (nextValue != null && currentIndexed.store[i] == null) {
147+
if (store == null) {
148+
store = copyOfRange(currentIndexed.store, 0, currentIndexed.store.length);
149+
}
150+
store[i] = nextValue;
151+
}
152+
}
153+
// Apply any additional values from nextIndexed if any
154+
for (int i = currentIndexed.store.length; i < nextIndexed.store.length; i++) {
155+
Object nextValue = nextIndexed.store[i];
156+
if (nextValue != null) {
157+
store[i] = nextValue;
158+
}
159+
}
160+
// If store was not allocated, no value from nextIndexed was taken
161+
return store == null ? current : new IndexedContext(store);
162+
}
163+
throw new IllegalStateException("Unsupported context type: " + next.getClass().getName());
164+
}
165+
}

components/context/src/main/java/datadog/context/IndexedContext.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
/** {@link Context} containing many values. */
1212
@ParametersAreNonnullByDefault
1313
final class IndexedContext implements Context {
14-
private final Object[] store;
14+
final Object[] store;
1515

1616
IndexedContext(Object[] store) {
1717
this.store = store;

components/context/src/main/java/datadog/context/SingletonContext.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
/** {@link Context} containing a single value. */
1111
@ParametersAreNonnullByDefault
1212
final class SingletonContext implements Context {
13-
private final int index;
14-
private final Object value;
13+
final int index;
14+
final Object value;
1515

1616
SingletonContext(int index, Object value) {
1717
this.index = index;

0 commit comments

Comments
 (0)