Skip to content

feat(observability): add @sourceloop/observability with OTEL-first bootstrap, profiles, and pluggable instrumentation#2502

Open
samarpan-b wants to merge 5 commits intomasterfrom
arc-obf
Open

feat(observability): add @sourceloop/observability with OTEL-first bootstrap, profiles, and pluggable instrumentation#2502
samarpan-b wants to merge 5 commits intomasterfrom
arc-obf

Conversation

@samarpan-b
Copy link
Copy Markdown
Contributor

@samarpan-b samarpan-b commented Apr 13, 2026

Description

This PR introduces a new reusable package, @sourceloop/observability, to provide a generic OpenTelemetry-first observability layer for Sourceloop services.

The package is designed around:

  • early bootstrap for tracing setup before app startup
  • optional LoopBack integration through ObservabilityComponent
  • profile-based configuration for OTLP, New Relic, SigNoz, and Datadog
  • optional instrumentation packages that consumers can install and enable selectively
  • generic tracing helpers for spans, errors, and propagation

Configuration model

Startup-critical observability config is resolved during bootstrap from:

  1. explicit bootstrap overrides
  2. environment variables
  3. defaults

LoopBack DI config is still supported, but only for component-level enrichment rather than early startup control.

Instrumentation behavior

By default, only:

  • http
  • express

are enabled.

All other built-in instrumentations are disabled by default and must be explicitly enabled through bootstrap config or env vars.

Instrumentation packages are optional peer dependencies. If an instrumentation is enabled but its package is not installed, bootstrap fails fast with a clear validation error.

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Intermediate change (work in progress)

How Has This Been Tested?

Verified locally with:

  • npm run build
  • npm test
  • npm run lint

Checklist:

  • Performed a self-review of my own code
  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Any dependent changes have been merged and published in downstream modules

Copilot AI review requested due to automatic review settings April 13, 2026 21:46
@samarpan-b samarpan-b requested a review from a team as a code owner April 13, 2026 21:46
@sonarqube-agent
Copy link
Copy Markdown

sonarqube-agent bot commented Apr 13, 2026

Remediation Agent Summary 📊

🤖 To review: Fixes are ready for 4 of 6 issues found.
💪 Save time: Applying these fixes could save you an estimated 58 minutes.
Suggested fixes (4)

QualityIssue
Maintainability
🔴 High
Function has a complexity of 13 which is greater than 10 authorized.

Why is this an issue?

Why is this an issue?

The Cyclomatic Complexity of functions should not exceed a defined threshold. Complex code may perform poorly and can be difficult to test thoroughly.


Maintainability
🔴 High
Function has a complexity of 21 which is greater than 10 authorized.

Why is this an issue?

Why is this an issue?

The Cyclomatic Complexity of functions should not exceed a defined threshold. Complex code may perform poorly and can be difficult to test thoroughly.


Maintainability
🔴 High
Function has a complexity of 14 which is greater than 10 authorized.

Why is this an issue?

Why is this an issue?

The Cyclomatic Complexity of functions should not exceed a defined threshold. Complex code may perform poorly and can be difficult to test thoroughly.


Maintainability
🔴 High
Refactor this code to not nest more than 3 if/for/while/switch/try statements.

Why is this an issue?

Why is this an issue?

Nested control flow statements such as if, for, while, switch, and try are often key ingredients in creating what’s known as "Spaghetti code". This code smell can make your program difficult to understand and maintain.

When numerous control structures are placed inside one another, the code becomes a tangled, complex web. This significantly reduces the code’s readability and maintainability, and it also complicates the testing process.

Resources


Issues requiring manual fix (2)

QualityIssue
Maintainability
🟡 Low
This assertion is unnecessary since it does not change the type of the expression.

Why is this an issue?

Why is this an issue?

In TypeScript, casts and non-null assertions are two mechanisms used to inform the TypeScript compiler about the intended types of variables, expressions, or values in the code. They are used to help the compiler understand the types more accurately and to handle certain situations where type inference might not be sufficient:

  • A type assertion tells the compiler to treat the value as the specified type, even if the compiler’s type inference suggests a different type.
  • A non-null assertion is a way to tell the TypeScript compiler explicitly that you are certain that a variable will not be null or undefined, and you want to access its properties or methods without any null checks.

However, a redundant cast occurs when you perform a type assertion that is not needed because the compiler already knows the type based on the context or explicit type declarations. Similarly, a redundant non-null assertion occurs when you use the non-null assertion operator ! to assert that a variable is not null or undefined, but the compiler already knows that based on the context.

