On a deployment with AXIOM_API_TOKEN and AXIOM_LOGS_DATASET set, traces arrive in Axiom but logs never do. No error appears anywhere, and the same entries still show up in Cloudflare Workers Logs, so the Worker looks healthy.
The cause is two flush paths that race.
runInvocation flushes correctly. Its finally calls ctx.waitUntil(flush(env)), so the ingest survives the end of the invocation (packages/observability/src/index.ts:230).
apps/api/src/libs/logger.ts:7 also flushes, from an Elysia onAfterResponse hook mounted globally at apps/api/src/modules/index.ts:19. That hook runs after the response is sent and its await is not inside waitUntil.
On every request the second path wins:
- The handler logs into
invocation.entries.
onAfterResponse fires and calls flush.
flush splices out every entry (index.ts:148) and writes each one to console, which is why Workers Logs has them.
- It starts the Axiom
fetch, unprotected.
runInvocation's finally runs ctx.waitUntil(flush(env)), but the buffer is empty, so it returns at the guard on index.ts:146 and registers nothing.
- The invocation ends and the orphaned fetch is cancelled.
Traces are unaffected because otel-cf-workers registers its own export through waitUntil.
A second defect hides the first. The ingest call at index.ts:161 never checks the response status. fetch rejects only on a transport error, so any 4xx from Axiom is discarded silently and the catch never runs. A log shipper that cannot report its own failure will hide the next one too.
Suggested fix: remove the onAfterResponse hook and let runInvocation own the flush for the fetch path. The Durable Object paths in apps/api/src/actor/subscriber.ts already use this.ctx.waitUntil(this.flush()) and are correct. Separately, check res.ok and log the status and body.
On a deployment with
AXIOM_API_TOKENandAXIOM_LOGS_DATASETset, traces arrive in Axiom but logs never do. No error appears anywhere, and the same entries still show up in Cloudflare Workers Logs, so the Worker looks healthy.The cause is two flush paths that race.
runInvocationflushes correctly. Itsfinallycallsctx.waitUntil(flush(env)), so the ingest survives the end of the invocation (packages/observability/src/index.ts:230).apps/api/src/libs/logger.ts:7also flushes, from an ElysiaonAfterResponsehook mounted globally atapps/api/src/modules/index.ts:19. That hook runs after the response is sent and itsawaitis not insidewaitUntil.On every request the second path wins:
invocation.entries.onAfterResponsefires and callsflush.flushsplices out every entry (index.ts:148) and writes each one toconsole, which is why Workers Logs has them.fetch, unprotected.runInvocation'sfinallyrunsctx.waitUntil(flush(env)), but the buffer is empty, so it returns at the guard onindex.ts:146and registers nothing.Traces are unaffected because
otel-cf-workersregisters its own export throughwaitUntil.A second defect hides the first. The ingest call at
index.ts:161never checks the response status.fetchrejects only on a transport error, so any 4xx from Axiom is discarded silently and thecatchnever runs. A log shipper that cannot report its own failure will hide the next one too.Suggested fix: remove the
onAfterResponsehook and letrunInvocationown the flush for the fetch path. The Durable Object paths inapps/api/src/actor/subscriber.tsalready usethis.ctx.waitUntil(this.flush())and are correct. Separately, checkres.okand log the status and body.