A comprehensive example showcasing how to achieve application-level transparency in OTel tracing using @waitingliou/tsc-otel-instrumentation - automatic, non-invasive OpenTelemetry instrumentation for your Express project running on Node.js runtime.
Primary Goals
- Show how to achieve clean business logic without manual instrumentation
- Provide a complete example of
@waitingliou/tsc-otel-instrumentationintegration - Demonstrate basic observability stack (traces + logs)
Who Is This For?
- Developers learning OpenTelemetry and automatic instrumentation
- Teams, Organizations seeking non-invasive tracing approaches
- Zero Code Pollution: Business logic remains free of tracing code
- Clean Architecture: Clear separation of concerns
- Dependency Injection: Proper service composition
- Type Safety: Full TypeScript support
- Automatic Tracing: Services and repositories automatically traced
- Structured Logging: Pino logger with OpenTelemetry context
- Production Ready: Proper error handling and logging
| Component | Technology | Version | Purpose |
|---|---|---|---|
| Web Framework | Express | 4.x+ | Fast, minimalist web framework |
| Runtime | Node.js | 18.0+ | JavaScript runtime |
| Language | TypeScript | 5.0+ | Type-safe development |
| Compiler | TypeScript Compiler (tsc) | 5.0+ | |
| Instrumentation | @waitingliou/tsc-otel-instrumentation | 1.1.7+ | Automatic class method tracing |
| Observability | OpenTelemetry | Latest | Distributed tracing standard |
| Trace Backend | Grafana Cloud Tempo | Cloud | Trace storage and visualization |
This example follows Clean Architecture principles to demonstrate real-world usage:
┌────────────────────────────────────────┐
│ Controllers (HTTP Handlers) │
│ - Handle requests/responses │
└─────────────┬──────────────────────────┘
│ Dependency Injection
┌─────────────▼──────────────────────────┐
│ Use Cases (Business Logic Services) │ ← Automatically instrumented
│ - UserService, NotificationService │
└─────────────┬──────────────────────────┘
│ Interfaces (Ports)
┌─────────────▼──────────────────────────┐
│ Repositories (Data Access) │ ← Automatically instrumented
│ - UserRepository, SocialLinkRepo │
└────────────────────────────────────────┘
- Node.js >= 18.0.0
- npm or yarn
- Grafana Cloud account (free tier available)
# Install dependencies
npm installCreate .env file:
# Application Configuration
NODE_ENV=production
PORT=3003
# OpenTelemetry - Get from Grafana Cloud → Connections → OTLP
OTEL_EXPORTER_OTLP_ENDPOINT=...
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <your-base64-token>"
# Grafana Cloud Credentials (for logs if using Loki)
GRAFANA_HOST=https://logs-prod-XXX.grafana.net
GRAFANA_USERNAME=123456
GRAFANA_CLOUD_TOKEN=glc_xxxxxxxxxxxxxxxxxxxxxHow to get credentials:
- Visit grafana.com and sign up
- Navigate to Connections → Add new connection
- Select OpenTelemetry (OTLP) for traces
- Select Loki for logs
- Copy credentials to
.env
npm run devServer starts at http://localhost:3003
GET /healthPOST /users/signup
Content-Type: application/json
{
"name": "John Doe",
"email": "[email protected]"
}The magic happens in tsconfig.json:
{
"compilerOptions": {
"plugins": [
{
"transform": "@waitingliou/tsc-otel-instrumentation/transformer",
"include": [
"**/*service.ts",
"**/*repository.ts"
],
"instrumentPrivateMethods": true,
"spanNamePrefix": "Demo",
"autoInjectTracer": true
}
]
}
}This configuration automatically instruments:
- All classes in files matching
*service.tspattern - All classes in files matching
*repository.tspattern - Both public and private methods
- Generates spans with "Demo" prefix
Compare traditional manual instrumentation vs. automatic:
❌ Manual (Invasive)
async signup(name: string, email: string): Promise<string> {
const span = tracer.startSpan('UserService.signup');
try {
// business logic
span.end();
} catch (error) {
span.recordException(error);
span.end();
throw error;
}
}✅ Automatic (Clean)
async signup(name: string, email: string): Promise<string> {
// Just pure business logic!
const validationResult = await this.emailValidationService.validateEmail(email);
if (!validationResult.isValid) {
throw new Error(`Email validation failed`);
}
return await this.usersRepository.signup(name, email);
}Happy tracing! 🎉
Achieve application-level transparency in your TypeScript projects with zero code pollution.