Both redundant casts and redundant non-null assertions should be avoided in TypeScript code, as they add unnecessary noise, clutter the code, and lead to confusion.

View this code on SonarQube Cloud

Remove all the redundant casts and non-null assertions based on the contextual typing information, as inferred by the TypeScript compiler.

View this code on SonarQube Cloud

Resources

Documentation


Maintainability
🟡 Low
This assertion is unnecessary since it does not change the type of the expression.

Why is this an issue?

Why is this an issue?

In TypeScript, casts and non-null assertions are two mechanisms used to inform the TypeScript compiler about the intended types of variables, expressions, or values in the code. They are used to help the compiler understand the types more accurately and to handle certain situations where type inference might not be sufficient:

  • A type assertion tells the compiler to treat the value as the specified type, even if the compiler’s type inference suggests a different type.
  • A non-null assertion is a way to tell the TypeScript compiler explicitly that you are certain that a variable will not be null or undefined, and you want to access its properties or methods without any null checks.

However, a redundant cast occurs when you perform a type assertion that is not needed because the compiler already knows the type based on the context or explicit type declarations. Similarly, a redundant non-null assertion occurs when you use the non-null assertion operator ! to assert that a variable is not null or undefined, but the compiler already knows that based on the context.

Both redundant casts and redundant non-null assertions should be avoided in TypeScript code, as they add unnecessary noise, clutter the code, and lead to confusion.

View this code on SonarQube Cloud

Remove all the redundant casts and non-null assertions based on the contextual typing information, as inferred by the TypeScript compiler.

View this code on SonarQube Cloud

Resources

Documentation


🤖 Agent created PR #2503

💡 Got issues in your backlog? Let the agent fix them for you.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new @sourceloop/observability package that bootstraps OpenTelemetry tracing early in service startup, with optional LoopBack integration (bindings + extension point) and a small helper API for spans/correlation/errors.

Changes:

  • Introduces a new packages/observability workspace package (bootstrap/runtime, LoopBack component, profiles, config resolution/validation, tracing helpers).
  • Adds built-in OTLP-based profiles (default, newrelic, signoz, datadog) and optional auto-instrumentation loading via optional peer deps.
  • Adds unit/integration tests and wires the new package into the monorepo workspace + lockfile.

Reviewed changes

Copilot reviewed 37 out of 38 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/observability/tsconfig.json TypeScript build configuration for the new package
packages/observability/src/types.ts Public types for config, profiles, runtime, and instrumentations
packages/observability/src/tracing.ts withSpan / addSpanAttributes helpers
packages/observability/src/runtime.ts Singleton runtime state management helpers
packages/observability/src/profiles/signoz.profile.ts SigNoz OTLP defaults profile
packages/observability/src/profiles/registry.service.ts LoopBack extension-point registry for profiles
packages/observability/src/profiles/otlp.profile.ts Base OTLP profile: resource/sampler/exporter + auto-instrumentations
packages/observability/src/profiles/newrelic.profile.ts New Relic OTLP defaults profile
packages/observability/src/profiles/keys.ts LoopBack extension-point keys + binding template helper
packages/observability/src/profiles/instrumentations.ts Optional peer-dep instrumentation loading + toggles
packages/observability/src/profiles/index.ts Public exports for profiles
packages/observability/src/profiles/datadog.profile.ts Datadog OTLP defaults profile
packages/observability/src/keys.ts LoopBack binding keys for config/runtime/resolved config
packages/observability/src/index.ts Package public API surface
packages/observability/src/errors.ts recordException span helper
packages/observability/src/correlation.ts Trace context propagation/correlation helpers
packages/observability/src/config/validate-config.ts Startup validation (dependencies + coherent toggles)
packages/observability/src/config/resolve-config.ts Env/bootstrap/DI config resolution logic
packages/observability/src/config/defaults.ts Default resolved config + default instrumentation toggles
packages/observability/src/component.ts Optional LoopBack component wiring + built-in profile bindings
packages/observability/src/bootstrap.ts Core bootstrap/shutdown/runtime creation
packages/observability/src/tests/unit/validate-config.unit.ts Unit test for config validation
packages/observability/src/tests/unit/resolve-config.unit.ts Unit test for config resolution precedence
packages/observability/src/tests/unit/profile-defaults.unit.ts Unit tests for built-in profile defaults
packages/observability/src/tests/unit/instrumentations.unit.ts Unit tests for instrumentation loading/enabling
packages/observability/src/tests/unit/README.md Unit test folder marker doc
packages/observability/src/tests/integration/runtime.integration.ts Integration test for runtime initialization + helpers
packages/observability/src/tests/integration/README.md Integration test folder marker doc
packages/observability/src/tests/acceptance/README.md Acceptance test folder marker doc
packages/observability/package.json New package manifest (deps/peers/scripts)
packages/observability/README.md Package documentation (usage, resolution model, profiles)
packages/observability/.prettierrc Prettier config for the new package
packages/observability/.prettierignore Prettier ignore rules
packages/observability/.gitignore Package-level ignore rules
packages/observability/.eslintrc.js ESLint config (typed linting)
packages/observability/.eslintignore ESLint ignore rules
package.json Adds packages/observability/ to workspaces
package-lock.json Lockfile updates for the new workspace package and dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/observability/src/profiles/otlp.profile.ts
Comment thread packages/observability/src/config/validate-config.ts Outdated
Comment thread packages/observability/src/runtime.ts Outdated
Comment thread packages/observability/src/config/resolve-config.ts Outdated
Comment thread packages/observability/src/profiles/otlp.profile.ts Outdated
@samarpan-b samarpan-b changed the title feat(observability): add a new package for otel based standard and best practices observability / tracing feat(observability): add @sourceloop/observability with OTEL-first bootstrap, profiles, and pluggable instrumentation Apr 14, 2026
samarpan-b pushed a commit that referenced this pull request Apr 14, 2026
…plexity from #2502 (#2503)

