Skip to content

Commit

Permalink
Merge pull request #3005 from cloudflare/jsnell/move-traceid-to-workerd
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell authored Oct 28, 2024
2 parents aefa9f8 + e47c40c commit bbde7e4
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/workerd/io/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ wd_cc_library(
"@capnp-cpp//src/capnp:capnp-rpc",
"@capnp-cpp//src/capnp:capnpc",
"@capnp-cpp//src/kj:kj-async",
"@capnp-cpp//src/kj/compat:kj-http",
"@ssl",
],
)

Expand Down Expand Up @@ -327,3 +329,10 @@ kj_test(
"@capnp-cpp//src/capnp:capnpc",
],
)

kj_test(
src = "trace-test.c++",
deps = [
":trace",
],
)
66 changes: 66 additions & 0 deletions src/workerd/io/trace-test.c++
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) 2017-2022 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

#include <workerd/io/trace.h>

#include <kj/test.h>

namespace workerd::tracing {
namespace {

KJ_TEST("can read trace ID string format") {
KJ_EXPECT(TraceId::fromGoString("z"_kj) == kj::none);

KJ_EXPECT(TraceId::fromGoString("fedcba9876543210z"_kj) == kj::none);

// Go parser supports non-(64 or 128) bit lengths -- unclear if anything cares.
KJ_EXPECT(TraceId(0, 0) == KJ_ASSERT_NONNULL(TraceId::fromGoString(""_kj)));
KJ_EXPECT(TraceId(0x1, 0) == KJ_ASSERT_NONNULL(TraceId::fromGoString("1"_kj)));

KJ_EXPECT(TraceId(0xfedcba9876543210, 0) ==
KJ_ASSERT_NONNULL(TraceId::fromGoString("fedcba9876543210"_kj)));
KJ_EXPECT(TraceId(0xfedcba9876543210, 0) ==
KJ_ASSERT_NONNULL(TraceId::fromGoString("FEDCBA9876543210"_kj)));

KJ_EXPECT(TraceId(0xfedcba9876543210, 0x1) ==
KJ_ASSERT_NONNULL(TraceId::fromGoString("01fedcba9876543210"_kj)));

KJ_EXPECT(TraceId(0xfedcba9876543211, 0xfedcba9876543212) ==
KJ_ASSERT_NONNULL(TraceId::fromGoString("fedcba9876543212fedcba9876543211"_kj)));

KJ_EXPECT(TraceId::fromGoString("01fedcba9876543212fedcba9876543211"_kj) == kj::none);
}

KJ_TEST("can write trace ID string format") {
KJ_EXPECT(TraceId(0x1, 0).toGoString() == "0000000000000001"_kj);
KJ_EXPECT(TraceId(0xfedcba9876543210, 0).toGoString() == "fedcba9876543210"_kj);
KJ_EXPECT(TraceId(0xfedcba9876543210, 0x1).toGoString() == "0000000000000001fedcba9876543210"_kj);

KJ_EXPECT(TraceId(0xfedcba9876543211, 0xfedcba9876543212).toGoString() ==
"fedcba9876543212fedcba9876543211"_kj);
}

KJ_TEST("can read trace ID protobuf format") {
KJ_EXPECT(TraceId::fromProtobuf(""_kjb) == kj::none);
KJ_EXPECT(TraceId::fromProtobuf("z"_kjb) == kj::none);
KJ_EXPECT(TraceId::fromProtobuf("\xfe\xdc\xba\x98\x76\x54\x32\x12\xfe"_kjb) == kj::none);
KJ_EXPECT(
TraceId::fromProtobuf(
"\xfe\xdc\xba\x98\x76\x54\x32\x12\xfe\xdc\xba\x98\x76\x54\x32\x11\x01"_kjb) == kj::none);

KJ_EXPECT(KJ_ASSERT_NONNULL(TraceId::fromProtobuf(
"\xfe\xdc\xba\x98\x76\x54\x32\x12\xfe\xdc\xba\x98\x76\x54\x32\x11"_kjb)) ==
TraceId(0xfedcba9876543211, 0xfedcba9876543212));
}

KJ_TEST("can write trace ID protobuf format") {
KJ_EXPECT(TraceId(0, 0).toProtobuf() ==
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"_kjb);

KJ_EXPECT(TraceId(0xfedcba9876543211, 0xfedcba9876543212).toProtobuf() ==
"\xfe\xdc\xba\x98\x76\x54\x32\x12\xfe\xdc\xba\x98\x76\x54\x32\x11"_kjb);
}

} // namespace
} // namespace workerd::tracing
143 changes: 143 additions & 0 deletions src/workerd/io/trace.c++
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,158 @@
#include <workerd/io/trace.h>
#include <workerd/util/thread-scopes.h>

#include <openssl/rand.h>

#include <capnp/message.h>
#include <capnp/schema.h>
#include <kj/compat/http.h>
#include <kj/debug.h>
#include <kj/time.h>

#include <cstdlib>

namespace workerd {

namespace tracing {
namespace {
kj::Maybe<kj::uint> tryFromHexDigit(char c) {
if ('0' <= c && c <= '9') {
return c - '0';
} else if ('a' <= c && c <= 'f') {
return c - ('a' - 10);
} else if ('A' <= c && c <= 'F') {
return c - ('A' - 10);
} else {
return kj::none;
}
}

kj::Maybe<uint64_t> hexToUint64(kj::ArrayPtr<const char> s) {
KJ_ASSERT(s.size() <= 16);
uint64_t value = 0;
for (auto ch: s) {
KJ_IF_SOME(d, tryFromHexDigit(ch)) {
value = (value << 4) + d;
} else {
return kj::none;
}
}
return value;
}

void addHex(kj::Vector<char>& out, uint64_t v) {
constexpr char HEX_DIGITS[] = "0123456789abcdef";
for (int i = 0; i < 16; ++i) {
out.add(HEX_DIGITS[v >> (64 - 4)]);
v = v << 4;
}
};

void addBigEndianBytes(kj::Vector<byte>& out, uint64_t v) {
for (int i = 0; i < 8; ++i) {
out.add(v >> (64 - 8));
v = v << 8;
}
};
} // namespace

// Reference: https://github.com/jaegertracing/jaeger/blob/e46f8737/model/ids.go#L58
kj::Maybe<TraceId> TraceId::fromGoString(kj::ArrayPtr<const char> s) {
auto n = s.size();
if (n > 32) {
return kj::none;
} else if (n <= 16) {
KJ_IF_SOME(low, hexToUint64(s)) {
return TraceId(low, 0);
}
} else {
KJ_IF_SOME(high, hexToUint64(s.slice(0, n - 16))) {
KJ_IF_SOME(low, hexToUint64(s.slice(n - 16, n))) {
return TraceId(low, high);
}
}
}
return kj::none;
}

// Reference: https://github.com/jaegertracing/jaeger/blob/e46f8737/model/ids.go#L50
kj::String TraceId::toGoString() const {
if (high == 0) {
kj::Vector<char> s(17);
addHex(s, low);
s.add('\0');
return kj::String(s.releaseAsArray());
}
kj::Vector<char> s(33);
addHex(s, high);
addHex(s, low);
s.add('\0');
return kj::String(s.releaseAsArray());
}

// Reference: https://github.com/jaegertracing/jaeger/blob/e46f8737/model/ids.go#L111
kj::Maybe<TraceId> TraceId::fromProtobuf(kj::ArrayPtr<const byte> buf) {
if (buf.size() != 16) {
return kj::none;
}
uint64_t high = 0;
for (auto i: kj::zeroTo(8)) {
high = (high << 8) + buf[i];
}
uint64_t low = 0;
for (auto i: kj::zeroTo(8)) {
low = (low << 8) + buf[i + 8];
}
return TraceId(low, high);
}

// Reference: https://github.com/jaegertracing/jaeger/blob/e46f8737/model/ids.go#L81
kj::Array<byte> TraceId::toProtobuf() const {
kj::Vector<byte> s(16);
addBigEndianBytes(s, high);
addBigEndianBytes(s, low);
return s.releaseAsArray();
}

// Reference https://www.w3.org/TR/trace-context/#trace-id
kj::String TraceId::toW3C() const {
kj::Vector<char> s(32);
addHex(s, high);
addHex(s, low);
return kj::str(s.releaseAsArray());
}

TraceId TraceId::fromEntropy(kj::Maybe<kj::EntropySource&> entropySource) {
if (isPredictableModeForTest()) {
return TraceId(0x2a2a2a2a2a2a2a2a, 0x2a2a2a2a2a2a2a2a);
}

uint64_t low = 0;
uint64_t high = 0;
uint8_t tries = 0;

do {
tries++;
KJ_IF_SOME(entropy, entropySource) {
entropy.generate(kj::arrayPtr(&low, 1).asBytes());
entropy.generate(kj::arrayPtr(&high, 1).asBytes());
} else {
KJ_ASSERT(RAND_bytes(reinterpret_cast<uint8_t*>(&low), sizeof(low)) == 1);
KJ_ASSERT(RAND_bytes(reinterpret_cast<uint8_t*>(&high), sizeof(high)) == 1);
}
// On the extreme off chance that we ended with with zeroes for both,
// let's try again, but only up to three times.
} while (low == 0 && high == 0 && tries < 3);

return TraceId(low, high);
}

kj::String KJ_STRINGIFY(const TraceId& id) {
return id;
}

} // namespace tracing

// Approximately how much external data we allow in a trace before we start ignoring requests. We
// want this number to be big enough to be useful for tracing, but small enough to make it hard to
// DoS the C++ heap -- keeping in mind we can record a trace per handler run during a request.
Expand Down
87 changes: 87 additions & 0 deletions src/workerd/io/trace.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,93 @@ using kj::uint;
typedef rpc::Trace::Log::Level LogLevel;
typedef rpc::Trace::ExecutionModel ExecutionModel;

namespace tracing {
// A 128-bit globally unique trace identifier. This will be used for both
// external and internal tracing. Specifically, for internal tracing, this
// is used to represent tracing IDs for jaeger traces. For external tracing,
// this is used for both the trace ID and invocation ID for tail workers.
class TraceId final {
public:
// A null trace ID. This is only acceptable for use in tests.
constexpr TraceId(decltype(nullptr)): low(0), high(0) {}

// A trace ID with the given low and high values.
constexpr TraceId(uint64_t low, uint64_t high): low(low), high(high) {}

constexpr TraceId(const TraceId& other) = default;
constexpr TraceId& operator=(const TraceId& other) = default;

constexpr TraceId(TraceId&& other): low(other.low), high(other.high) {
other.low = 0;
other.high = 0;
}

constexpr TraceId& operator=(TraceId&& other) {
low = other.low;
high = other.high;
other.low = 0;
other.high = 0;
return *this;
}

constexpr TraceId& operator=(decltype(nullptr)) {
low = 0;
high = 0;
return *this;
}

constexpr bool operator==(const TraceId& other) const {
return low == other.low && high == other.high;
}
constexpr bool operator==(decltype(nullptr)) const {
return low == 0 && high == 0;
}
constexpr operator bool() const {
return low || high;
}

operator kj::String() const {
return toGoString();
}

// Replicates Jaeger go library's string serialization.
kj::String toGoString() const;

// Replicates Jaeger go library's protobuf serialization.
kj::Array<byte> toProtobuf() const;

// Replicates W3C Serialization
kj::String toW3C() const;

// Creates a random Trace Id, optionally usig a given entropy source. If an
// entropy source is not given, then we fallback to using BoringSSL's RAND_bytes.
static TraceId fromEntropy(kj::Maybe<kj::EntropySource&> entropy = kj::none);

// Replicates Jaeger go library's string serialization.
static kj::Maybe<TraceId> fromGoString(kj::ArrayPtr<const char> s);

// Replicates Jaeger go library's protobuf serialization.
static kj::Maybe<TraceId> fromProtobuf(kj::ArrayPtr<const kj::byte> buf);

// A null trace ID. This is really only acceptable for use in tests.
static const TraceId nullId;

inline uint64_t getLow() const {
return low;
}
inline uint64_t getHigh() const {
return high;
}

private:
uint64_t low = 0;
uint64_t high = 0;
};
constexpr TraceId TraceId::nullId = nullptr;

kj::String KJ_STRINGIFY(const TraceId& id);
} // namespace tracing

enum class PipelineLogLevel {
// WARNING: This must be kept in sync with PipelineDef::LogLevel (which is not in the OSS
// release).
Expand Down

0 comments on commit bbde7e4

Please sign in to comment.