Skip to content

Commit e789b3a

Browse files
authored
Merge pull request #3028 from cloudflare/jsnell/user-span-context
2 parents 7efa08a + c9edd5d commit e789b3a

File tree

5 files changed

+231
-0
lines changed

5 files changed

+231
-0
lines changed

src/workerd/io/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,5 +325,6 @@ kj_test(
325325
src = "trace-test.c++",
326326
deps = [
327327
":trace",
328+
"//src/workerd/util:thread-scopes",
328329
],
329330
)

src/workerd/io/trace-test.c++

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
// https://opensource.org/licenses/Apache-2.0
44

55
#include <workerd/io/trace.h>
6+
#include <workerd/util/thread-scopes.h>
67

8+
#include <capnp/message.h>
79
#include <kj/test.h>
810

911
namespace workerd::tracing {
@@ -62,5 +64,53 @@ KJ_TEST("can write trace ID protobuf format") {
6264
"\xfe\xdc\xba\x98\x76\x54\x32\x12\xfe\xdc\xba\x98\x76\x54\x32\x11"_kjb);
6365
}
6466

67+
KJ_TEST("InvocationSpanContext") {
68+
setPredictableModeForTest();
69+
auto sc = InvocationSpanContext::newForInvocation();
70+
71+
// We can create an InvocationSpanContext...
72+
static constexpr auto kCheck = TraceId(0x2a2a2a2a2a2a2a2a, 0x2a2a2a2a2a2a2a2a);
73+
KJ_EXPECT(sc->getTraceId() == kCheck);
74+
KJ_EXPECT(sc->getInvocationId() == kCheck);
75+
KJ_EXPECT(sc->getSpanId() == 0);
76+
77+
// And serialize that to a capnp struct...
78+
capnp::MallocMessageBuilder builder;
79+
auto root = builder.initRoot<rpc::InvocationSpanContext>();
80+
sc->toCapnp(root);
81+
82+
// Then back again...
83+
auto sc2 = KJ_ASSERT_NONNULL(InvocationSpanContext::fromCapnp(root.asReader()));
84+
KJ_EXPECT(sc2->getTraceId() == kCheck);
85+
KJ_EXPECT(sc2->getInvocationId() == kCheck);
86+
KJ_EXPECT(sc2->getSpanId() == 0);
87+
KJ_EXPECT(sc2->isTrigger());
88+
89+
// The one that has been deserialized from capnp cannot create children...
90+
try {
91+
sc2->newChild();
92+
KJ_FAIL_ASSERT("should not be able to create child span with SpanContext from capnp");
93+
} catch (kj::Exception& ex) {
94+
KJ_EXPECT(ex.getDescription() ==
95+
"expected counter != nullptr; unable to create child spans on this context"_kj);
96+
}
97+
98+
auto sc3 = sc->newChild();
99+
KJ_EXPECT(sc3->getTraceId() == kCheck);
100+
KJ_EXPECT(sc3->getInvocationId() == kCheck);
101+
KJ_EXPECT(sc3->getSpanId() == 1);
102+
103+
auto sc4 = InvocationSpanContext::newForInvocation(sc2);
104+
KJ_EXPECT(sc4->getTraceId() == kCheck);
105+
KJ_EXPECT(sc4->getInvocationId() == kCheck);
106+
KJ_EXPECT(sc4->getSpanId() == 0);
107+
108+
auto& sc5 = KJ_ASSERT_NONNULL(sc4->getParent());
109+
KJ_EXPECT(sc5->getTraceId() == kCheck);
110+
KJ_EXPECT(sc5->getInvocationId() == kCheck);
111+
KJ_EXPECT(sc5->getSpanId() == 0);
112+
KJ_EXPECT(sc5->isTrigger());
113+
}
114+
65115
} // namespace
66116
} // namespace workerd::tracing

src/workerd/io/trace.c++

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,73 @@ kj::String KJ_STRINGIFY(const TraceId& id) {
155155
return id;
156156
}
157157