* fix: Commit 1 - Fully fix typescript:S1541

Commit 1 of SonarQube suggestions

Fully fixed issues:
- [typescript:S1541] AZ2I0JCED4ljEi2GS_-X: Function has a complexity of 13 which is greater than 10 authorized.
- [typescript:S1541] AZ2I0JCED4ljEi2GS_-Y: Function has a complexity of 21 which is greater than 10 authorized.

Generated by SonarQube Agent

* fix: Commit 2 - Fully fix typescript:S1541, typescript:S134

Commit 2 of SonarQube suggestions

Fully fixed issues:
- [typescript:S1541] AZ2I0JE5D4ljEi2GS_-c: Function has a complexity of 14 which is greater than 10 authorized.
- [typescript:S134] AZ2I0JE5D4ljEi2GS_-d: Refactor this code to not nest more than 3 if/for/while/switch/try statements.

Generated by SonarQube Agent

---------

Co-authored-by: sonarqube-agent[bot] <210722872+sonarqube-agent[bot]@users.noreply.github.com>
}

initialize(
config: ResolvedObservabilityConfig,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I noticed that bootstrapObservability() in bootstrap.ts already calls profile.applyDefaults(resolvedConfig) before invoking profile.initialize(). Since this override calls this.applyDefaults(config) again before delegating to super, applyDefaults ends up running twice — headers get merged twice and vendor.apm gets set twice. The same pattern exists in DatadogObservabilityProfile and SignozObservabilityProfile.

Removing the initialize() override here (and in Datadog/Signoz) should be enough — BaseOtlpObservabilityProfile.initialize handles initialization fully, and applyDefaults is already called by the bootstrap flow.

// applyDefaults() is already called by bootstrapObservability() before initialize() is invoked.
// The base class implementation is sufficient — no need to override initialize() here.
export class NewRelicObservabilityProfile extends BaseOtlpObservabilityProfile {
  name: ObservabilityProfileName = 'newrelic';

  applyDefaults(config: ResolvedObservabilityConfig): ResolvedObservabilityConfig {
    const licenseKey = process.env.NEW_RELIC_LICENSE_KEY?.trim();
    return {
      ...config,
      otlpEndpoint:
        config.otlpEndpoint ??
        (config.exporterProtocol === 'grpc'
          ? 'https://otlp.nr-data.net:4317'
          : 'https://otlp.nr-data.net:4318/v1/traces'),
      otlpHeaders: {
        ...(licenseKey ? {'api-key': licenseKey} : {}),
        ...config.otlpHeaders,
      },
      resourceAttributes: {
        'vendor.apm': 'newrelic',
        ...config.resourceAttributes,
      },
    };
  }
}

With the double call, the config.otlpHeaders spread runs twice, which can put headers in a state that's tricky to trace back to the source.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. profile defaults are now applied once during bootstrap resolution; vendor profile initialize() no longer reapplies them.

return await fn();
} catch (error) {
span.recordException(error as Error);
throw error;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The catch block records the exception but doesn't follow up with span.setStatus({ code: SpanStatusCode.ERROR }). Per the OpenTelemetry spec, recording an exception and setting the span status are two separate operations — the exception is attached as an event on the span, but the span status stays UNSET unless explicitly updated. Most trace backends (Jaeger, Grafana Tempo, Datadog, New Relic) use the span status to colour and surface error traces, so without this, spans that throw would appear successful in dashboards.

Worth noting that recordException in errors.ts already handles this correctly — withSpan just needs to match it:

import {context, trace, SpanStatusCode} from '@opentelemetry/api';

// inside the catch block:
} catch (error) {
  span.recordException(error as Error);
  span.setStatus({
    code: SpanStatusCode.ERROR,
    message: error instanceof Error ? error.message : String(error),
  });
  throw error;
} finally {
  span.end();
}

This is the one thing I'd consider a hard blocker — without it the package would make errors harder to find, which is the opposite of what observability is for.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. withSpan() now marks failed spans with SpanStatusCode.ERROR in addition to recording the exception, so traces no longer appear successful on error paths.

Comment thread packages/observability/src/bootstrap.ts Outdated
validateObservabilityConfig(profileConfig);

const initResult = profile.initialize(profileConfig, {
createExporter: createOtlpExporter,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two related reliability gaps I wanted to flag together here.

First — if profile.initialize() throws (an instrumentation ABI mismatch, a tracerProvider.register() failure, a missing peer dep slipping past validation), updateRuntimeState({profile, config}) has already run but updateRuntimeState({runtime}) hasn't. A subsequent call to bootstrapObservability() then sees runtime === undefined, re-enters against corrupted partial state, and the original error is never logged. This can be surprisingly hard to diagnose in production.

Second — await runtime.shutdown() in shutdownObservability() isn't wrapped in a try-catch. The OTel SDK's tracerProvider.shutdown() can reject on flush timeout, which becomes an unhandled rejection in a SIGTERM handler and crashes the process in Node 15+.

// bootstrapObservability — wrap initialization in try-catch
try {
  const profile = resolveProfile(resolvedConfig.profile);
  const profileConfig = profile.applyDefaults(resolvedConfig);
  validateObservabilityConfig(profileConfig);

  const initResult = profile.initialize(profileConfig, {
    createExporter: createOtlpExporter,
  });

  const runtime: ObservabilityRuntime = {
    enabled: true,
    profile: profile.name,
    config: profileConfig,
    tracerProvider: initResult.tracerProvider,
    async shutdown() {
      await profile.shutdown?.();
      clearRuntimeState();
    },
  };

  updateRuntimeState({profile, config: profileConfig, runtime});
  return runtime;
} catch (err) {
  clearRuntimeState(); // prevent corrupted partial state on retry
  console.error('[observability] Initialization failed. Tracing will be disabled.', err);
  throw err;
}

// shutdownObservability — guard against rejection
export async function shutdownObservability(): Promise<void> {
  const runtime = getRuntimeState().runtime;
  if (!runtime) return;

  try {
    await runtime.shutdown();
  } catch (err) {
    // Log but don't rethrow — a shutdown error must not crash
    // the process during SIGTERM handling.
    console.error(
      '[observability] Error during shutdown. Some spans may not have been flushed.',
      err,
    );
  }
}

The clearRuntimeState() on failure is the key part — it ensures a retry starts clean rather than against partial state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. bootstrap now guards profile initialization and clears partial runtime state on failure. shutdownObservability() also handles shutdown errors defensively so process termination does not leave the singleton in a corrupt state.

Copy link
Copy Markdown
Contributor

@rohit-sourcefuse rohit-sourcefuse left a comment

Choose a reason for hiding this comment

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

Really impressive work here — the 3-tier config pipeline, the BaseOtlpObservabilityProfile abstraction shared cleanly across all four vendor profiles, and the lazy require() approach for optional instrumentation are all the right design choices for a package like this. The ProfileBootstrapContext injection seam is a particularly nice touch that makes testing straightforward without pulling in real OTel packages.

A few things I've flagged inline that I'd like to see addressed before this merges:

Worth resolving before merge:

  • Double applyDefaults in NewRelicObservabilityProfile, DatadogObservabilityProfile, and SignozObservabilityProfilebootstrap.ts already calls applyDefaults before initialize, so the overrides run it twice
  • SpanStatusCode.ERROR missing in withSpan catch block — without this, error spans appear successful in every trace backend (Jaeger, Datadog, Grafana Tempo, etc.)
  • No error handling around profile.initialize() in bootstrapObservability() — a failure there leaves the module singleton in a corrupted partial state with no logging; and shutdownObservability() needs a try-catch guard since tracerProvider.shutdown() can reject in a SIGTERM handler

Happy to defer to a follow-up:

  • ObservabilityProfileRegistry is wired into the component but bootstrapObservability() never consults it — a custom profile registered via LoopBack DI is silently ignored today. Worth either connecting it or leaving a comment noting it's planned for a later iteration
  • Exporter peer deps (@opentelemetry/exporter-trace-otlp-http, -grpc) don't have the same pre-flight check that instrumentation packages get in validateObservabilityConfig — a missing exporter gives a raw MODULE_NOT_FOUND instead of a helpful install prompt
  • Test coverage for the idempotency guard, shutdownObservability(), and the withSpan error rethrow path

Also worth confirming — are the prettier failures and Node 24 EBADENGINE warnings in CI pre-existing on the base branch, or introduced here?

@inject(ObservabilityBindings.config, {optional: true})
private readonly config?: ObservabilityConfig,
) {
const runtime = getRuntimeState().runtime ?? bootstrapObservability();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 Architecture: No LoopBack Sequence integration for tracer/meter enrichment

Issue: The component bootstraps OTEL but never wires tracer or meter into the LoopBack request sequence. Without a custom Sequence (or SequenceActions), there is no automatic span-per-request, no correlation of traceId/spanId into request logs, and no request-level metrics (counters, histograms). The tracerProvider sits in the DI container but nothing starts a root span for incoming HTTP requests.

Fix: Add an ObservabilitySequence (or ObservabilityAction) that wraps each request in a span and injects trace context into the logger:

// packages/observability/src/sequence.ts
import {inject} from '@loopback/core';
import {RestBindings, SequenceHandler, RequestContext} from '@loopback/rest';
import {trace, context, propagation} from '@opentelemetry/api';
import {ObservabilityBindings} from './keys';

export class ObservabilitySequence implements SequenceHandler {
  constructor(
    @inject(RestBindings.Http.CONTEXT)
    private readonly requestCtx: RequestContext,
    @inject(ObservabilityBindings.runtime, {optional: true})
    private readonly runtime?: ObservabilityRuntime,
  ) {}

  async handle(request: Request, response: Response): Promise<void> {
    const tracer = trace.getTracer('@sourceloop/observability');
    const carrier: Record<string, string> = {};
    // Extract incoming W3C traceparent from request headers
    propagation.extract(context.active(), request.headers, {
      get: (h, k) => h[k] as string,
      keys: h => Object.keys(h),
    });
    // Start root span per request
    await tracer.startActiveSpan(`${request.method} ${request.path}`, async span => {
      try {
        // inject traceId into logger context here
        await next(); // delegate to LoopBack routing
      } catch (err) {
        span.recordException(err as Error);
        throw err;
      } finally {
        span.end();
      }
    });
  }
}

Then bind it in ObservabilityComponent:

this.application.sequence(ObservabilitySequence);

This is the standard LoopBack 4 pattern for cross-cutting request concerns (auth, logging, tracing).

Credit: Open Code

resourceAttributes: Record<string, string>;
}

export interface ProfileInitResult {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 Missing: Metrics (MeterProvider) support — traces-only design

Issue: ProfileInitResult and ObservabilityRuntime expose only tracerProvider. There is no meterProvider in the type definitions, no @opentelemetry/sdk-metrics integration, and no way for consumers to record counters, histograms, or gauges. The PR title says "OTEL-first" but ships traces only — metrics and logs are absent from the type contract.

Fix: Extend the types to include metrics:

import {MeterProvider} from '@opentelemetry/sdk-metrics';

export interface ProfileInitResult {
  exporterName: string;
  tracerProvider: NodeTracerProvider;
  meterProvider?: MeterProvider; // add
}

export interface ObservabilityRuntime {
  enabled: boolean;
  profile: ObservabilityProfileName;
  config: ResolvedObservabilityConfig;
  tracerProvider?: NodeTracerProvider;
  meterProvider?: MeterProvider; // add
  shutdown(): Promise<void>;
}

And wire MeterProvider in BaseOtlpObservabilityProfile.initialize() alongside NodeTracerProvider.

Credit: Open Code

runtime?: ObservabilityRuntime;
};

const state: RuntimeState = {};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Concurrency: Module-level mutable singleton breaks test isolation and multi-instance apps

Issue: const state: RuntimeState = {} is a module-level singleton mutated in place. In test suites that don't call clearRuntimeState() between tests (or where tests run in parallel), state bleeds across tests. In monorepo scenarios where multiple services are loaded in the same process (e.g. via require), they share this singleton — whichever bootstraps first wins and silently ignores subsequent configs.

Fix: Either:

  1. Use a Symbol-keyed property on globalThis so it is truly process-global and explicit, or
  2. Accept a store parameter in getRuntimeState/updateRuntimeState for testability:
// Explicit global registry (option 1)
const STATE_KEY = Symbol.for('@sourceloop/observability.state');
function getGlobalState(): RuntimeState {
  if (!(globalThis as Record<symbol, unknown>)[STATE_KEY]) {
    (globalThis as Record<symbol, unknown>)[STATE_KEY] = {};
  }
  return (globalThis as Record<symbol, unknown>)[STATE_KEY] as RuntimeState;
}

Also: clearRuntimeState uses property-by-property = undefined which is fragile — add a new key to RuntimeState and you must remember to clear it here too. Prefer Object.assign(state, {bootstrapOverrides: undefined, config: undefined, profile: undefined, runtime: undefined}) or delete all keys in a loop.

Credit: Open Code

};
}

