exjs-controllers

Tracing

Wrap controller handlers, use cases and arbitrary methods in OpenTelemetry spans.

Every controller handler runs inside an OpenTelemetry span automatically. Use cases decorated with @DefineUseCase add a nested span around execute. You can wrap any other method with @TraceSpan to keep the call graph complete.

exjs-controllers depends only on @opentelemetry/api — bringing the SDK (NodeSDK, exporters, processors) is your responsibility. Without an SDK installed, span creation is a no-op.

Automatic spans

SpanAttributes
ControllerName.handlerName (per request)http.method, http.route, http.controller.name, http.controller.handler, http.handler.name, http.handler.method
UseCaseName.execute (per @DefineUseCase)exjs.handler.name, exjs.handler.method, exjs.use_case.name, exjs.use_case.method
BaseSchema.validate (per instance.validate)(defaults)

Exceptions thrown inside the wrapped function are recorded on the span and the status is marked as ERROR before being rethrown — so failures show up cleanly in your tracing backend.

@TraceSpan(name?, options?)

Wrap any method with a custom span. Works on controllers, services, repositories or use cases.

import { TraceSpan } from 'exjs-controllers/decorators/TraceSpan'

class UsersRepository {
  @TraceSpan('UsersRepository.findAll')
  async findAll() { /* ... */ }

  @TraceSpan('UsersRepository.findOne', {
    attributes: { 'db.system': 'postgresql' }
  })
  async findOne(id: string) { /* ... */ }
}

If name is omitted, the span name defaults to <ClassName>.<methodName>.

runWithSpan and runWithControllerSpan

For ad-hoc wrapping, use the helpers directly.

import {
  runWithSpan,
  runWithControllerSpan
} from 'exjs-controllers/observability/tracing'

await runWithSpan(
  { name: 'syncProducts', attributes: { 'job.kind': 'scheduled' } },
  () => syncProducts()
)

Both helpers preserve sync vs async behaviour — they return the original value type, end the span on success, and on failure record the exception and mark SpanStatusCode.ERROR.

Bringing an OpenTelemetry SDK

Install @opentelemetry/sdk-node (or the browser equivalent) and start it before configureApplication:

src/telemetry.ts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
  })
})

await sdk.start()
src/server.ts
import './telemetry' // must come first
import { createApplication } from 'exjs-controllers/http/application'
import { configureApplication } from 'exjs-controllers/config/configureApplication'

const app = createApplication()
await configureApplication(app, { /* ... */ })
app.listen(3000)

Tying logs and traces together

The HTTP logger emits correlationId per request, and the logger automatically stamps traceId / spanId onto every log line whenever an OpenTelemetry span is active:

  • Logs emitted inside a handler get the current span's context via a Pino mixin that reads trace.getActiveSpan() at log time (so nested spans surface their own spanId).
  • The HTTP access log is emitted on the response close event — after the span has ended and outside the async context — so the framework captures the request's outermost span context (the controller span) into the request log context and reuses it for that final line.

This requires a registered OpenTelemetry SDK/provider (see above). With only @opentelemetry/api and no provider, spans are no-ops with an invalid context, so traceId / spanId are omitted (they show as - in the pretty formatter). The pretty log formatter surfaces both when present, so you can pivot from a log line straight to your tracing UI.

On this page