@vtvlive/interactive-apm
v0.0.18
Published
APM integration package supporting both Elastic APM and OpenTelemetry with NestJS integration
Maintainers
Readme
@vtvlive/interactive-apm
APM integration package supporting both Elastic APM and OpenTelemetry with NestJS integration
Features
- Dual APM Provider Support: Switch between Elastic APM and OpenTelemetry via environment variable or config
- NestJS Integration: Ready-to-use global module with dependency injection (exposed via the
/nestjsentry point) - Standalone Usage: Works without NestJS for vanilla TypeScript/JavaScript projects
- Provider-Agnostic Helper:
TracingHelperauto-detects the active provider so the same code works with both - Flexible Configuration: Configure via environment variables or programmatic options
- Debug Mode: Comprehensive logging for troubleshooting connection issues
- OTLP Transport Options: Support for HTTP, gRPC, and PROTO (protobuf over HTTP) transports
- Auto Instrumentation (OpenTelemetry standalone): HTTP, Express, and ioredis out of the box
- TypeScript Support: Fully typed with TypeScript
- Auto Transaction Naming: HTTP requests automatically named as "METHOD /route/path"
Package Entry Points
The package ships two entry points so that the core tracing API does not pull in @nestjs/common:
| Import path | Use for | Requires @nestjs/common? |
| --------------------------------- | ----------------------------------------------------------------------- | -------------------------- |
| @vtvlive/interactive-apm | Core API: TracingService, TracingHelper, init functions, factory, types, providers | No |
| @vtvlive/interactive-apm/nestjs | Everything above plus the TracingModule NestJS dynamic module | Yes |
⚠️
TracingModuleis only exported from@vtvlive/interactive-apm/nestjs. Importing it from the root path will fail.
Installation
Private GitLab Registry Installation
This package is published to the internal GitLab NPM Package Registry for this project.
Create a GitLab deploy token with the read_package_registry scope, then expose the token value as VTVLIVE_NPM_TOKEN. In consumer projects, add an .npmrc file:
@vtvlive:registry=https://source.vtvlive.vn/api/v4/projects/<PROJECT_ID>/packages/npm/
//source.vtvlive.vn/api/v4/projects/<PROJECT_ID>/packages/npm/:_authToken=${VTVLIVE_NPM_TOKEN}
always-auth=trueReplace <PROJECT_ID> with the GitLab project ID for itv/internal/interactive-apm.
Install the package:
npm install @vtvlive/interactive-apmFor GitLab CI consumers, store VTVLIVE_NPM_TOKEN as a masked CI/CD variable before running npm ci or npm install.
Security note: do not commit real NPM or GitLab tokens to .npmrc. Rotate any token that was stored in plaintext locally or committed previously.
See PUBLISH_GITLAB.md and DEPLOYMENT.md for full publishing and installation details.
Peer Dependencies
All APM and NestJS dependencies are declared as optional peer dependencies — install only the ones you need for your chosen provider.
For OpenTelemetry
npm install @vtvlive/interactive-apm \
@opentelemetry/api \
@opentelemetry/sdk-node \
@opentelemetry/sdk-trace-base \
@opentelemetry/resources \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/instrumentation-http \
@opentelemetry/instrumentation-expressOptional add-ons:
# gRPC or PROTO transport support
npm install @opentelemetry/exporter-trace-otlp-grpc @opentelemetry/exporter-trace-otlp-proto
# ioredis auto-instrumentation (enabled automatically when present, in standalone init)
npm install @opentelemetry/instrumentation-ioredisFor Elastic APM
npm install @vtvlive/interactive-apm elastic-apm-nodeFor NestJS Integration
Add the required NestJS dependencies:
npm install @nestjs/common @nestjs/configEnvironment Variables
| Variable | Description | Default |
| -------------------------------------- | --------------------------------------------------------------------------- | --------------------------------- |
| APM_PROVIDER | Provider selection: elastic-apm or opentelemetry | opentelemetry |
| APM_DEBUG | Enable debug mode with detailed logging | false |
| ELASTIC_APM_SERVICE_NAME | Service name for APM | interactive-backend |
| ELASTIC_APM_SERVER_URL | Elastic APM server URL (Elastic APM only) | http://localhost:8200 |
| ELASTIC_APM_SECRET_TOKEN | Secret token for authentication | Optional |
| ELASTIC_APM_ENVIRONMENT | Deployment environment | development |
| ELASTIC_OTLP_ENDPOINT | OTLP endpoint (OpenTelemetry only) | http://localhost:8200/v1/traces |
| ELASTIC_OTLP_TRANSPORT | OTLP transport: http, grpc, or proto | http |
| ELASTIC_OTLP_AUTH_TOKEN | OTLP-specific auth token (takes precedence over ELASTIC_APM_SECRET_TOKEN) | Optional |
| ELASTIC_OTLP_HEADERS | Additional OTLP headers as JSON string | Optional |
| ELASTIC_OTLP_ENABLE_CONSOLE_EXPORTER | Enable console exporter for debugging | false |
The OTLP endpoint is normalized automatically: a port and the
/v1/tracespath are added for HTTP/PROTO, and/v1/tracesis stripped for gRPC.
Debug Mode
Enable debug mode to see detailed logs about APM initialization, span creation, and export status:
# Enable debug mode (accepts: 1, true, yes)
APM_DEBUG=trueDebug mode provides:
- Initialization details (service name, endpoint, transport type)
- Span creation and lifecycle events
- Export success/failure status
- HTTP request/response details for troubleshooting
Usage
NestJS Integration
Import TracingModule from the /nestjs entry point. TracingModule is decorated with @Global(), so TracingService becomes injectable everywhere without re-importing the module.
Complete Example with ConfigService (recommended)
registerAsync automatically initializes the underlying provider (autoInit defaults to true), which starts the OpenTelemetry SDK / Elastic APM agent.
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TracingModule } from '@vtvlive/interactive-apm/nestjs';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
TracingModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
// Provider selection
provider: configService.get<string>('APM_PROVIDER', 'opentelemetry'),
// Common configuration
serviceName: configService.get<string>('ELASTIC_APM_SERVICE_NAME', 'my-service'),
environment: configService.get<string>('ELASTIC_APM_ENVIRONMENT', 'development'),
// OpenTelemetry configuration
otlpEndpoint: configService.get<string>('ELASTIC_OTLP_ENDPOINT', 'http://localhost:8200/v1/traces'),
otlpTransport: configService.get<string>('ELASTIC_OTLP_TRANSPORT', 'http'), // 'http' | 'grpc' | 'proto'
otlpAuthToken: configService.get<string>('ELASTIC_OTLP_AUTH_TOKEN'),
otlpHeaders: configService.get('ELASTIC_OTLP_HEADERS'),
enableConsoleExporter: configService.get<string>('ELASTIC_OTLP_ENABLE_CONSOLE_EXPORTER', 'false') === 'true',
// Elastic APM configuration
serverUrl: configService.get<string>('ELASTIC_APM_SERVER_URL', 'http://localhost:8200'),
secretToken: configService.get<string>('ELASTIC_APM_SECRET_TOKEN'),
};
},
inject: [ConfigService],
}),
],
})
export class AppModule {}Basic Usage (reads from environment)
// app.module.ts
import { Module } from '@nestjs/common';
import { TracingModule } from '@vtvlive/interactive-apm/nestjs';
@Module({
imports: [
TracingModule.registerAsync(),
// ... other modules
],
})
export class AppModule {}Note: the synchronous
TracingModule.register(options)creates the provider but does not callinitialize(). PreferregisterAsync()so the SDK/agent is started automatically. If you useregister(), initialize the provider yourself before tracing.
In Controllers
import { Controller, Get } from '@nestjs/common';
import { TracingService } from '@vtvlive/interactive-apm/nestjs';
@Controller('healthcheck')
export class HealthController {
constructor(private readonly tracingService: TracingService) {}
@Get('ping')
ping() {
return this.tracingService.startSpanWithParent(
'healthcheck.ping',
async (span) => {
span?.setAttribute('healthcheck.type', 'liveness');
return { message: 'pong' };
},
{ 'http.method': 'GET', 'http.route': '/healthcheck/ping' }
);
}
}In Services
import { Injectable, NotFoundException } from '@nestjs/common';
import { TracingService } from '@vtvlive/interactive-apm/nestjs';
@Injectable()
export class UserService {
constructor(
private readonly tracingService: TracingService,
private readonly userRepository: UserRepository
) {}
async findById(id: string) {
return this.tracingService.startSpanWithParent(
'user.findById',
async (span) => {
span?.setAttribute('userId', id);
const user = await this.userRepository.findById(id);
if (!user) {
throw new NotFoundException(`User ${id} not found`);
}
span?.setAttribute('user.exists', true);
return user;
},
{ 'operation.type': 'read' }
);
}
async create(data: CreateUserDto) {
return this.tracingService.startSpanWithParent(
'user.create',
async (span) => {
span?.setAttribute('user.email', data.email);
// This nested call creates a child span under the active context
const hashedPassword = await this.tracingService.startSpanWithParent(
'user.hashPassword',
async () => await bcrypt.hash(data.password, 10),
{ 'operation.type': 'crypto' }
);
const user = await this.userRepository.create({ ...data, password: hashedPassword });
// Attach a label to the root transaction span (not the current child span)
this.tracingService.setTransactionLabel('user.created', true);
return user;
},
{ 'operation.type': 'write' }
);
}
}Standalone Usage (without NestJS)
Call the init function at the very top of your entry file, before importing your app code, so auto-instrumentation can hook into http/express/ioredis.
OpenTelemetry
initOpenTelemetry is async and returns the started SDK instance ({ start, shutdown, flush? }). HTTP, Express, and ioredis instrumentation are enabled by default.
import { initOpenTelemetry } from '@vtvlive/interactive-apm';
await initOpenTelemetry({
serviceName: 'my-service',
otlpEndpoint: 'http://localhost:8200/v1/traces',
environment: 'production',
otlpTransport: 'proto', // 'http' | 'grpc' | 'proto'
enableConsoleExporter: false,
// enableHttpInstrumentation: false, // opt out if needed
// enableExpressInstrumentation: false,
});
// Your application code
import express from 'express';
const app = express();Elastic APM
initElasticApm is synchronous and returns the started agent instance.
import { initElasticApm } from '@vtvlive/interactive-apm';
initElasticApm({
serviceName: 'my-service',
serverUrl: 'http://localhost:8200',
environment: 'production',
secretToken: 'your-secret-token',
});
// Your application codeUsing TracingService standalone
Compose a provider via the factory and wrap it with TracingService:
import { createTracingProvider, TracingService } from '@vtvlive/interactive-apm';
const provider = createTracingProvider({ provider: 'opentelemetry', serviceName: 'my-service' });
// OpenTelemetry providers must be initialized before use:
await (provider as any).initialize?.();
const tracing = new TracingService(provider);
await tracing.startSpanWithParent('job.run', async (span) => {
span?.setAttribute('job.id', '123');
});Provider-agnostic helper
TracingHelper is a static class that auto-detects the active provider (OpenTelemetry or Elastic APM) and routes to the right API. Useful when you don't have access to DI.
import { TracingHelper, SpanKind } from '@vtvlive/interactive-apm';
await TracingHelper.startSpanWithParent(
'cache.lookup',
async (span) => {
span?.setAttribute('cache.key', key);
return await cache.get(key);
},
{ 'operation.type': 'read' },
SpanKind.CLIENT
);
// Or manage the span manually
const span = TracingHelper.startSpan('my.operation', { key: 'value' }, SpanKind.SERVER);
try {
// ... code
} finally {
TracingHelper.endSpan(span);
}
const traceId = TracingHelper.getTraceId();API Reference
Exports overview
// From '@vtvlive/interactive-apm' (core, no NestJS)
import {
TracingService,
TracingHelper,
createTracingProvider,
TracingProviderFactory,
TRACING_PROVIDER_TOKEN,
OpenTelemetryTracingProvider,
ElasticApmTracingProvider,
initOpenTelemetry,
initElasticApm,
isElasticApmStarted,
getElasticApmAgent,
shouldUseElasticApm,
shouldUseOpenTelemetry,
ApmProvider,
SpanKind,
OtlpTransport,
} from '@vtvlive/interactive-apm';
// From '@vtvlive/interactive-apm/nestjs' (adds the module)
import { TracingModule } from '@vtvlive/interactive-apm/nestjs';TracingModule (/nestjs)
NestJS global dynamic module for APM integration.
| Method | Description |
| ------------------------ | -------------------------------------------------------------------------------------- |
| register(options) | Register with synchronous options. Does not auto-initialize the provider. |
| registerAsync(options) | Register with async options (e.g. ConfigService). Auto-initializes when autoInit !== false. |
TracingModuleOptions: provider, serviceName, environment, serverUrl, secretToken, otlpEndpoint, serviceVersion, autoInit.
TracingModuleAsyncOptions: imports, useFactory, inject (returns TracingModuleOptions).
TracingService
Injectable service that wraps the active ITracingProvider.
| Method | Description |
| ----------------------------------------------------- | ------------------------------------------------------------------ |
| startSpan(name, attributes?, spanKind?) | Start a span (default kind INTERNAL); returns ISpan \| null — call span.end() manually |
| startSpanWithParent(name, fn, attributes?, spanKind?) | Run fn inside an auto-closed span (default kind SERVER) — recommended |
| captureError(error) | Record an error on the active span |
| setAttribute(key, value) | Set an attribute on the active span |
| setTransactionLabel(key, value) | Set a label/attribute on the root transaction span |
| endSpan(span?, result?) | End a span manually (active span if omitted) |
| shutdown() | Flush and shut down the provider |
TracingHelper
Static, provider-agnostic helper (auto-detects provider from APM_PROVIDER or runtime state).
| Method | Description |
| ------------------------------------------------------- | ------------------------------------------------- |
| startSpan(name, attributes?, spanKind?) | Start a span (returns ISpan) |
| startSpanWithParent(name, fn, attributes?, spanKind?) | Run fn inside an auto-closed span |
| captureError(error) | Record an error on the active span |
| setAttribute(key, value) | Set an attribute on the active span |
| setTransactionLabel(key, value) | Set a label on the active transaction |
| endSpan(span?, result?) | End a span manually |
| getActiveSpan() | Get the current active span |
| getTraceId() | Get the current trace ID |
| resetProviderCache() | Reset the cached provider (useful for testing) |
Factory & init functions
| Export | Description |
| ----------------------------------------------- | --------------------------------------------------------------------------- |
| createTracingProvider(config?) | Create an ITracingProvider from config/env. Returns OpenTelemetry or Elastic provider |
| TRACING_PROVIDER_TOKEN | DI token ('ITracingProvider') for custom NestJS providers |
| TracingProviderFactory | Ready-made NestJS factory provider object |
| initOpenTelemetry(options?) | async — start the OpenTelemetry NodeSDK (returns the SDK) |
| initElasticApm(options?) | Start the Elastic APM agent (returns the agent) |
| isElasticApmStarted() / getElasticApmAgent()| Inspect the Elastic APM agent |
| shouldUseElasticApm() / shouldUseOpenTelemetry() | Resolve provider choice from APM_PROVIDER |
SpanKind
Types of spans (used with startSpan / startSpanWithParent):
INTERNAL: Internal operation (default forstartSpan)SERVER: Server-side request handler (API endpoints) — default forstartSpanWithParentCLIENT: Client-side call (outgoing HTTP, database)PRODUCER: Message producerCONSUMER: Message consumerWEBSOCKET: WebSocket communicationOTHER: Other custom span kind
OtlpTransport
Transport options for the OpenTelemetry OTLP exporter:
HTTP(http): JSON over HTTP (default)GRPC(grpc): gRPC with protobufPROTO(proto): Protobuf over HTTP (recommended for Elastic APM)
Transaction Naming
Root HTTP SERVER spans are automatically renamed in the format METHOD /route/path:
- Request to
GET /api/healthcheck/ping→ Transaction:GET /api/healthcheck/ping - Request to
POST /api/users→ Transaction:POST /api/users
This applies to both OpenTelemetry and Elastic APM providers.
Provider Comparison
| Feature | OpenTelemetry | Elastic APM | | -------------------- | ----------------------------------- | ---------------- | | Transport Options | HTTP, gRPC, PROTO | HTTP/HTTPS | | Auto Instrumentation | HTTP, Express, ioredis (standalone) | Built-in | | Debug Logging | Comprehensive | Comprehensive | | Transaction Naming | Auto + Custom | Auto + Custom | | Standard | W3C Trace Context | Elastic APM spec |
Troubleshooting
No traces appearing in APM
- Set
APM_DEBUG=trueto see detailed logs - Verify the endpoint URL is correct (and reachable)
- Confirm the provider was initialized — use
TracingModule.registerAsync()or callinitialize()/ an init function - Verify the authentication token
- Make sure the required peer dependencies for your provider/transport are installed
Using npm link for local development
# In the package directory
cd /path/to/interactive-apm
npm link
# In your project directory
cd /path/to/your-project
npm link @vtvlive/interactive-apmAfter making changes to the package, rebuild it:
cd /path/to/interactive-apm
npm run buildTesting
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # With coverage
npm run test:unit # Unit tests only
npm run test:integration # Integration tests onlyDevelopment
npm run build # Build the package (tsc)
npm run build:watch # Build in watch mode
npm run typecheck # Type-check without emitting
npm run lint # Lint src
npm run format # Format src with Prettier
npm run clean # Remove distLicense
MIT
Author
VTVLive
For more information:
