One log call. Complete observability.
Stop writing separate code for logs, metrics, and traces. Write one log entry and automatically get:
- ✅ Structured logs (Azure Log Analytics, Loki, or local files)
- ✅ Metrics dashboards (Azure Monitor, Prometheus, Grafana)
- ✅ Distributed traces (Azure Application Insights, Tempo)
- ✅ Service dependency maps (automatic correlation)
You write code for yourself during development. But you write logs for the operations engineer staring at a screen at 7 PM on Friday.
Picture this: Your application just crashed in production. Everyone on your team has left for the weekend. The ops engineer who got the alert doesn't know your codebase, doesn't know your business logic, and definitely doesn't want to be there right now. They're trying to piece together what went wrong from cryptic error messages and scattered log entries.
Make their job easy.
Good logging is the difference between:
- ❌ "Some null reference exception occurred somewhere" (cue 3-hour debugging session)
- ✅ "User authentication failed for email 'john@company.com' - invalid password attempt #3, account locked for security" (fixed in 5 minutes)
When you write clear, contextual logs, you're not just debugging future problems—you're earning respect. That ops engineer will look up who wrote this beautifully logged code and think: "Now THIS is a developer who knows what they're doing."
Help them get home to their family. Help yourself build a reputation as someone who writes production-ready code.
Your future self (and your colleagues) will thank you.
// Traditional approach: 20+ lines per operation
logger.info('Payment processed', { orderId: '123' });
paymentCounter.inc();
paymentDuration.observe(duration);
const span = tracer.startSpan('processPayment');
span.setAttributes({ orderId: '123' });
span.end();
// ... manually correlate logs, metrics, traces// sovdev-logger: 1 line, complete observability
const FUNCTIONNAME = 'processPayment';
const input = { orderId: '123', amount: 99.99 };
const output = { transactionId: 'tx-456', status: 'approved' };
sovdev_log(INFO, FUNCTIONNAME, 'Payment processed', PEER_SERVICES.PAYMENT_GATEWAY, input, output);
// ↑ Automatic logs + metrics + traces + correlationResult: 95% less instrumentation code, complete observability out of the box.
npm install @sovdev/loggerCreate test.ts:
import { sovdev_initialize, sovdev_log, sovdev_flush, SOVDEV_LOGLEVELS, create_peer_services } from '@sovdev/logger';
// INTERNAL is auto-generated, just pass empty object if no external systems
const PEER_SERVICES = create_peer_services({});
async function main() {
const FUNCTIONNAME = 'main';
// Initialize
sovdev_initialize('my-app');
// Log with full context
const input = { userId: '123', action: 'processOrder' };
const output = { orderId: '456', status: 'success' };
sovdev_log(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
'Order processed successfully',
PEER_SERVICES.INTERNAL,
input,
output
);
// Flush before exit (CRITICAL!)
await sovdev_flush();
}
main().catch(console.error);npx tsx test.ts- ✅ Console: Human-readable colored output
- ✅ File: Structured JSON in
./logs/dev.log - 📊 Want Grafana dashboards? → See Configuration
┌─────────────────────────────────────────────────────┐
│ Your Code: sovdev_log(...) │
│ ↓ │
│ One Log Call │
└──────────────┬──────────────────────────────────────┘
│
┌──────────┼──────────┬──────────┐
↓ ↓ ↓ ↓
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Logs │ │Metrics │ │Traces │ │ File │
│Azure LA│ │Azure │ │App │ │ (JSON) │
│ Loki │ │Monitor │ │Insights│ │ │
│ │ │Grafana │ │ Tempo │ │ │
└────────┘ └────────┘ └────────┘ └────────┘
Every sovdev_log() call generates:
- Logs: Structured JSON with full context (what happened, input, output)
- Metrics: Counters, histograms, gauges for Azure Monitor, Prometheus, or Grafana
- Traces: Distributed tracing spans with automatic correlation (Azure Application Insights, Tempo)
- Service Maps: Automatic dependency graphs showing system-to-system calls
- File Logs: Optional JSON files for local development and debugging
No extra code required.
All log entries follow a consistent structure with snake_case field naming (underscores, not dots or camelCase):
{
"event_id": "10a1d43f-bd70-4581-8e30-c6fa60160ff0",
"service_name": "my-app",
"service_version": "1.0.0",
"function_name": "processPayment",
"level": "info",
"log_type": "transaction",
"message": "Payment processed successfully",
"timestamp": "2025-10-10T19:38:39.109Z",
"trace_id": "a17e6a44986c4581a13e19d6b0a9b295",
"span_id": "b7e53ae49dd3b969",
"peer_service": "SYS2034567",
"input_json": {
"orderId": "123",
"amount": 99.99
},
"response_json": {
"transactionId": "tx-456",
"status": "approved"
}
}{
"event_id": "b73f6657-7731-453b-9d17-f61a6da52a71",
"service_name": "my-app",
"service_version": "1.0.0",
"function_name": "processPayment",
"level": "error",
"log_type": "transaction",
"message": "Payment failed",
"timestamp": "2025-10-10T19:38:40.440Z",
"trace_id": "5c50f2b84f562949abcedb71298cd39a",
"span_id": "b7f6c8e6b794f83f",
"peer_service": "SYS2034567",
"exception_type": "Error",
"exception_message": "HTTP 404: Payment gateway unavailable",
"exception_stacktrace": "Error: HTTP 404...\n at processPayment (/app/payment.ts:50:20)\n ...",
"input_json": {
"orderId": "123",
"amount": 99.99
},
"response_json": {
"status": "failed"
}
}{
"event_id": "d2f77136-f6c8-499a-b1ac-4d8e09c7501d",
"service_name": "my-app",
"function_name": "importUsers",
"level": "info",
"log_type": "job.status",
"message": "Job Started: UserImportJob",
"timestamp": "2025-10-10T19:38:39.110Z",
"trace_id": "3024d80d06a74509a212baca13870433",
"peer_service": "my-app",
"input_json": {
"job_name": "UserImportJob",
"job_status": "Started",
"totalUsers": 5000
},
"response_json": null
}{
"event_id": "41247f72-6b93-4c26-92ed-d58cb20e3851",
"service_name": "my-app",
"function_name": "importUsers",
"level": "info",
"log_type": "job.progress",
"message": "Processing user-123 (45/5000)",
"timestamp": "2025-10-10T19:38:39.111Z",
"trace_id": "5f0b0d8227f94217bb3028920d2cb07e",
"peer_service": "my-app",
"input_json": {
"job_name": "UserImportJob",
"item_id": "user-123",
"current_item": 45,
"total_items": 5000,
"progress_percentage": 0.9
},
"response_json": null
}Key Field Naming Rules:
- ✅ Use underscores:
service_name,function_name,trace_id,span_id - ✅ Flat structure:
exception_type, NOTexception.type - ✅ Consistent casing: All field names are lowercase with underscores
- ❌ Never use dots: Avoid
service.nameor nested structures for standard fields - ❌ Never use camelCase: Avoid
serviceNameorfunctionName
Why snake_case?
- OpenTelemetry automatically converts dot notation to underscores when storing in backends
- Using snake_case directly avoids transformation inconsistencies
- Ensures fields are stored and retrieved with the same names
- Simplifies querying in Grafana, Loki, and Prometheus
"I only know Azure Monitor and Application Insights..."
Good news! This library uses OpenTelemetry - Microsoft's recommended standard for observability. Your code works with both Azure and open-source tools:
// Same code works everywhere
sovdev_log(INFO, FUNCTIONNAME, 'Order processed', PEER_SERVICES.INTERNAL, input, output);Where your logs go:
| Environment | Logs | Metrics | Traces |
|---|---|---|---|
| Azure Production | Azure Log Analytics | Azure Monitor | Application Insights |
| Local Development | Console + JSON files | Grafana (optional) | Tempo (optional) |
| On-Premises | Loki | Prometheus | Tempo |
Key benefits for Azure developers:
- ✅ No vendor lock-in: Write once, deploy anywhere (Azure, AWS, on-prem)
- ✅ Local testing: Full observability stack on your laptop (no cloud costs)
- ✅ Azure-compatible: OpenTelemetry Protocol (OTLP) works with Azure Monitor
- ✅ Future-proof: Microsoft recommends OpenTelemetry for new applications
Want Azure integration? See Configuration for Azure below.
Choose your path:
| Goal | Next Section |
|---|---|
| 📖 Just logging to console/file? | You're done! Keep using sovdev_log() |
| ☁️ Send to Azure Monitor? | Configuration for Azure |
| 📊 Send to Grafana/Loki/Tempo? | Configuration |
| 🔄 Processing batches/jobs? | Batch Job Pattern |
| 🔗 Link related operations? | Using traceId |
| 🤔 Understand how it works? | How It Works |
// Define peer services once at the top of your file
const PEER_SERVICES = create_peer_services({
PAYMENT_GATEWAY: 'SYS2034567' // External payment system (system ID)
// INTERNAL is auto-generated - no need to declare it
});
async function processPayment(orderId: string, amount: number) {
// BEST PRACTICE: Use const FUNCTIONNAME at the start of every function
const FUNCTIONNAME = 'processPayment';
// Capture input BEFORE the operation
const input = { orderId, amount, currency: 'USD' };
try {
// Call external system
const result = await paymentGateway.charge(orderId, amount);
// Capture output AFTER the operation
const output = { transactionId: result.id, status: 'approved' };
// Log success: input + output = complete audit trail
sovdev_log(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
'Payment processed successfully',
PEER_SERVICES.PAYMENT_GATEWAY, // Track external dependency
input,
output
);
return result;
} catch (error) {
// Capture error output
const output = { status: 'failed', reason: error.message };
// Log failure: input + output + exception
sovdev_log(
SOVDEV_LOGLEVELS.ERROR,
FUNCTIONNAME,
'Payment failed',
PEER_SERVICES.PAYMENT_GATEWAY, // Still track peer service on error
input,
output,
error // Exception object for stack trace
);
throw error;
}
}Key Points:
- ✅
const FUNCTIONNAME- Makes finding bugs easier - ✅
const input- Explicit variable names show what data went in - ✅ Track peer service even on errors - Creates service dependency graphs
- ✅ Log both input AND output - Complete audit trail
async function importUsers(users: User[]) {
const FUNCTIONNAME = 'importUsers';
// 1. Log job START - marks beginning of batch operation
sovdev_log_job_status(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
'UserImportJob', // Job name (for filtering in Grafana)
'Started', // Job status
PEER_SERVICES.INTERNAL, // Use INTERNAL for batch jobs (not calling external systems)
{ totalUsers: users.length, source: 'CSV' }
);
// Track success/failure counts
let successCount = 0;
let failureCount = 0;
// 2. Process each item and log PROGRESS
for (let i = 0; i < users.length; i++) {
const user = users[i];
try {
await createUser(user);
successCount++;
// ✅ BEST PRACTICE: Log progress every N items (avoid log spam)
if ((i + 1) % 10 === 0 || i === users.length - 1) {
sovdev_log_job_progress(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
user.id, // itemId: Identifier for the item being processed (shows in logs)
i + 1, // currentItem: How many items processed so far (1, 2, 3...)
users.length, // totalItems: Total number of items in batch (calculates %)
PEER_SERVICES.INTERNAL, // Batch processing is internal
{ successCount, failureCount }
);
}
} catch (error) {
failureCount++;
// ✅ IMPORTANT: Always log individual failures (don't skip errors)
sovdev_log_job_progress(
SOVDEV_LOGLEVELS.ERROR,
FUNCTIONNAME,
user.id, // itemId: Which user failed (for troubleshooting)
i + 1, // currentItem: Position in batch where failure occurred
users.length, // totalItems: Total batch size (shows % failed)
PEER_SERVICES.INTERNAL, // Still INTERNAL even on error
{ email: user.email, error: error.message }
);
}
}
// 3. Log job COMPLETION - marks end of batch with final statistics
sovdev_log_job_status(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
'UserImportJob',
'Completed', // Job status
PEER_SERVICES.INTERNAL, // Batch job completion is internal
{ totalUsers: users.length, successCount, failureCount }
);
}Result in Grafana:
- Query:
{job_name="UserImportJob"}shows job lifecycle (Started → Progress → Completed) - See which specific users failed: filter by ERROR level
- Calculate success rate:
successCount / totalUsers
Use Case: Process one company through multiple steps (lookup → validate → save). You want all 3 operations grouped together in Grafana.
import { sovdev_generate_trace_id } from '@sovdev/logger';
async function processCompany(orgNumber: string) {
const FUNCTIONNAME = 'processCompany';
// IMPORTANT: Generate ONE traceId at the start - use it for ALL operations
const companyTraceId = sovdev_generate_trace_id();
// Step 1: Lookup company in external registry (BRREG)
const input1 = { organisasjonsnummer: orgNumber };
const companyData = await lookupInBREG(orgNumber);
const output1 = { name: companyData.name };
sovdev_log(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
'Company found',
PEER_SERVICES.BRREG, // External system call
input1,
output1,
null, // No exception
companyTraceId // ← Links this operation to the company
);
// Step 2: Validate data (internal operation)
const isValid = validateCompany(companyData);
const input2 = { name: companyData.name };
const output2 = { valid: isValid };
sovdev_log(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
'Validation complete',
PEER_SERVICES.INTERNAL, // Internal operation
input2,
output2,
null,
companyTraceId // ← Same traceId links to step 1
);
// Step 3: Save to database
await saveCompany(companyData);
const input3 = { organisasjonsnummer: orgNumber };
const output3 = { saved: true };
sovdev_log(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
'Company saved',
PEER_SERVICES.DATABASE, // Database call
input3,
output3,
null,
companyTraceId // ← Same traceId links all 3 steps together
);
}Result in Grafana:
Query: {traceId="company-abc123"}
Shows ALL 3 operations (all share traceId "company-abc123"):
├─ lookupCompany → BRREG (200ms)
├─ validateCompany → INTERNAL (5ms)
└─ saveCompany → Database (50ms)
Total duration: 255ms
Complete flow for this company visible in one view!
When to use traceId:
- ✅ Processing one item through multiple steps (read → transform → write)
- ✅ Transaction flows where you want to see the complete journey
- ✅ Debugging: "Show me everything that happened for company X"
- ❌ Single, independent operations (library auto-generates traceId)
async function main() {
sovdev_log(INFO, FUNCTIONNAME, 'Test', PEER_SERVICES.INTERNAL, input);
// Missing: await sovdev_flush();
}
// Result: Last logs lost!Fix: Always call await sovdev_flush() before exit.
async function main() {
sovdev_log(INFO, FUNCTIONNAME, 'Test', PEER_SERVICES.INTERNAL, input);
await sovdev_flush(); // ✅
}
main().catch(async (error) => {
console.error('Fatal error:', error);
await sovdev_flush(); // ✅ Flush even on error!
process.exit(1);
});// ❌ Wrong - hardcoded string (typo-prone)
function processPayment() {
sovdev_log(INFO, 'proccessPayment', msg, PEER_SERVICES.PAYMENT_GATEWAY, input, output); // Typo!
}
// ✅ Correct - use constant
function processPayment() {
const FUNCTIONNAME = 'processPayment';
sovdev_log(INFO, FUNCTIONNAME, msg, PEER_SERVICES.PAYMENT_GATEWAY, input, output);
}Why:
- Prevents typos
- Makes refactoring easier
- Consistent pattern across all functions
- Easier to search logs by function name
// ❌ Wrong - hardcoded string (no type safety, hard to maintain)
sovdev_log(INFO, FUNCTIONNAME, msg, 'SYS1234567', input, output);
// ✅ Correct - use PEER_SERVICES constants
const PEER_SERVICES = create_peer_services({
BRREG: 'SYS1234567' // INTERNAL auto-generated
});
sovdev_log(INFO, FUNCTIONNAME, msg, PEER_SERVICES.BRREG, input, output);Why:
- Type-safe: IDE autocomplete and compile-time checks
- Single source of truth for external system names
- Easy to update when system IDs change
- Creates automatic service dependency maps in Grafana
API Naming Convention:
TypeScript uses snake_case function names (sovdev_log, sovdev_initialize, create_peer_services) for consistency with Python implementation and the specification. All field names are snake_case across all languages (service_name, function_name, trace_id, peer_service).
sovdev_initialize(
serviceName: string,
serviceVersion?: string,
peerServices: Record<string, string>
): voidInitialize the logger with service information and peer system mappings. Must be called once at application startup.
Parameters:
-
serviceName(required) - Unique identifier for your service- Examples:
"user-service","payment-api","company-lookup"
- Examples:
-
serviceVersion(optional) - Version of your service- Auto-detected from:
SERVICE_VERSIONenv var,npm_package_version,package.json - Falls back to
"unknown"
- Auto-detected from:
-
peerServices- PassPEER_SERVICES.mappings- Tells the logger which external systems (peer services) your application calls
- Enables service dependency maps in Grafana showing which external systems you call
- Example:
{ BRREG: 'INT1001234', ALTINN: 'INT1005678' }(INTERNAL auto-added) - Always use
PEER_SERVICES.mappings- don't manually create this object
Example:
// .env file:
// OTEL_SERVICE_NAME=my-company-lookup-service ← Your service name (OpenTelemetry standard)
// BRREG_SYSTEM_ID=INT1001234 ← External system ID
// ALTINN_SYSTEM_ID=INT1005678 ← External system ID
// Define which external systems (peer services) your app calls
const PEER_SERVICES = create_peer_services({
// INTERNAL is auto-generated - no need to declare it!
BRREG: process.env.BRREG_SYSTEM_ID!, // External system: INT1001234
ALTINN: process.env.ALTINN_SYSTEM_ID! // External system: INT1005678
});
// Initialize with YOUR service name and peer services
sovdev_initialize(
process.env.OTEL_SERVICE_NAME!, // 'my-company-lookup-service' (OpenTelemetry standard)
'1.0.0', // Your version
PEER_SERVICES.mappings // INTERNAL auto-added as 'my-company-lookup-service'
);
// Now you can use PEER_SERVICES.INTERNAL in your logs!
// It will automatically resolve to 'my-company-lookup-service'sovdev_log(
level: sovdev_log_level,
functionName: string,
message: string,
peerService: string,
inputJSON?: any,
responseJSON?: any,
exceptionObject?: any,
traceId?: string
): voidGeneral purpose logging function that captures complete operation context.
Parameters:
level- Log severity fromSOVDEV_LOGLEVELS(DEBUG, INFO, WARN, ERROR, FATAL)functionName- Name of the function/operation being logged- Best practice: Use
const FUNCTIONNAME = 'functionName'
- Best practice: Use
message- Human-readable description of what happenedpeerService(required) - The external system/service you're calling- Use
PEER_SERVICES.INTERNALfor internal operations - Use
PEER_SERVICES.PAYMENT_GATEWAY,PEER_SERVICES.DATABASE, etc. for external calls - Why: Creates automatic service dependency graphs in Grafana
- Use
inputJSON(optional) - Data that went INTO the operationresponseJSON(optional) - Data that came OUT of the operationexceptionObject(optional) - Error or exception object if operation failedtraceId(optional, advanced) - Manual trace correlation ID- 99% of developers should omit this - it's auto-generated!
- Only use to manually correlate operations (see Using traceId)
Example:
const FUNCTIONNAME = 'processPayment';
const input = { orderId: '123', amount: 99.99 };
const output = { transactionId: 'tx-456', status: 'approved' };
sovdev_log(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
'Payment processed successfully',
PEER_SERVICES.PAYMENT_GATEWAY,
input,
output
);sovdev_log_job_status(
level: sovdev_log_level,
functionName: string,
jobName: string,
status: string,
peerService: string,
inputJSON?: any,
traceId?: string
): voidTrack the lifecycle of long-running jobs or batch processes (start, completion, failure).
Parameters:
status- Job lifecycle state:'Started','Completed','Failed','Cancelled'jobName- Unique job identifier (e.g.,'UserSyncJob','DailyBackup')
Example:
const FUNCTIONNAME = 'syncUserData';
// Job start
sovdev_log_job_status(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
'UserSyncJob',
'Started',
PEER_SERVICES.INTERNAL,
{ source: 'ActiveDirectory', totalUsers: 5000 }
);
// ... process users ...
// Job completion
sovdev_log_job_status(
SOVDEV_LOGLEVELS.INFO,
FUNCTIONNAME,
'UserSyncJob',
'Completed',
PEER_SERVICES.INTERNAL,
{ usersProcessed: 5000, duration: '45s' }
);sovdev_log_job_progress(
level: sovdev_log_level,
functionName: string,
itemId: string,
current: number,
total: number,
peerService: string,
inputJSON?: any,
traceId?: string
): voidShow progress when processing batches, arrays, or large datasets. Creates "Processing item 45 of 100" style logs.
Parameters:
itemId- Current item identifier (e.g.,userId,orderId,fileName)current- Current item number (1-based counting: 1, 2, 3...)total- Total number of items to process
Example:
for (let i = 0; i < users.length; i++) {
const user = users[i];
await createUser(user);
sovdev_log_job_progress(
SOVDEV_LOGLEVELS.INFO,
'importUsers',
user.id,
i + 1,
users.length,
PEER_SERVICES.INTERNAL,
{ email: user.email, status: 'created' }
);
}async sovdev_flush(): Promise<void>Flush logs to ensure they are sent to OTLP collector. Must be called before application exit.
Why This Is Critical:
OpenTelemetry uses a BatchLogRecordProcessor which batches logs for performance. When your application exits, any logs still in the batch buffer will be lost unless you explicitly flush them.
Best Practice:
async function main() {
sovdev_initialize('my-service');
// ... your application code ...
// Always flush before exit
await sovdev_flush();
}
main().catch(async (error) => {
console.error('Fatal error:', error);
await sovdev_flush(); // Flush even on error!
process.exit(1);
});sovdev_validate_config(): {
valid: boolean;
missing: string[];
warnings: string[];
config: {
serviceName: string | undefined;
logsEndpoint: string | undefined;
metricsEndpoint: string | undefined;
tracesEndpoint: string | undefined;
headers: string | undefined;
protocol: string | undefined;
};
}Validate that all required OpenTelemetry environment variables are set and properly formatted.
Returns:
valid-trueif all required variables are set,falseotherwisemissing- Array of missing required environment variable nameswarnings- Array of configuration warningsconfig- Current configuration values
Checks for Required Variables:
OTEL_SERVICE_NAMEOTEL_EXPORTER_OTLP_LOGS_ENDPOINTOTEL_EXPORTER_OTLP_METRICS_ENDPOINTOTEL_EXPORTER_OTLP_TRACES_ENDPOINTOTEL_EXPORTER_OTLP_HEADERS(must be valid JSON with Host header)
Example:
import { sovdev_validate_config, sovdev_initialize } from '@sovdev/logger';
// Validate configuration before initialization
const validation = sovdev_validate_config();
if (!validation.valid) {
console.warn('⚠️ OTLP configuration incomplete:');
validation.missing.forEach(v => console.warn(` - ${v}`));
console.warn(' File logging will work, but OTLP export may be disabled.');
}
if (validation.warnings.length > 0) {
console.warn('⚠️ Configuration warnings:');
validation.warnings.forEach(w => console.warn(` - ${w}`));
}
// Proceed with initialization (file logging still works without OTLP)
sovdev_initialize('my-service', '1.0.0');When to Use:
- ✅ During development to verify .env file is configured correctly
- ✅ In deployment scripts to validate environment before starting service
- ✅ When debugging "why aren't logs appearing in Loki/Prometheus/Tempo?"
- ❌ NOT required for normal application operation
async sovdev_test_otlp_connection(timeout?: number): Promise<{
success: boolean;
logs: { reachable: boolean; error?: string };
metrics: { reachable: boolean; error?: string };
traces: { reachable: boolean; error?: string };
}>Test connectivity to all three OTLP endpoints by sending properly formatted test data.
Parameters:
timeout- Optional timeout in milliseconds (default: 5000ms)
Returns: Promise resolving to object containing:
success-trueif ALL three endpoints are reachablelogs- Connectivity result for logs endpointmetrics- Connectivity result for metrics endpointtraces- Connectivity result for traces endpoint
Example:
import { sovdev_test_otlp_connection, sovdev_initialize } from '@sovdev/logger';
// Test connectivity before initialization
console.log('🔌 Testing OTLP connectivity...');
const connectivityTest = await sovdev_test_otlp_connection(5000);
if (!connectivityTest.success) {
console.warn('⚠️ OTLP connectivity issues detected:');
if (!connectivityTest.logs.reachable) {
console.warn(` Logs: ${connectivityTest.logs.error}`);
}
if (!connectivityTest.metrics.reachable) {
console.warn(` Metrics: ${connectivityTest.metrics.error}`);
}
if (!connectivityTest.traces.reachable) {
console.warn(` Traces: ${connectivityTest.traces.error}`);
}
console.warn(' Proceeding anyway (file logging will still work)...');
} else {
console.log('✅ All OTLP endpoints reachable');
}
// Proceed with initialization
sovdev_initialize('my-service', '1.0.0');Common Errors:
- 404 Not Found - Usually indicates missing
Host: otel.localhostheader inOTEL_EXPORTER_OTLP_HEADERS - Connection refused - OTLP collector not running or wrong endpoint URL
- Timeout - Network issue or endpoint unreachable
When to Use:
- ✅ During development to verify OTLP collector is running and accessible
- ✅ In deployment health checks to validate infrastructure connectivity
- ✅ When debugging OTLP connection issues (404, connection refused, timeouts)
- ✅ In CI/CD pipelines to validate deployment environment
- ❌ NOT required for normal application operation
Why Three Separate Endpoints?
OpenTelemetry OTLP collector exposes three separate endpoints by design:
/v1/logs- Log records/v1/metrics- Metric data points/v1/traces- Trace spans
Each signal type has different structure and backend routing requirements. This is OpenTelemetry specification standard, not an implementation choice.
No configuration needed! Just install and use:
npm install @sovdev/loggerThe library will:
- ✅ Log to console (colored, human-readable)
- ✅ Log to files (JSON, structured in
./logs/) - ❌ Not send to OTLP (Grafana/Loki) yet
Optional: Enable file logging explicitly:
LOG_TO_FILE=true
LOG_FILE_PATH=./logs/app.log # Optional: custom path
ERROR_LOG_PATH=./logs/error.log # Optional: custom error pathUse Case: Running your Node.js app in Azure App Service, Container Apps, or AKS - sending observability data to Azure Monitor.
npm install @azure/monitor-opentelemetryUpdate your app initialization:
import { sovdev_initialize, sovdev_log, sovdev_flush, SOVDEV_LOGLEVELS, create_peer_services } from '@sovdev/logger';
import { useAzureMonitor } from '@azure/monitor-opentelemetry';
// Initialize Azure Monitor (reads APPLICATIONINSIGHTS_CONNECTION_STRING from env)
useAzureMonitor();
// Initialize sovdev-logger
const PEER_SERVICES = create_peer_services({
DATABASE: 'INT1234567',
PAYMENT_API: 'SYS7654321'
// INTERNAL is auto-generated
});
sovdev_initialize('my-azure-app', '1.0.0');
// Your application code...
async function main() {
const FUNCTIONNAME = 'main';
sovdev_log(SOVDEV_LOGLEVELS.INFO, FUNCTIONNAME, 'Application started', PEER_SERVICES.INTERNAL);
await sovdev_flush();
}Set environment variable in Azure (App Service → Configuration → Application Settings):
APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=xxxxx-xxxx-xxxx-xxxx-xxxxxxxxx;IngestionEndpoint=https://...That's it! Your sovdev_log() calls now send:
- ✅ Logs → Azure Log Analytics
- ✅ Metrics → Azure Monitor Metrics
- ✅ Traces → Application Insights (transaction search, application map)
View in Azure Portal:
- Logs:
Application Insights → Logs → traces table - Metrics:
Application Insights → Metrics - Dependencies:
Application Insights → Application map
Use Case: Running your app locally on Mac, sending logs to OTLP collector in sovdev-infrastructure (Rancher Desktop cluster).
Create .env file:
# Use IP address 127.0.0.1 (Node.js cannot resolve .localhost domains)
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://127.0.0.1/v1/logs
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://127.0.0.1/v1/metrics
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1/v1/traces
# Optional file logging
LOG_TO_FILE=true
LOG_FILE_PATH=./logs/app.log
NODE_ENV=developmentUpdate your run script to include the Host header:
#!/bin/bash
# REQUIRED: Host header for Traefik routing
OTEL_EXPORTER_OTLP_HEADERS='{"Host":"otel.localhost"}' npx tsx your-app.tsWhy These Values:
- ✅
127.0.0.1- Your Mac's localhost resolves to sovdev-infrastructure (Rancher Desktop) - ✅ Port 80 - Traefik ingress listens on port 80
- ✅
Host: otel.localhost- Traefik routes based on this header - ❌
otel.localhostURL - Node.js DNS can't resolve.localhostdomains
Open Grafana:
open http://grafana.localhostSee Advanced Configuration for:
- Kubernetes deployment (inside sovdev-infrastructure)
- Azure / Cloud deployment
- Troubleshooting OTLP connection
Traditional observability requires developers to implement three separate instrumentation strategies:
- Logs: Write logging code (e.g., console.log, winston)
- Metrics: Add counters, gauges, histograms (e.g., Prometheus client)
- Traces: Instrument distributed tracing (e.g., OpenTelemetry spans)
sovdev-logger eliminates this complexity. When you write a single log entry, the library automatically generates:
1. Structured Logs (for searching and debugging)
- Exported via OpenTelemetry protocol (OTLP)
- Searchable by service name, function, error level
- Contains full context: what happened, what went in, what came out
2. Prometheus Metrics (for dashboards and alerting)
- Automatic counters: How many operations? How many errors?
- Automatic histograms: How long did operations take?
- Enables sub-second dashboard queries
3. Distributed Traces (for understanding flow)
- Automatic span creation showing operation timeline
- Links related operations across services
- Generates service dependency graphs automatically
4. Session Grouping (for execution tracking)
- Unique
session.idgenerated when your application starts - Every log/metric/trace from that run gets the same session ID
- Find everything from "the 3 AM batch run" or "my test execution"
| Metric | Type | Description |
|---|---|---|
sovdev_operations_total |
Counter | Total operations count |
sovdev_errors_total |
Counter | Total errors (ERROR/FATAL) |
sovdev_operation_duration_milliseconds |
Histogram | Operation duration |
sovdev_operations_active |
Gauge | Currently active operations |
Example queries:
# Operations rate by service
rate(sovdev_operations_total[1m])
# Error rate
rate(sovdev_errors_total[1m]) / rate(sovdev_operations_total[1m])
# Average operation duration
rate(sovdev_operation_duration_milliseconds_sum[1m]) / rate(sovdev_operation_duration_milliseconds_count[1m])
See examples/basic/ for simple logging demonstration.
cd examples/basic
npm install
npm startSee examples/advanced/ for company lookup service with batch processing.
cd examples/advanced
npm install
npm startAlready covered in Configuration above.
Use Case: Your app is deployed as a pod in sovdev-infrastructure (Kubernetes cluster), sending logs to OTLP collector in the same cluster.
# Use Kubernetes internal service DNS (monitoring namespace)
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://otel-collector-opentelemetry-collector.monitoring.svc.cluster.local:4318/v1/logs
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://otel-collector-opentelemetry-collector.monitoring.svc.cluster.local:4318/v1/metrics
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel-collector-opentelemetry-collector.monitoring.svc.cluster.local:4318/v1/traces
# NO Host header needed - direct connection, no Traefik
# File logging (optional - logs go to pod filesystem)
LOG_TO_FILE=false
NODE_ENV=productionWhy These Values:
- ✅ Kubernetes DNS works inside sovdev-infrastructure
- ✅ Port 4318 - Direct OTLP HTTP port (bypasses Traefik)
- ✅ No Host header needed - Direct service-to-service communication
Use Case: App running in Azure, sending logs to Azure Application Insights or cloud-based OTLP collector.
Azure Application Insights:
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://your-app-insights.azure.com/v1/logs
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://your-app-insights.azure.com/v1/metrics
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://your-app-insights.azure.com/v1/traces
# Authentication (use Azure managed identity or connection string)
OTEL_EXPORTER_OTLP_HEADERS={"Authorization":"Bearer YOUR_TOKEN"}
LOG_TO_FILE=false
NODE_ENV=productionGrafana Cloud:
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://otlp-gateway-prod-eu-west-0.grafana.net/otlp/v1/logs
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://otlp-gateway-prod-eu-west-0.grafana.net/otlp/v1/metrics
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://otlp-gateway-prod-eu-west-0.grafana.net/otlp/v1/traces
OTEL_EXPORTER_OTLP_HEADERS={"Authorization":"Basic BASE64_ENCODED_CREDENTIALS"}
NODE_ENV=productionStep 1: Verify Configuration
# Check environment variables
echo $OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
echo $OTEL_EXPORTER_OTLP_HEADERSStep 2: Test OTLP Endpoint
# Scenario 1 (Mac + Traefik)
curl -v -X POST http://127.0.0.1/v1/logs \
-H "Host: otel.localhost" \
-H "Content-Type: application/json" \
-d '{}'
# Should return HTTP 200 or 400 (not 404)
# Scenario 2 (Kubernetes)
curl -v http://otel-collector-opentelemetry-collector.monitoring.svc.cluster.local:4318/v1/logsStep 3: Check Console Output
Look for OpenTelemetry initialization messages:
✅ Global LoggerProvider set
✅ OpenTelemetry SDK started successfully
📡 OTLP Log exporter configured for: http://...
If you see errors during init, the OTLP endpoint is likely unreachable.
This library implements "Loggeloven av 2025" requirements:
- ✅ Structured JSON format: All logs use structured JSON with consistent schema
- ✅ Required fields: Every log includes
service_name,function_name,timestamp,trace_id,event_id - ✅ snake_case field naming: All field names use underscores (
service_name,function_name,exception_type,span_id) - ✅ OpenTelemetry-compliant exception fields: Flat structure with
exception_type,exception_message,exception_stacktrace(not nested, not dot notation) - ✅ ERROR/FATAL levels trigger ServiceNow incidents: Automatic alerting on critical errors
- ✅ Security: Credentials automatically removed from logs (Authorization headers, auth objects)
- ✅ Distributed tracing: OpenTelemetry trace and span correlation for operation tracking
Field Naming Standard: All log fields use snake_case (lowercase with underscores) to ensure consistent storage and retrieval across OpenTelemetry backends (Loki, Tempo, Prometheus). This avoids transformation inconsistencies when OTLP automatically converts dot notation to underscores.
Note: This section is for library contributors who want to modify sovdev-logger itself. If you're a library user (using sovdev-logger in your app), you don't need this section.
# Clone the repository
git clone https://github.com/terchris/sovdev-logger.git
cd sovdev-logger/typescript
# Install dependencies
npm install
# Build the library
npm run build
# Watch mode (rebuilds on file changes)
npm run devThe library has three levels of automated tests:
# Run unit tests (fast, no dependencies)
npm run test:unit
# Run integration tests (tests console/file logging)
npm run test:integration
# Run E2E tests (verifies full OTLP pipeline with Loki/Prometheus/Tempo)
npm run test:e2e
# Run all tests (recommended before commits)
npm run test:allTest Coverage:
- Unit tests (18 tests): Log levels, peer services, trace ID generation
- Integration tests (19 tests): Console logging, file logging, flush behavior, initialization
- E2E tests: Full OTLP pipeline verification with Grafana stack
For detailed E2E testing and verification instructions, see test/e2e/README.md.
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-improvement - Make your changes
- Run tests:
npm run test:all - Build:
npm run build - Commit:
git commit -m "Add feature: ..." - Push and create a pull request
MIT