158+
InvocationSpanContext::InvocationSpanContext(kj::Badge<InvocationSpanContext>,
159+
kj::Maybe<kj::Rc<SpanIdCounter>> counter,
160+
TraceId traceId,
161+
TraceId invocationId,
162+
kj::uint spanId,
163+
kj::Maybe<kj::Rc<InvocationSpanContext>> parentSpanContext)
164+
: counter(kj::mv(counter)),
165+
traceId(kj::mv(traceId)),
166+
invocationId(kj::mv(invocationId)),
167+
spanId(spanId),
168+
parentSpanContext(kj::mv(parentSpanContext)) {}
169+
170+
kj::Rc<InvocationSpanContext> InvocationSpanContext::newChild() {
171+
auto& c = KJ_ASSERT_NONNULL(counter, "unable to create child spans on this context");
172+
return kj::rc<InvocationSpanContext>(kj::Badge<InvocationSpanContext>(), c.addRef(), traceId,
173+
invocationId, c->next(), addRefToThis());
174+
}
175+
176+
kj::Rc<InvocationSpanContext> InvocationSpanContext::newForInvocation(
177+
kj::Maybe<kj::Rc<InvocationSpanContext>&> triggerContext,
178+
kj::Maybe<kj::EntropySource&> entropySource) {
179+
kj::Maybe<kj::Rc<InvocationSpanContext>> parent;
180+
auto traceId = triggerContext
181+
.map([&](kj::Rc<InvocationSpanContext>& ctx) {
182+
parent = ctx.addRef();
183+
return ctx->traceId;
184+
}).orDefault([&] { return TraceId::fromEntropy(entropySource); });
185+
return kj::rc<InvocationSpanContext>(kj::Badge<InvocationSpanContext>(), kj::rc<SpanIdCounter>(),
186+
kj::mv(traceId), TraceId::fromEntropy(entropySource), 0, kj::mv(parent));
187+
}
188+
189+
TraceId TraceId::fromCapnp(rpc::InvocationSpanContext::TraceId::Reader reader) {
190+
return TraceId(reader.getLow(), reader.getHigh());
191+
}
192+
193+
void TraceId::toCapnp(rpc::InvocationSpanContext::TraceId::Builder writer) const {
194+
writer.setLow(low);
195+
writer.setHigh(high);
196+
}
197+
198+
kj::Maybe<kj::Rc<InvocationSpanContext>> InvocationSpanContext::fromCapnp(
199+
rpc::InvocationSpanContext::Reader reader) {
200+
if (!reader.hasTraceId() || !reader.hasInvocationId()) {
201+
// If the reader does not have a traceId or invocationId field then it is
202+
// invalid and we will just ignore it.
203+
return kj::none;
204+
}
205+
206+
auto sc = kj::rc<InvocationSpanContext>(kj::Badge<InvocationSpanContext>(), kj::none,
207+
TraceId::fromCapnp(reader.getTraceId()), TraceId::fromCapnp(reader.getInvocationId()),
208+
reader.getSpanId());
209+
// If the traceId or invocationId are invalid, then we'll ignore them.
210+
if (!sc->getTraceId() || !sc->getInvocationId()) return kj::none;
211+
return kj::mv(sc);
212+
}
213+
214+
void InvocationSpanContext::toCapnp(rpc::InvocationSpanContext::Builder writer) const {
215+
traceId.toCapnp(writer.initTraceId());
216+
invocationId.toCapnp(writer.initInvocationId());
217+
writer.setSpanId(spanId);
218+
kj::mv(getParent()); // Just invalidating the parent. Not moving it anywhere.
219+
}
220+
221+
kj::String KJ_STRINGIFY(const kj::Rc<InvocationSpanContext>& context) {
222+
return kj::str(context->getTraceId(), "-", context->getInvocationId(), "-", context->getSpanId());
223+
}
224+
158225
} // namespace tracing
159226

160227
// Approximately how much external data we allow in a trace before we start ignoring requests. We

src/workerd/io/trace.h

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,116 @@ class TraceId final {
109109
return high;
110110
}
111111

112+
static TraceId fromCapnp(rpc::InvocationSpanContext::TraceId::Reader reader);
113+
void toCapnp(rpc::InvocationSpanContext::TraceId::Builder writer) const;
114+
112115
private:
113116
uint64_t low = 0;
114117
uint64_t high = 0;
115118
};
116119
constexpr TraceId TraceId::nullId = nullptr;
117120

