From 50337550cc932d4567f8c8d11699346597deed1f Mon Sep 17 00:00:00 2001 From: Jeremy Morrell Date: Thu, 9 Oct 2025 14:12:22 -0700 Subject: [PATCH 1/4] Add instrumentation test in prep for adding attributes --- src/workerd/api/BUILD.bazel | 5 +- .../api/sync-kv-instrumentation-test.js | 96 +++++++++++++++++++ src/workerd/api/sync-kv-test.wd-test | 13 ++- 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 src/workerd/api/sync-kv-instrumentation-test.js diff --git a/src/workerd/api/BUILD.bazel b/src/workerd/api/BUILD.bazel index fec6c94864d..c4097b2faf5 100644 --- a/src/workerd/api/BUILD.bazel +++ b/src/workerd/api/BUILD.bazel @@ -498,7 +498,10 @@ wd_test( wd_test( src = "sync-kv-test.wd-test", args = ["--experimental"], - data = ["sync-kv-test.js"], + data = [ + "sync-kv-instrumentation-test.js", + "sync-kv-test.js", + ], ) wd_test( diff --git a/src/workerd/api/sync-kv-instrumentation-test.js b/src/workerd/api/sync-kv-instrumentation-test.js new file mode 100644 index 00000000000..68069323cd0 --- /dev/null +++ b/src/workerd/api/sync-kv-instrumentation-test.js @@ -0,0 +1,96 @@ +// Copyright (c) 2017-2023 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +import * as assert from 'node:assert'; + +// tailStream is going to be invoked multiple times, but we want to wait +// to run the test until all executions are done. Collect promises for +// each +let invocationPromises = []; +let spans = new Map(); + +export default { + tailStream(event, env, ctx) { + // For each "onset" event, store a promise which we will resolve when + // we receive the equivalent "outcome" event + let resolveFn; + invocationPromises.push( + new Promise((resolve, reject) => { + resolveFn = resolve; + }) + ); + + // Accumulate the span info for easier testing + return (event) => { + // span ids are simple counters for tests, but invocation ID allows us to differentiate them + let spanKey = event.invocationId + event.spanContext.spanId; + switch (event.event.type) { + case 'spanOpen': + console.log(event.invocationId + event.event.spanId); + console.log(event.event.name); + spans.set(event.invocationId + event.event.spanId, { + name: event.event.name, + }); + break; + case 'attributes': { + let span = spans.get(spanKey); + if (span) { + for (let { name, value } of event.event.info) { + span[name] = value; + } + spans.set(spanKey, span); + } + break; + } + case 'spanClose': { + let span = spans.get(spanKey); + span['closed'] = true; + spans.set(spanKey, span); + break; + } + case 'outcome': + resolveFn(); + break; + } + }; + }, +}; + +export const test = { + async test() { + // Wait for all the tailStream executions to finish + await Promise.allSettled(invocationPromises); + + // Recorded streaming tail worker events, in insertion order. + let received = Array.from(spans.values()); + console.log(received); + + // spans emitted by sync-kv-test.js in execution order + let expected = [ + { name: 'durable_object_storage_kv_get', closed: true }, + { name: 'durable_object_storage_kv_put', closed: true }, + { name: 'durable_object_storage_kv_get', closed: true }, + { name: 'durable_object_storage_kv_get', closed: true }, + { name: 'durable_object_storage_kv_put', closed: true }, + { name: 'durable_object_storage_kv_get', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_put', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_storage_kv_delete', closed: true }, + { name: 'durable_object_storage_kv_delete', closed: true }, + { name: 'durable_object_storage_kv_list', closed: true }, + { name: 'durable_object_subrequest', closed: true }, + ]; + + assert.deepStrictEqual(received, expected); + }, +}; diff --git a/src/workerd/api/sync-kv-test.wd-test b/src/workerd/api/sync-kv-test.wd-test index 5eedc1a219b..e233bfedb46 100644 --- a/src/workerd/api/sync-kv-test.wd-test +++ b/src/workerd/api/sync-kv-test.wd-test @@ -4,13 +4,16 @@ const config :Workerd.Config = ( services = [ (name = "main", worker = .mainWorker), (name = "TEST_TMPDIR", disk = (writable = true)), + (name = "tail", worker= .tailWorker) ], ); const mainWorker :Workerd.Worker = ( compatibilityDate = "2025-08-01", - compatibilityFlags = ["enable_ctx_exports", "nodejs_compat"], + compatibilityFlags = ["enable_ctx_exports", "nodejs_compat", "streaming_tail_worker", "tail_worker_user_spans"], + + streamingTails = ["tail"], modules = [ (name = "worker", esModule = embed "sync-kv-test.js"), @@ -24,3 +27,11 @@ const mainWorker :Workerd.Worker = ( durableObjectStorage = (localDisk = "TEST_TMPDIR"), ); + +const tailWorker :Workerd.Worker = ( + modules = [ + (name = "worker", esModule = embed "sync-kv-instrumentation-test.js") + ], + compatibilityDate = "2024-10-14", + compatibilityFlags = ["experimental", "nodejs_compat"], +); From c8fb17ceb3141a246cc5f2a503320d5ac02d1c7b Mon Sep 17 00:00:00 2001 From: Jeremy Morrell Date: Thu, 9 Oct 2025 14:30:51 -0700 Subject: [PATCH 2/4] Add attributes and update tests --- .../api/sync-kv-instrumentation-test.js | 183 +++++++++++++++--- src/workerd/api/sync-kv.c++ | 46 ++++- 2 files changed, 204 insertions(+), 25 deletions(-) diff --git a/src/workerd/api/sync-kv-instrumentation-test.js b/src/workerd/api/sync-kv-instrumentation-test.js index 68069323cd0..5561422d2cb 100644 --- a/src/workerd/api/sync-kv-instrumentation-test.js +++ b/src/workerd/api/sync-kv-instrumentation-test.js @@ -26,8 +26,6 @@ export default { let spanKey = event.invocationId + event.spanContext.spanId; switch (event.event.type) { case 'spanOpen': - console.log(event.invocationId + event.event.spanId); - console.log(event.event.name); spans.set(event.invocationId + event.event.spanId, { name: event.event.name, }); @@ -63,31 +61,168 @@ export const test = { // Recorded streaming tail worker events, in insertion order. let received = Array.from(spans.values()); - console.log(received); // spans emitted by sync-kv-test.js in execution order let expected = [ - { name: 'durable_object_storage_kv_get', closed: true }, - { name: 'durable_object_storage_kv_put', closed: true }, - { name: 'durable_object_storage_kv_get', closed: true }, - { name: 'durable_object_storage_kv_get', closed: true }, - { name: 'durable_object_storage_kv_put', closed: true }, - { name: 'durable_object_storage_kv_get', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_put', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, - { name: 'durable_object_storage_kv_delete', closed: true }, - { name: 'durable_object_storage_kv_delete', closed: true }, - { name: 'durable_object_storage_kv_list', closed: true }, + { + name: 'durable_object_storage_kv_get', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'get', + 'cloudflare.durable_object.kv.query.keys': 'foo', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_put', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'put', + 'cloudflare.durable_object.kv.query.keys': 'foo', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_get', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'get', + 'cloudflare.durable_object.kv.query.keys': 'foo', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_get', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'get', + 'cloudflare.durable_object.kv.query.keys': 'bar', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_put', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'put', + 'cloudflare.durable_object.kv.query.keys': 'bar', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_get', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'get', + 'cloudflare.durable_object.kv.query.keys': 'bar', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.reverse': 'true', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + closed: true, + }, + { + name: 'durable_object_storage_kv_put', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'put', + 'cloudflare.durable_object.kv.query.keys': 'baz', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.prefix': 'ba', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.kv.query.limit': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.reverse': 'true', + 'cloudflare.kv.query.limit': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.start': 'b', + 'cloudflare.durable_object.kv.query.end': 'c', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.start': 'b', + 'cloudflare.durable_object.kv.query.end': 'baz', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.start': 'bar', + 'cloudflare.durable_object.kv.query.end': 'c', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.startAfter': 'bar', + 'cloudflare.durable_object.kv.query.end': 'c', + closed: true, + }, + { + name: 'durable_object_storage_kv_delete', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'delete', + 'cloudflare.durable_object.kv.query.keys': 'qux', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + 'cloudflare.durable_object.kv.response.deleted_count': 0n, + closed: true, + }, + { + name: 'durable_object_storage_kv_delete', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'delete', + 'cloudflare.durable_object.kv.query.keys': 'bar', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + 'cloudflare.durable_object.kv.response.deleted_count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + closed: true, + }, { name: 'durable_object_subrequest', closed: true }, ]; diff --git a/src/workerd/api/sync-kv.c++ b/src/workerd/api/sync-kv.c++ index fe21f445848..ed328315a4f 100644 --- a/src/workerd/api/sync-kv.c++ +++ b/src/workerd/api/sync-kv.c++ @@ -14,6 +14,11 @@ jsg::JsValue SyncKvStorage::get(jsg::Lock& js, kj::String key) { auto userSpan = IoContext::current().makeUserTraceSpan("durable_object_storage_kv_get"_kjc); SqliteKv& sqliteKv = getSqliteKv(js); + userSpan.setTag("db.system.name"_kjc, kj::str("cloudflare-durable-object-sql"_kjc)); + userSpan.setTag("db.operation.name"_kjc, kj::str("get"_kjc)); + userSpan.setTag("cloudflare.durable_object.kv.query.keys"_kjc, kj::str(key)); + userSpan.setTag("cloudflare.durable_object.kv.query.keys.count"_kjc, static_cast(1)); + kj::Maybe result; if (sqliteKv.get(key, [&](kj::ArrayPtr value) { result = deserializeV8Value(js, key, value); })) { @@ -28,6 +33,30 @@ jsg::Ref SyncKvStorage::list( auto userSpan = IoContext::current().makeUserTraceSpan("durable_object_storage_kv_list"_kjc); SqliteKv& sqliteKv = getSqliteKv(js); + userSpan.setTag("db.system.name"_kjc, kj::str("cloudflare-durable-object-sql"_kjc)); + userSpan.setTag("db.operation.name"_kjc, kj::str("list"_kjc)); + + KJ_IF_SOME(o, maybeOptions) { + KJ_IF_SOME(start, o.start) { + userSpan.setTag("cloudflare.durable_object.kv.query.start"_kjc, kj::str(start)); + } + KJ_IF_SOME(startAfter, o.startAfter) { + userSpan.setTag("cloudflare.durable_object.kv.query.startAfter"_kjc, kj::str(startAfter)); + } + KJ_IF_SOME(end, o.end) { + userSpan.setTag("cloudflare.durable_object.kv.query.end"_kjc, kj::str(end)); + } + KJ_IF_SOME(prefix, o.prefix) { + userSpan.setTag("cloudflare.durable_object.kv.query.prefix"_kjc, kj::str(prefix)); + } + KJ_IF_SOME(reverse, o.reverse) { + userSpan.setTag("cloudflare.durable_object.kv.query.reverse"_kjc, kj::str(reverse)); + } + KJ_IF_SOME(limit, o.limit) { + userSpan.setTag("cloudflare.kv.query.limit"_kjc, static_cast(limit)); + } + } + // Convert our options to DurableObjectStorageOperations::ListOptions (which also have the // `allowConcurrency` and `noCache` options, which are irrelevant in the sync interface). auto asyncOptions = kj::mv(maybeOptions).map([&](ListOptions&& options) { @@ -71,6 +100,11 @@ void SyncKvStorage::put(jsg::Lock& js, kj::String key, jsg::JsValue value) { auto userSpan = IoContext::current().makeUserTraceSpan("durable_object_storage_kv_put"_kjc); SqliteKv& sqliteKv = getSqliteKv(js); + userSpan.setTag("db.system.name"_kjc, kj::str("cloudflare-durable-object-sql"_kjc)); + userSpan.setTag("db.operation.name"_kjc, kj::str("put"_kjc)); + userSpan.setTag("cloudflare.durable_object.kv.query.keys"_kjc, kj::str(key)); + userSpan.setTag("cloudflare.durable_object.kv.query.keys.count"_kjc, static_cast(1)); + sqliteKv.put(key, serializeV8Value(js, value)); } @@ -78,7 +112,17 @@ kj::OneOf SyncKvStorage::delete_(jsg::Lock& js, kj::String key) { auto userSpan = IoContext::current().makeUserTraceSpan("durable_object_storage_kv_delete"_kjc); SqliteKv& sqliteKv = getSqliteKv(js); - return sqliteKv.delete_(key); + userSpan.setTag("db.system.name"_kjc, kj::str("cloudflare-durable-object-sql"_kjc)); + userSpan.setTag("db.operation.name"_kjc, kj::str("delete"_kjc)); + userSpan.setTag("cloudflare.durable_object.kv.query.keys"_kjc, kj::str(key)); + userSpan.setTag("cloudflare.durable_object.kv.query.keys.count"_kjc, static_cast(1)); + + auto deleted = sqliteKv.delete_(key); + + userSpan.setTag("cloudflare.durable_object.kv.response.deleted_count"_kjc, + static_cast(deleted ? 1 : 0)); + + return deleted; } } // namespace workerd::api From 067e2fa047da5232a2b56ba0e3dc7040f508bf70 Mon Sep 17 00:00:00 2001 From: Jeremy Morrell Date: Mon, 27 Oct 2025 15:55:57 -0700 Subject: [PATCH 3/4] Changes based on feedback --- .../api/sync-kv-instrumentation-test.js | 392 ++++++++---------- src/workerd/api/sync-kv.c++ | 4 +- 2 files changed, 176 insertions(+), 220 deletions(-) diff --git a/src/workerd/api/sync-kv-instrumentation-test.js b/src/workerd/api/sync-kv-instrumentation-test.js index 5561422d2cb..1e400c858f2 100644 --- a/src/workerd/api/sync-kv-instrumentation-test.js +++ b/src/workerd/api/sync-kv-instrumentation-test.js @@ -2,230 +2,186 @@ // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 import * as assert from 'node:assert'; +import { + createInstrumentationState, + createTailStreamHandler, + runInstrumentationTest, +} from '../../cloudflare/internal/test/instrumentation-test-helper.js'; -// tailStream is going to be invoked multiple times, but we want to wait -// to run the test until all executions are done. Collect promises for -// each -let invocationPromises = []; -let spans = new Map(); +// Create module-level state using the helper +const state = createInstrumentationState(); export default { - tailStream(event, env, ctx) { - // For each "onset" event, store a promise which we will resolve when - // we receive the equivalent "outcome" event - let resolveFn; - invocationPromises.push( - new Promise((resolve, reject) => { - resolveFn = resolve; - }) - ); - - // Accumulate the span info for easier testing - return (event) => { - // span ids are simple counters for tests, but invocation ID allows us to differentiate them - let spanKey = event.invocationId + event.spanContext.spanId; - switch (event.event.type) { - case 'spanOpen': - spans.set(event.invocationId + event.event.spanId, { - name: event.event.name, - }); - break; - case 'attributes': { - let span = spans.get(spanKey); - if (span) { - for (let { name, value } of event.event.info) { - span[name] = value; - } - spans.set(spanKey, span); - } - break; - } - case 'spanClose': { - let span = spans.get(spanKey); - span['closed'] = true; - spans.set(spanKey, span); - break; - } - case 'outcome': - resolveFn(); - break; - } - }; - }, + tailStream: createTailStreamHandler(state), }; export const test = { async test() { - // Wait for all the tailStream executions to finish - await Promise.allSettled(invocationPromises); - - // Recorded streaming tail worker events, in insertion order. - let received = Array.from(spans.values()); - - // spans emitted by sync-kv-test.js in execution order - let expected = [ - { - name: 'durable_object_storage_kv_get', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'get', - 'cloudflare.durable_object.kv.query.keys': 'foo', - 'cloudflare.durable_object.kv.query.keys.count': 1n, - closed: true, - }, - { - name: 'durable_object_storage_kv_put', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'put', - 'cloudflare.durable_object.kv.query.keys': 'foo', - 'cloudflare.durable_object.kv.query.keys.count': 1n, - closed: true, - }, - { - name: 'durable_object_storage_kv_get', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'get', - 'cloudflare.durable_object.kv.query.keys': 'foo', - 'cloudflare.durable_object.kv.query.keys.count': 1n, - closed: true, - }, - { - name: 'durable_object_storage_kv_get', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'get', - 'cloudflare.durable_object.kv.query.keys': 'bar', - 'cloudflare.durable_object.kv.query.keys.count': 1n, - closed: true, - }, - { - name: 'durable_object_storage_kv_put', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'put', - 'cloudflare.durable_object.kv.query.keys': 'bar', - 'cloudflare.durable_object.kv.query.keys.count': 1n, - closed: true, - }, - { - name: 'durable_object_storage_kv_get', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'get', - 'cloudflare.durable_object.kv.query.keys': 'bar', - 'cloudflare.durable_object.kv.query.keys.count': 1n, - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - 'cloudflare.durable_object.kv.query.reverse': 'true', - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - closed: true, - }, - { - name: 'durable_object_storage_kv_put', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'put', - 'cloudflare.durable_object.kv.query.keys': 'baz', - 'cloudflare.durable_object.kv.query.keys.count': 1n, - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - 'cloudflare.durable_object.kv.query.prefix': 'ba', - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - 'cloudflare.kv.query.limit': 1n, - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - 'cloudflare.durable_object.kv.query.reverse': 'true', - 'cloudflare.kv.query.limit': 1n, - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - 'cloudflare.durable_object.kv.query.start': 'b', - 'cloudflare.durable_object.kv.query.end': 'c', - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - 'cloudflare.durable_object.kv.query.start': 'b', - 'cloudflare.durable_object.kv.query.end': 'baz', - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - 'cloudflare.durable_object.kv.query.start': 'bar', - 'cloudflare.durable_object.kv.query.end': 'c', - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - 'cloudflare.durable_object.kv.query.startAfter': 'bar', - 'cloudflare.durable_object.kv.query.end': 'c', - closed: true, - }, - { - name: 'durable_object_storage_kv_delete', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'delete', - 'cloudflare.durable_object.kv.query.keys': 'qux', - 'cloudflare.durable_object.kv.query.keys.count': 1n, - 'cloudflare.durable_object.kv.response.deleted_count': 0n, - closed: true, - }, - { - name: 'durable_object_storage_kv_delete', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'delete', - 'cloudflare.durable_object.kv.query.keys': 'bar', - 'cloudflare.durable_object.kv.query.keys.count': 1n, - 'cloudflare.durable_object.kv.response.deleted_count': 1n, - closed: true, - }, - { - name: 'durable_object_storage_kv_list', - 'db.system.name': 'cloudflare-durable-object-sql', - 'db.operation.name': 'list', - closed: true, - }, - { name: 'durable_object_subrequest', closed: true }, - ]; - - assert.deepStrictEqual(received, expected); + await runInstrumentationTest(state, expectedSpans, { + testName: 'Sync KV instrumentation', + }); }, }; + +const expectedSpans = [ + { + name: 'durable_object_storage_kv_get', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'get', + 'cloudflare.durable_object.kv.query.keys': 'foo', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_put', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'put', + 'cloudflare.durable_object.kv.query.keys': 'foo', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_get', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'get', + 'cloudflare.durable_object.kv.query.keys': 'foo', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_get', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'get', + 'cloudflare.durable_object.kv.query.keys': 'bar', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_put', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'put', + 'cloudflare.durable_object.kv.query.keys': 'bar', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_get', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'get', + 'cloudflare.durable_object.kv.query.keys': 'bar', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.reverse': true, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + closed: true, + }, + { + name: 'durable_object_storage_kv_put', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'put', + 'cloudflare.durable_object.kv.query.keys': 'baz', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.prefix': 'ba', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.limit': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.reverse': true, + 'cloudflare.durable_object.kv.query.limit': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.start': 'b', + 'cloudflare.durable_object.kv.query.end': 'c', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.start': 'b', + 'cloudflare.durable_object.kv.query.end': 'baz', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.start': 'bar', + 'cloudflare.durable_object.kv.query.end': 'c', + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + 'cloudflare.durable_object.kv.query.startAfter': 'bar', + 'cloudflare.durable_object.kv.query.end': 'c', + closed: true, + }, + { + name: 'durable_object_storage_kv_delete', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'delete', + 'cloudflare.durable_object.kv.query.keys': 'qux', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + 'cloudflare.durable_object.kv.response.deleted_count': 0n, + closed: true, + }, + { + name: 'durable_object_storage_kv_delete', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'delete', + 'cloudflare.durable_object.kv.query.keys': 'bar', + 'cloudflare.durable_object.kv.query.keys.count': 1n, + 'cloudflare.durable_object.kv.response.deleted_count': 1n, + closed: true, + }, + { + name: 'durable_object_storage_kv_list', + 'db.system.name': 'cloudflare-durable-object-sql', + 'db.operation.name': 'list', + closed: true, + }, + { name: 'durable_object_subrequest', closed: true }, +]; diff --git a/src/workerd/api/sync-kv.c++ b/src/workerd/api/sync-kv.c++ index ed328315a4f..6bf1a986207 100644 --- a/src/workerd/api/sync-kv.c++ +++ b/src/workerd/api/sync-kv.c++ @@ -50,10 +50,10 @@ jsg::Ref SyncKvStorage::list( userSpan.setTag("cloudflare.durable_object.kv.query.prefix"_kjc, kj::str(prefix)); } KJ_IF_SOME(reverse, o.reverse) { - userSpan.setTag("cloudflare.durable_object.kv.query.reverse"_kjc, kj::str(reverse)); + userSpan.setTag("cloudflare.durable_object.kv.query.reverse"_kjc, reverse); } KJ_IF_SOME(limit, o.limit) { - userSpan.setTag("cloudflare.kv.query.limit"_kjc, static_cast(limit)); + userSpan.setTag("cloudflare.durable_object.kv.query.limit"_kjc, static_cast(limit)); } } From fda1548bc89193a0e53ff71a74931352113600ab Mon Sep 17 00:00:00 2001 From: Jeremy Morrell Date: Tue, 28 Oct 2025 09:44:56 -0700 Subject: [PATCH 4/4] Roll back test changes --- .../api/sync-kv-instrumentation-test.js | 65 +++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/src/workerd/api/sync-kv-instrumentation-test.js b/src/workerd/api/sync-kv-instrumentation-test.js index 1e400c858f2..c17d4e6f1c6 100644 --- a/src/workerd/api/sync-kv-instrumentation-test.js +++ b/src/workerd/api/sync-kv-instrumentation-test.js @@ -2,24 +2,67 @@ // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 import * as assert from 'node:assert'; -import { - createInstrumentationState, - createTailStreamHandler, - runInstrumentationTest, -} from '../../cloudflare/internal/test/instrumentation-test-helper.js'; -// Create module-level state using the helper -const state = createInstrumentationState(); +// tailStream is going to be invoked multiple times, but we want to wait +// to run the test until all executions are done. Collect promises for +// each +let invocationPromises = []; +let spans = new Map(); export default { - tailStream: createTailStreamHandler(state), + tailStream(event, env, ctx) { + // For each "onset" event, store a promise which we will resolve when + // we receive the equivalent "outcome" event + let resolveFn; + invocationPromises.push( + new Promise((resolve, reject) => { + resolveFn = resolve; + }) + ); + + // Accumulate the span info for easier testing + return (event) => { + // span ids are simple counters for tests, but invocation ID allows us to differentiate them + let spanKey = event.invocationId + event.spanContext.spanId; + switch (event.event.type) { + case 'spanOpen': + spans.set(event.invocationId + event.event.spanId, { + name: event.event.name, + }); + break; + case 'attributes': { + let span = spans.get(spanKey); + if (span) { + for (let { name, value } of event.event.info) { + span[name] = value; + } + spans.set(spanKey, span); + } + break; + } + case 'spanClose': { + let span = spans.get(spanKey); + span['closed'] = true; + spans.set(spanKey, span); + break; + } + case 'outcome': + resolveFn(); + break; + } + }; + }, }; export const test = { async test() { - await runInstrumentationTest(state, expectedSpans, { - testName: 'Sync KV instrumentation', - }); + // Wait for all the tailStream executions to finish + await Promise.allSettled(invocationPromises); + + // Recorded streaming tail worker events, in insertion order. + let received = Array.from(spans.values()); + + assert.deepStrictEqual(received, expectedSpans); }, };