Skip to content

feat(server-utils): Add @opentelemetry/instrumentation-koa orchestrion integration#22146

Open
s1gr1d wants to merge 2 commits into
developfrom
sig/rewrite-koa-orchestrion
Open

feat(server-utils): Add @opentelemetry/instrumentation-koa orchestrion integration#22146
s1gr1d wants to merge 2 commits into
developfrom
sig/rewrite-koa-orchestrion

Conversation

@s1gr1d

@s1gr1d s1gr1d commented Jul 9, 2026

Copy link
Copy Markdown
Member

Adds koaChannelIntegration in @sentry/server-utils for injecting orchestrion channels into koa.

A subscriber wraps each registered layer in a span-creating proxy.
Span-helpers are ported from the vendored instrumentation, preserving span names (but adapting to new conventions - check todo comments).

One thing to know for review: we instrument koa-compose, not use from koa. koa's use lives in koa's main entry (lib/application.js), and transforming a package's main entry forces its top-level require chain through Node's require(esm) bridge, which throws on Node < 24.13.

  1. Orchestrion instruments by rewriting a module's source at load time (via the ESM load hook).
  2. use lives in koa's main entry (lib/application.js), so to instrument it we transform that file. But transforming a main entry pulls its whole top-level require chain into the loader's handling --> and that changes how those requires are loaded (through the ESM→CommonJS translator, not the normal sync require path).
  3. koa is CJS (but support ESM). When importing koa, it loads a shim that loads the CJS code: import 'koa'dist/koa.mjs (a ESM shim) → import '../lib/application.js' (CJS). That CJS entry has a top-level require('is-generator-function').
  4. is-generator-functionrequire('generator-function'), and generator-function points at an .mjs file, which in turn imports ./index.js.
    --> So the top-level require in koa's application.js (CJS) becomes require(esm) of an ESM file importing a CJS file.
  5. On Node < 24.13 (we pin 20.19.5), the loader can't pre-link this dual-package shape into the require(esm) cache, so it throws request for './index.js' is not in cache.

The failing chain:

koa/lib/application.js (CJS)
  └─ require('is-generator-function')        (CJS)
       └─ require('generator-function')      → require(esm) → require.mjs (ESM)
            └─ import './index.js'            (ESM importing CJS)

koa-compose is koa's zero-dependency dispatch engine, so it's safe to transform, and compose(app.middleware) sees the same layers use would. Since @koa/router also calls compose per request, the subscriber uses getActiveSpan() to only wrap at app startup (no active span) and skip the per-request router composition.

Closes #20758

Linear: https://linear.app/getsentry/issue/JS-2409/rewrite-opentelemetryinstrumentation-koa-to-orchestrion

@s1gr1d s1gr1d requested a review from a team as a code owner July 9, 2026 13:52
@s1gr1d s1gr1d requested review from JPeer264, andreiborza and mydea and removed request for a team July 9, 2026 13:52
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 27.59 kB - -
@sentry/browser - with treeshaking flags 26.03 kB - -
@sentry/browser (incl. Tracing) 46.34 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 48.13 kB - -
@sentry/browser (incl. Tracing, Profiling) 51.12 kB - -
@sentry/browser (incl. Tracing, Replay) 85.62 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 75.26 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 90.32 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 102.97 kB - -
@sentry/browser (incl. Feedback) 44.76 kB - -
@sentry/browser (incl. sendFeedback) 32.38 kB - -
@sentry/browser (incl. FeedbackAsync) 37.51 kB - -
@sentry/browser (incl. Metrics) 28.67 kB - -
@sentry/browser (incl. Logs) 28.91 kB - -
@sentry/browser (incl. Metrics & Logs) 29.59 kB - -
@sentry/react 29.38 kB - -
@sentry/react (incl. Tracing) 48.61 kB - -
@sentry/vue 33.03 kB - -
@sentry/vue (incl. Tracing) 48.3 kB - -
@sentry/svelte 27.61 kB - -
CDN Bundle 30 kB - -
CDN Bundle (incl. Tracing) 48.32 kB - -
CDN Bundle (incl. Logs, Metrics) 31.57 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 49.64 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 70.81 kB - -
CDN Bundle (incl. Tracing, Replay) 85.84 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 87.14 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 91.64 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 92.92 kB - -
CDN Bundle - uncompressed 89.35 kB - -
CDN Bundle (incl. Tracing) - uncompressed 146.1 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 94.05 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 150.07 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 218.78 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 265.3 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 269.26 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 279 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 282.95 kB - -
@sentry/nextjs (client) 51.16 kB - -
@sentry/sveltekit (client) 46.78 kB - -
@sentry/core/server 78.42 kB - -
@sentry/core/browser 64.74 kB - -
@sentry/node-core 62.72 kB - -
@sentry/node 125.37 kB -0.01% -3 B 🔽
@sentry/node (incl. diagnostics channel injection) 139.06 kB +0.38% +516 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 69.95 kB - -
@sentry/node/light 50.72 kB - -
@sentry/node - without tracing 74.06 kB +0.01% +1 B 🔺
@sentry/aws-serverless 85.5 kB +0.01% +1 B 🔺
@sentry/cloudflare (withSentry) - minified 181.71 kB - -
@sentry/cloudflare (withSentry) 449.16 kB - -

View base workflow run

@isaacs isaacs self-requested a review July 10, 2026 18:19

@isaacs isaacs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good!

The main question is if the update to orchestrion can allow us to instrument koa's main export properly. Apart from that just some mechanical stuff and a few bikeshed nits. (Wire Deno's options through, and update test snapshots so the CI stops complaining.)

* spans nest under the active HTTP server span.
*/
const _denoKoaIntegration = (() => {
const inner = koaChannelIntegration();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: This loses the ignoreLayersType option. It should take the options and thread them through, like how we do in packages/deno/src/integrations/postgres.ts

export const koaConfig = [
{
channelName: 'compose',
module: { name: 'koa-compose', versionRange: '>=4.0.0 <5', filePath: 'index.js' },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rationale in the PR description makes sense, and this is a good workaround to address it.

But I believe that the require(esm) issue will be addressed by @apm-js-collab/tracing-hooks's next release, once we land apm-js-collab/tracing-hooks#45

Is that accurate? If so, it might be worth backing this out, and patching the main export directly, if only to keep the versions aligned with the OTel instrumentation.

// subscription here.
let subscribed = false;

let ignoreLayersType: KoaLayerType[] = [];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This is functionally fine, because of the subscriber/integration deduping. But we've done this sort of "integration-local" variables within a closure in eg pg/redis/ioredis.

I don't have a strong opinion about which is better, but it might be good to settle on one pattern. The closure approach seems like it might be slightly safer, because it's future-proofed against potentially relaxing the deduplication constraint, but it seems unlikely we'd ever do that, so this is very much 100% a bikeshed nit.

};
}

function getMiddlewareMetadata(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than duplicate all the stuff from the vendored utils.ts, it'd be good to extract those out to a shared helpers.ts first, just to have one less thing to keep in sync.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, on further thought, I guess that somewhat is tricky because the utils.ts file is in a completely different package (sentry/node vs sentry/server-utils) 🤔

I'm not sure if it makes sense to have the node SDK's vendored otel koa instrumentation pull from server-utils to get its reusables. Feels like a stretch. Maybe this concern can be addressed with a comment just mentioning that the two files should be kept in sync as long as we're using the otel koa in node. They're unlikely to change very much anyway, I suppose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rewrite @opentelemetry/instrumentation-koa to orchestrion

2 participants