121+
// The InvocationSpanContext is a tuple of a trace id, invocation id, and span id.
122+
// The trace id represents a top-level request and should be shared across all
123+
// invocation spans and events within those spans. The invocation id identifies
124+
// a specific worker invocation. The span id identifies a specific span within an
125+
// invocation. Every invocation of every worker should have an InvocationSpanContext.
126+
// That may or may not have a trigger InvocationSpanContext.
127+
class InvocationSpanContext final: public kj::Refcounted,
128+
public kj::EnableAddRefToThis<InvocationSpanContext> {
129+
public:
130+
// Spans within a InvocationSpanContext are identified by a span id that is a
131+
// monotically increasing number. Every InvocationSpanContext has a root span
132+
// whose ID is zero. Every child span context created within that context will
133+
// have a span id that is one greater than the previously created one.
134+
class SpanIdCounter final: public kj::Refcounted {
135+
public:
136+
SpanIdCounter() = default;
137+
KJ_DISALLOW_COPY_AND_MOVE(SpanIdCounter);
138+
139+
inline kj::uint next() {
140+
#ifdef KJ_DEBUG
141+
static constexpr kj::uint kMax = kj::maxValue;
142+
KJ_ASSERT(id < kMax, "max number of spans exceeded");
143+
#endif
144+
return id++;
145+
}
146+
147+
private:
148+
kj::uint id = 1;
149+
};
150+
151+
// The constructor is public only so kj::rc can see it and create a new instance.
152+
// User code should use the static factory methods or the newChild method.
153+
InvocationSpanContext(kj::Badge<InvocationSpanContext>,
154+
kj::Maybe<kj::Rc<SpanIdCounter>> counter,
155+
TraceId traceId,
156+
TraceId invocationId,
157+
kj::uint spanId = 0,
158+
kj::Maybe<kj::Rc<InvocationSpanContext>> parentSpanContext = kj::none);
159+
KJ_DISALLOW_COPY_AND_MOVE(InvocationSpanContext);
160+
161+
inline const TraceId& getTraceId() const {
162+
return traceId;
163+
}
164+
165+
inline const TraceId& getInvocationId() const {
166+
return invocationId;
167+
}
168+
169+
inline const kj::uint getSpanId() const {
170+
return spanId;
171+
}
172+
173+
inline const kj::Maybe<kj::Rc<InvocationSpanContext>>& getParent() const {
174+
return parentSpanContext;
175+
}
176+
177+
// Creates a new child span. If the current context does not have a counter,
178+
// then this will assert. If isTrigger() is true then it will not have a
179+
// counter.
180+
kj::Rc<InvocationSpanContext> newChild();
181+
182+
// An InvocationSpanContext is a trigger context if it has no counter. This
183+
// generally means the SpanContext was create from a capnp message and
184+
// represents an InvocationSpanContext that was propagated from a parent
185+
// or triggering context.
186+
bool isTrigger() const {
187+
return counter == kj::none;
188+
}
189+
190+
// Creates a new InvocationSpanContext. If the triggerContext is given, then its
191+
// traceId is used as the traceId for the newly created context. Otherwise a new
192+
// traceId is generated. The invocationId is always generated new and the spanId
193+
// will be 0 with no parent span.
194+
static kj::Rc<InvocationSpanContext> newForInvocation(
195+
kj::Maybe<kj::Rc<InvocationSpanContext>&> triggerContext = kj::none,
196+
kj::Maybe<kj::EntropySource&> entropySource = kj::none);
197+
198+
// Creates a new InvocationSpanContext from a capnp message. The returned
199+
// InvocationSpanContext will not be capable of creating child spans and
200+
// is considered only a "trigger" span.
201+
static kj::Maybe<kj::Rc<InvocationSpanContext>> fromCapnp(
202+
rpc::InvocationSpanContext::Reader reader);
203+
void toCapnp(rpc::InvocationSpanContext::Builder writer) const;
204+
205+
private:
206+
// If there is no counter, then child spans cannot be created from
207+
// this InvocationSpanContext.
208+
kj::Maybe<kj::Rc<SpanIdCounter>> counter;
209+
const TraceId traceId;
210+
const TraceId invocationId;
211+
const kj::uint spanId;
212+
213+
// The parentSpanContext can be either a direct parent or a trigger
214+
// context. If it is a trigger context, then it should have the same
215+
// traceId but a different invocationId (unless predictable mode for
216+
// testing is enabled). The isTrigger() should also return true.
217+
const kj::Maybe<kj::Rc<InvocationSpanContext>> parentSpanContext;
218+
};
219+
118220
kj::String KJ_STRINGIFY(const TraceId& id);
221+
kj::String KJ_STRINGIFY(const kj::Rc<InvocationSpanContext>& context);
119222
} // namespace tracing
120223

121224
enum class PipelineLogLevel {

src/workerd/io/worker-interface.capnp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ using import "/capnp/compat/byte-stream.capnp".ByteStream;
1515
using import "/workerd/io/outcome.capnp".EventOutcome;
1616
using import "/workerd/io/script-version.capnp".ScriptVersion;
1717

18+
struct InvocationSpanContext {
19+
struct TraceId {
20+
high @0 :UInt64;
21+
low @1 :UInt64;
22+
}
23+
traceId @0 :TraceId;
24+
invocationId @1 :TraceId;
25+
spanId @2 :UInt32;
26+
}
27+
1828
struct Trace @0x8e8d911203762d34 {
1929
logs @0 :List(Log);
2030
struct Log {

0 commit comments

Comments
 (0)