export function bootstrapObservability(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 No mutex / race guard on concurrent bootstrapObservability() calls

Issue: bootstrapObservability checks getRuntimeState().runtime and returns early if set. But two concurrent callers (e.g., two services starting simultaneously in the same process) can both pass the guard before either sets updateRuntimeState({runtime}) at line 101. This results in double-registration of the tracerProvider and two sets of instrumentations being activated.

Fix: Add a synchronous initialization lock:

let bootstrapping = false;

export function bootstrapObservability(
  overrides?: Partial<ObservabilityConfig>,
): ObservabilityRuntime {
  const existingRuntime = getRuntimeState().runtime;
  if (existingRuntime) return existingRuntime;
  if (bootstrapping) {
    throw new Error('bootstrapObservability called re-entrantly; ensure it is called once at startup.');
  }
  bootstrapping = true;
  try {
    // ... existing logic
  } finally {
    bootstrapping = false;
  }
}

Credit: Open Code


function loadOtlpExporter(config: ResolvedObservabilityConfig): SpanExporter {
if (config.exporterProtocol === 'grpc') {
const otlpGrpc = require('@opentelemetry/exporter-trace-otlp-grpc') as {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 require() inside methods bypasses module bundlers and breaks type safety

Issue: loadOtlpExporter uses require('@opentelemetry/exporter-trace-otlp-grpc') and require('@opentelemetry/exporter-trace-otlp-http') inside the function body. This pattern:

  • Prevents tree-shaking and static analysis
  • Gives any-typed return that is immediately cast — if the module shape changes, this fails silently at runtime
  • Errors thrown during require() are uncaught here and will crash the process with an unhelpful stack trace

Fix: Wrap in try/catch with a descriptive error, or use dynamic import() (which is async and works with ESM):

function loadOtlpExporter(config: ResolvedObservabilityConfig): SpanExporter {
  try {
    if (config.exporterProtocol === 'grpc') {
      // eslint-disable-next-line @typescript-eslint/no-var-requires
      const {OTLPTraceExporter} = require('@opentelemetry/exporter-trace-otlp-grpc');
      return new OTLPTraceExporter({url: config.otlpEndpoint, metadata: ...});
    }
    const {OTLPTraceExporter} = require('@opentelemetry/exporter-trace-otlp-http');
    return new OTLPTraceExporter({url: config.otlpEndpoint, headers: ...});
  } catch (err) {
    throw new Error(
      `Failed to load OTLP exporter for protocol "${config.exporterProtocol}". ` +
      `Install the required peer dependency. Original error: ${(err as Error).message}`
    );
  }
}

Credit: Open Code

return carrier;
}

export function getTraceContext(): {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 getTraceContext always calls createPropagationHeaders() — allocates a new carrier even when no span is active

Issue: getTraceContext() calls createPropagationHeaders() unconditionally, which calls propagation.inject(context.active(), carrier). When no span is active (activeSpan is undefined), this allocates and injects into an empty carrier unnecessarily on every call.

Also, for enriching logs/metrics with trace correlation, consumers typically need traceId and spanId in a structured format, but the returned object mixes OTEL-internal IDs with W3C propagation headers — two different concerns.

Fix: Guard the propagation call:

export function getTraceContext() {
  const activeSpan = trace.getSpan(context.active());
  const spanContext = activeSpan?.spanContext();
  if (!spanContext) {
    return {traceId: undefined, spanId: undefined, traceparent: undefined, tracestate: undefined};
  }
  const carrier = createPropagationHeaders();
  return {
    traceId: spanContext.traceId,
    spanId: spanContext.spanId,
    traceparent: carrier.traceparent,
    tracestate: carrier.tracestate,
  };
}

Credit: Open Code

INSTRUMENTATION_MODULE_REQUIREMENTS,
} from '../profiles/instrumentations';

function assertDependencyInstalled(moduleName: string, message: string): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 isDependencyInstalled duplicates isModuleInstalled in instrumentations.ts

Issue: isDependencyInstalled (line 17) and assertDependencyInstalled (line 9) both use require.resolve inside try/catch — the exact same pattern as isModuleInstalled in profiles/instrumentations.ts:45. This is code duplication across two files.

Fix: Extract a shared utility into packages/observability/src/utils/module-resolution.ts:

export function isModuleInstalled(moduleName: string): boolean {
  try { require.resolve(moduleName); return true; } catch { return false; }
}

export function assertModuleInstalled(moduleName: string, message: string): void {
  if (!isModuleInstalled(moduleName)) throw new Error(message);
}

Import from both validate-config.ts and instrumentations.ts.

Credit: Open Code

this.tracerProvider = tracerProvider;

return {
exporterName: loadOtlpExporter.name,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 exporterName returns function name of internal helper — leaks implementation detail

Issue: ProfileInitResult.exporterName is set to loadOtlpExporter.name (line 117), which evaluates to the string 'loadOtlpExporter'. This exposes an internal private function name in the public API contract. The integration test at runtime.integration.ts:85 asserts strictEqual(initResult.exporterName, 'loadOtlpExporter') — this will silently break if the function is renamed or minified.

Fix: Use a stable semantic string:

return {
  exporterName: `otlp-${config.exporterProtocol}`, // e.g. 'otlp-grpc' or 'otlp-http/protobuf'
  tracerProvider,
};

Update the integration test assertion accordingly.

Credit: Open Code

@inject(ObservabilityBindings.config, {optional: true})
private readonly config?: ObservabilityConfig,
) {
const runtime = getRuntimeState().runtime ?? bootstrapObservability();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 bootstrapObservability() called in component constructor — side effects at DI bind time

Issue: Line 40 calls getRuntimeState().runtime ?? bootstrapObservability(). bootstrapObservability() registers the global NodeTracerProvider (via tracerProvider.register() in otlp.profile.ts:112), which patches @opentelemetry/api globals. This happens during LoopBack DI binding resolution, before the app is fully started. If the app fails to start after this point, the global tracer provider is registered but never shut down, leaking instrumentation hooks.

Fix: Move bootstrapObservability() to a LoopBack lifecycle hook (start/stop) to align with the application lifecycle:

export class ObservabilityComponent implements Component, LifeCycleObserver {
  async start(): Promise<void> {
    const runtime = bootstrapObservability(this.config);
    this.application.bind(ObservabilityBindings.runtime).to(runtime);
  }

  async stop(): Promise<void> {
    await shutdownObservability();
  }
}

Credit: Open Code

}
}

function loadInstrumentations(
Copy link
Copy Markdown
Contributor

@sf-sahil-jassal sf-sahil-jassal Apr 17, 2026

Choose a reason for hiding this comment

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

🟠 Maintainability: Use @opentelemetry/auto-instrumentations-node instead of individual instrumentation packages
Issue: The current implementation manually require()s 6+ individual OTEL instrumentation packages and maintains a custom loadInstrumentations switch-case registry. Problems:

  • Maintenance burden: Every new library requires a new case, a new INSTRUMENTATION_MODULE_REQUIREMENTS entry, and a new InstrumentationToggles key
  • Version drift: 6+ packages each have independent release cycles — keeping them in sync with the SDK is error-prone
  • Incomplete coverage: Misses many libraries the OTEL ecosystem already supports (MongoDB, TypeORM, gRPC, DNS, Net, etc.)
    Fix: Replace the entire loadInstrumentations switch with @opentelemetry/auto-instrumentations-node, which bundles all supported instrumentations and accepts a per-package {enabled: boolean} config:

package.json — one dep instead of 6+:

{
  "peerDependencies": {
    "@opentelemetry/auto-instrumentations-node": "^0.56.0"
  }
}
import {getNodeAutoInstrumentations} from '@opentelemetry/auto-instrumentations-node';
import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
import {InstrumentationToggles, ResolvedObservabilityConfig} from '../types';
// Maps our toggle keys to the package names auto-instrumentations-node expects
const TOGGLE_TO_PACKAGE: Record<keyof InstrumentationToggles, string[]> = {
  http:    ['@opentelemetry/instrumentation-http'],
  express: ['@opentelemetry/instrumentation-express'],
  pg:      ['@opentelemetry/instrumentation-pg'],
  mysql:   ['@opentelemetry/instrumentation-mysql', '@opentelemetry/instrumentation-mysql2'],
  redis:   ['@opentelemetry/instrumentation-redis-4'],
  kafka:   ['@opentelemetry/instrumentation-kafkajs'],
};
export function createAutoInstrumentations(
  config: ResolvedObservabilityConfig,
  tracerProvider: NodeTracerProvider,
) {
  const enabledPackages = new Set<string>();
  for (const [toggle, packages] of Object.entries(TOGGLE_TO_PACKAGE)) {
    if (config.instrumentations[toggle as keyof InstrumentationToggles]) {
      packages.forEach(p => enabledPackages.add(p));
    }
  }
  // All packages disabled by default; only toggled-on ones are active
  const allPkgs = Object.values(TOGGLE_TO_PACKAGE).flat();
  const instrumentations = getNodeAutoInstrumentations(
    Object.fromEntries(allPkgs.map(pkg => [pkg, {enabled: enabledPackages.has(pkg)}]))
  );
  const all = [...instrumentations, ...config.customInstrumentations];
  for (const inst of all) {
    inst.setTracerProvider?.(tracerProvider);
    inst.enable();
  }
  return all;
}

Benefits:

  • Single peer dep to maintain and version-bump
  • Automatically gains new instrumentations as the package adds them
  • enabled: false entries are zero-cost no-ops at runtime
  • Backwards-compatible with existing InstrumentationToggles contract — no breaking change for consumers
  • Eliminates INSTRUMENTATION_MODULE_REQUIREMENTS and the peer dep checks in validate-config.ts entirely — auto-instrumentations-node handles graceful skipping internally

Credit: Open Code

samarpan-b and others added 4 commits April 16, 2026 22:43
…plexity from #2502 (#2503)

* fix: Commit 1 - Fully fix typescript:S1541

Commit 1 of SonarQube suggestions

Fully fixed issues:
- [typescript:S1541] AZ2I0JCED4ljEi2GS_-X: Function has a complexity of 13 which is greater than 10 authorized.
- [typescript:S1541] AZ2I0JCED4ljEi2GS_-Y: Function has a complexity of 21 which is greater than 10 authorized.

Generated by SonarQube Agent

* fix: Commit 2 - Fully fix typescript:S1541, typescript:S134

Commit 2 of SonarQube suggestions

Fully fixed issues:
- [typescript:S1541] AZ2I0JE5D4ljEi2GS_-c: Function has a complexity of 14 which is greater than 10 authorized.
- [typescript:S134] AZ2I0JE5D4ljEi2GS_-d: Refactor this code to not nest more than 3 if/for/while/switch/try statements.

Generated by SonarQube Agent

---------

Co-authored-by: sonarqube-agent[bot] <210722872+sonarqube-agent[bot]@users.noreply.github.com>
@sonarqubecloud
Copy link
Copy Markdown

SonarQube reviewer guide

Summary: Introduces a new @sourceloop/observability package providing OpenTelemetry-first observability bootstrap and optional LoopBack integration for Sourceloop services, with built-in profiles for major APM vendors and comprehensive tracing/instrumentation support.

Review Focus:

  • The hybrid bootstrap+component design: ensure bootstrap runs early before app imports and component is truly optional
  • Instrumentation toggles via environment variables and programmatic config—validate the precedence order is correct
  • Profile implementation for vendor-specific OTLP defaults (New Relic, SigNoz, Datadog) and config resolution layering
  • Peer dependency handling for optional OTel instrumentations with fail-fast validation
  • Runtime state management and graceful shutdown

Start review at: packages/observability/src/bootstrap.ts. It's the entry point that orchestrates profile resolution, config validation, and runtime initialization—critical for understanding the startup flow and how the package ensures observability is available system-wide before the application constructs.

💬 Please send your feedback

Quality Gate Passed Quality Gate passed

Issues
3 New issues
0 Accepted issues

Measures
0 Security Hotspots
0.0% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

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.

4 participants