@vtvlive/interactive-apm
v0.0.5
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
- NestJS Integration: Ready-to-use module with dependency injection
- Standalone Usage: Works without NestJS for vanilla TypeScript/JavaScript projects
- 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
- TypeScript Support: Fully typed with TypeScript
- Auto Transaction Naming: HTTP requests automatically named as "METHOD /route/path"
Installation
For OpenTelemetry
npm install @vtvlive/interactive-apm @opentelemetry/api @opentelemetry/exporter-trace-otlp-http @opentelemetry/instrumentation-http @opentelemetry/sdk-node @opentelemetry/resourcesFor gRPC or PROTO transport support:
npm install @opentelemetry/exporter-trace-otlp-grpc @opentelemetry/exporter-trace-otlp-protoFor 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 (default: false) | false |
| ELASTIC_APM_SERVICE_NAME | Service name for APM | Required |
| ELASTIC_APM_SERVER_URL | Elastic APM server URL | 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 |
Debug Mode
Enable debug mode to see detailed logs about APM initialization, span creation, and export status:
# Enable debug mode
APM_DEBUG=true
# Or in .env file
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
Complete Example with ConfigService
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TracingModule } from '@vtvlive/interactive-apm';
@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'),
// Debug mode - parse boolean from string env var
debug: ['1', 'true', 'yes'].includes(configService.get<string>('APM_DEBUG', 'false').toLowerCase()),
};
},
inject: [ConfigService],
}),
],
})
export class AppModule {}Basic Usage (reads from environment)
// app.module.ts
import { TracingModule } from '@vtvlive/interactive-apm';
@Module({
imports: [
TracingModule.registerAsync(),
// ... other modules
],
})
export class AppModule {}In Controllers
import { Controller, Get } from '@nestjs/common';
import { TracingService } from '@vtvlive/interactive-apm';
@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 } from '@nestjs/common';
import { TracingService } from '@vtvlive/interactive-apm';
@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
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,
});
span.setAttribute('user.created', 'true');
return user;
},
{ 'operation.type': 'write' }
);
}
}Standalone Usage (without NestJS)
OpenTelemetry
import { initOpenTelemetry } from '@vtvlive/interactive-apm';
// Initialize at the top of your application (before any imports)
await initOpenTelemetry({
serviceName: 'my-service',
otlpEndpoint: 'http://localhost:8200/v1/traces',
environment: 'production',
otlpTransport: 'proto', // 'http' | 'grpc' | 'proto'
enableConsoleExporter: false,
});
// Your application code
import express from 'express';
const app = express();Elastic APM
import { initElasticApm } from '@vtvlive/interactive-apm';
// Initialize at the top of your application
initElasticApm({
serviceName: 'my-service',
serverUrl: 'http://localhost:8200',
environment: 'production',
secretToken: 'your-secret-token',
});
// Your application codeAPI Reference
TracingModule
NestJS module for APM integration.
| Method | Description |
| ------------------------ | -------------------------------------------------------------- |
| register(options) | Register module with synchronous options |
| registerAsync(options) | Register module with asynchronous options (with ConfigService) |
TracingService
Service for tracing operations.
| Method | Description |
| -------------------------------------------- | ----------------------------------------------------- |
| startSpan(name, attributes?, spanKind?) | Start a new span (must call span.end() manually) |
| startSpanWithParent(name, fn, attributes?) | Execute function with automatic tracing (recommended) |
| captureError(error) | Capture error to active span |
| setAttribute(key, value) | Set attribute on active span |
| endSpan(span?) | End a span manually |
| shutdown() | Flush and shutdown provider |
SpanKind
Types of spans (used with startSpan):
INTERNAL: Internal operationSERVER: Server-side request handler (API endpoints)CLIENT: Client-side call (outgoing HTTP, database)PRODUCER: Message producerCONSUMER: Message consumer
OtlpTransport
Transport options for OpenTelemetry OTLP exporter:
HTTP: JSON over HTTP (default)GRPC: gRPC with protobufPROTO: Protobuf over HTTP (most efficient)
Transaction Naming
HTTP transactions are automatically named 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 (configurable) | 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
- Check if
APM_DEBUG=trueis set to see detailed logs - Verify endpoint URL is correct
- Check network connectivity to APM server
- Verify authentication token
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-apmNote: After making changes to the package, rebuild it:
cd /path/to/interactive-apm
npm run buildTesting
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
# Run only unit tests
npm run test:unit
# Run only integration tests
npm run test:integrationDevelopment
# Build the package
npm run build
# Build in watch mode
npm run build:watch
# Run tests
npm test
# Clean build artifacts
npm run cleanLicense
MIT
Author
VTVLive
For more information:
