From andercore-toolkit-services
Tracing, logging, and metrics with @andercore/toolkit. Use when working with observability, traces, logs, or metrics.
npx claudepluginhub andercore-labs/claudes-kitchen --plugin andercore-toolkit-servicesThis skill uses the workspace's default tool permissions.
| Need | Pattern | Location |
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
| Need | Pattern | Location |
|---|---|---|
| Trace method | @Trace() | service.ts:15 |
| Buffered log | @UseInterceptors(LoggerInterceptor) | controller.ts:10 |
| Standalone log | Logger.create('Service') | service.ts:8 |
| Manual span | tracingService.withSpan('name', fn) | service.ts:25 |
| Config metrics | MetricsModule.forRoot({ metrics: [...] }) | app.module.ts:10 |
| Inject metric | @Metric('metric.name') | service.ts:15 |
| Get metric | metricsService.getMetric(name) | service.ts:20 |
Implementing observability | adding traces | configuring logging | collecting metrics | monitoring
→ backend-services:hexagonal-architecture-recipe for layer boundaries
Configuration-driven metrics (MetricsModule) follow hexagonal principles:
package.json:
{
"scripts": {
"start:prod": "node --require @andercore/toolkit/dist/instrumentation.js dist/main.js"
}
}
.env:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_RESOURCE_ATTRIBUTES=service.name=api,service.version=1.0.0,deployment.environment=production
| Variable | Required | Purpose |
|---|---|---|
| OTEL_EXPORTER_OTLP_ENDPOINT | ✓ | Collector endpoint |
| OTEL_RESOURCE_ATTRIBUTES | ✓ | Service metadata |
| OTEL_LOG_LEVEL | ✗ | SDK diagnostics (default: info) |
| Pattern | Implementation | Outcome |
|---|---|---|
| Decorator | @Trace() above method | ✓ Auto span + SemConv attributes |
| Manual | tracingService.withSpan() | ✓ Custom span control |
| HTTP auto | HttpService injected | ✓ Zero-config HTTP tracing |
✓ DO:
@Injectable()
export class UserService {
@Trace()
async createUser(email: string): Task<User, ServiceError> {
return this.repo.save({ email })
}
@Trace('user-validation')
validateUser(user: User): Result<User, ValidationError> {
return validateEmail(user.email)
}
}
Automatic attributes:
code.function: Method namecode.namespace: Class nameComplex spans with events:
@Injectable()
export class PaymentService {
constructor(private tracing: TracingService) {}
processPayment(amount: number): Task<Payment, PaymentError> {
return this.tracing.withSpan('payment-processing', async (span) => {
span.setAttributes({
'payment.amount': amount,
'payment.currency': 'USD'
})
span.addEvent('validation-started')
const validated = await this.validate(amount)
span.addEvent('validation-completed')
span.addEvent('charging-started')
const result = await this.charge(validated)
span.addEvent('charging-completed')
return result
})
}
}
Zero-config for @nestjs/axios:
@Injectable()
export class UserApiService {
constructor(private httpService: HttpService) {}
async fetchUser(id: string) {
// Automatically traced with SemConv HTTP attributes
return firstValueFrom(this.httpService.get(`/users/${id}`))
}
}
Automatic attributes (SemConv):
http.request.methodurl.full, url.path, url.queryserver.address, server.porthttp.response.status_codeerror.type (on failures)✗ DON'T - Direct OTel imports outside observability:
// src/service/user/user.service.ts
import { trace } from '@opentelemetry/api' // ✗ VIOLATION
@UseInterceptors(LoggerInterceptor)
@Controller('users')
export class UsersController {
constructor(private logger: Logger) {}
@Post()
async create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
this.logger.info('Validating user data', { email: dto.email })
this.logger.debug('Checking email format', { email: dto.email })
const user = await this.service.create(dto)
this.logger.info('User created', {
userId: user.id,
email: user.email,
createdAt: new Date()
})
return this.toDto(user)
}
}
Logs flushed with HTTP context:
http.request.id (or generated UUID)http.request.methodurl.pathhttp.response.status_codetrace_id, span_id (automatic correlation)enduser.id (from request.user.id)organization.id (from x-tenant-id header)Flush triggers:
flushLevel (default: 'error')bufferSize (default: 1000)@Injectable()
export class AppService {
private logger = Logger.create('AppService')
onModuleInit() {
// Written immediately (not buffered)
this.logger.info('Application initialized', {
environment: process.env.NODE_ENV,
version: '1.0.0'
})
}
@Cron('0 0 * * *')
async dailyReport() {
this.logger.info('Generating daily report')
await this.generateReport()
this.logger.info('Daily report completed')
}
}
OTLP compliance - nested objects flattened:
this.logger.info('User created', {
userId: '123',
userData: {
email: 'user@example.com',
profile: { firstName: 'John' }
},
tags: ['premium', 'verified']
})
// Flattened attributes:
// meta.userId: "123"
// meta.userData.email: "user@example.com"
// meta.userData.profile.firstName: "John"
// meta.tags: '["premium","verified"]'
app.module.ts:
AndercoreToolkitModule.forRoot({
logger: {
flushLevel: 'warn',
bufferSize: 500,
bufferTTL: 3 * 60 * 1000
}
})
| Option | Default | Effect |
|---|---|---|
| flushLevel | 'error' | Flush on warn+ → immediate output |
| bufferSize | 1000 | Max logs per buffer |
| bufferTTL | 5min | Buffer expiry time |
| maxCacheSize | 500 | Max cached loggers (LRU) |
Use configuration-driven approach - MetricsModule.forRoot() + @Metric()
See metrics-architecture.md for complete patterns.
Module setup (→ typescript-services:true-myth-recipe for Maybe monad patterns):
// app.module.ts
import { MetricsModule, SEMCONV_UNITS, SEMCONV_ANNOTATIONS } from '@andercore/toolkit'
import { Maybe } from 'true-myth'
@Module({
imports: [
MetricsModule.forRoot({
metrics: [
{
name: 'app.requests',
type: 'counter',
description: Maybe.just('Total HTTP requests'),
unit: Maybe.just(SEMCONV_ANNOTATIONS.REQUEST),
valueType: Maybe.just('int')
},
{
name: 'app.request.duration',
type: 'histogram',
description: Maybe.just('Request processing duration'),
unit: Maybe.just(SEMCONV_UNITS.SECONDS),
valueType: Maybe.just('double')
}
],
global: Maybe.just(true),
meterName: Maybe.just('my-app.service'),
meterVersion: Maybe.just('1.0.0')
})
]
})
export class AppModule {}
Pattern 1: Decorator injection (recommended):
@Injectable()
export class PaymentService {
constructor(
@Metric('app.requests') private readonly requestCounter: Counter,
@Metric('app.request.duration') private readonly durationHistogram: Histogram
) {}
processPayment(amount: number): Task<Payment, PaymentError> {
this.requestCounter.add(1, { endpoint: '/payment', status: 'success' })
const start = Date.now()
// ... business logic
const durationSeconds = (Date.now() - start) / 1000
this.durationHistogram.record(durationSeconds, { endpoint: '/payment' })
return Task.succeed(payment)
}
}
Pattern 2: MetricsService with Result monad (→ typescript-services:true-myth-recipe for Result/Task patterns):
@Injectable()
export class OrderService {
constructor(private metricsService: MetricsService) {}
createOrder(order: Order): Task<Order, ServiceError> {
return this.metricsService.getMetric<Counter>('app.requests')
.match({
Ok: (counter) => {
counter.add(1, { orderId: order.id })
return Task.succeed(order)
},
Err: (error) => Task.fail(ServiceError.from(error.message))
})
}
}
Validation rules:
| Rule | Pattern | Valid | Invalid |
|---|---|---|---|
| Metric name | Lowercase dot notation | app.requests | App.Requests |
| Underscores | Within components | http.response_time | Leading/trailing |
| Consecutive dots | Forbidden | http.server.duration | http..server |
| Reserved namespace | Forbidden | app.metrics | otel.metric |
| .total suffix | Forbidden | app.requests | app.requests.total |
| Meter name | 1-255 chars, lowercase dot-dash | my-app.service | MyApp |
| Meter version | Semver | 1.0.0 | 1.0 |
Error types:
MetricValidationError // Invalid config
MetricNotFoundError // Metric not registered
DuplicateMetricError // Duplicate name
InvalidMetricTypeError // Invalid type
Exports:
import {
MetricsModule,
MetricsService,
Metric, // decorator
SEMCONV_UNITS,
SEMCONV_ANNOTATIONS,
MetricValidationError,
MetricNotFoundError
} from '@andercore/toolkit'
import type {
Counter,
Histogram,
UpDownCounter,
ObservableGauge,
MetricsModuleOptions,
MetricConfig
} from '@andercore/toolkit'
Reference: https://opentelemetry.io/docs/specs/semconv/ (v1.27.0)
| Type | Pattern | Example |
|---|---|---|
| Metrics | namespace.metric_name.unit | user.created.total, payment.duration.ms |
| Attributes | namespace.attribute_name | user.id, user.role, http.request.method |
| Spans | Operation-based | GET /users/{id}, UserService.createUser |
Standard attributes (prefer over custom):
http.request.method
http.response.status_code
url.path
server.address
error.type
Custom attributes (follow semconv naming):
✓ user.role (namespaced, snake_case)
✓ payment.amount (namespaced, snake_case)
✓ tenant.tier (namespaced, snake_case)
✗ userId (camelCase - VIOLATION)
✗ request_method (non-standard - use http.request.method)
Rule: Metric attributes MUST have <100 unique values
✗ FORBIDDEN - High cardinality:
// Millions of unique values
this.counter.add(1, { 'user.id': userId }) // ✗ CRITICAL
this.counter.add(1, { 'user.email': email }) // ✗ CRITICAL
this.counter.add(1, { 'request.id': requestId }) // ✗ CRITICAL
this.counter.add(1, { 'timestamp': Date.now() }) // ✗ CRITICAL
this.counter.add(1, { 'url.full': fullUrl }) // ✗ CRITICAL (unbounded paths)
✓ ALLOWED - Bounded cardinality:
// <100 unique values
this.counter.add(1, { 'user.role': 'admin' }) // ✓ <10 roles
this.counter.add(1, { 'http.response.status_code': 200 }) // ✓ ~50 codes
this.counter.add(1, { 'payment.method': 'card' }) // ✓ <20 methods
this.counter.add(1, { 'tenant.tier': 'premium' }) // ✓ <10 tiers
Detection:
Dynamic IDs as attributes → CRITICAL
Email/username as attributes → CRITICAL
Timestamps as attributes → CRITICAL
Full URLs as attributes → CRITICAL
Unbounded strings → CRITICAL
Toolkit provides auto-collected metrics:
Attributes: http.request.method, http.response.status_code, url.path, server.address
→ typescript-services:test-code-recipe for patterns | Mock metrics adapters | Testcontainers for OTLP
See validation-checklist.md for complete checklist and violation patterns.
See metrics-architecture.md for: