Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,460 changes: 1,460 additions & 0 deletions TRANSPORT_INJECTION_DESIGN.md

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,38 @@ The Datadog SDK supports many of the configurations supported by the OpenTelemet

For complete OTLP exporter configuration options, see the [OpenTelemetry OTLP Exporter documentation](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/).

<h3 id="log-capture">Log Capture</h3>

dd-trace-js includes experimental support for automatic log capture via native transport injection. When enabled, the tracer automatically attaches an HTTP transport to Winston, Bunyan, and Pino loggers at creation time, forwarding log records to a configurable HTTP intake endpoint without any changes to application code.

Enable by setting `DD_LOG_CAPTURE_ENABLED=true` and `DD_LOG_CAPTURE_METHOD=transport` along with a target host and port:

```javascript
const tracer = require('dd-trace').init({
logCaptureEnabled: true,
logCaptureMethod: 'transport',
logCaptureHost: 'localhost',
logCapturePort: 8080,
})

// Existing loggers are automatically instrumented — no changes needed
const winston = require('winston')
const logger = winston.createLogger({ level: 'debug' })
logger.info('This log is forwarded to the intake endpoint automatically')
```

#### Supported Configuration

- `DD_LOG_CAPTURE_ENABLED` - Enable log capture (default: `false`)
- `DD_LOG_CAPTURE_METHOD` - Log capture method. Set to `transport` to enable native transport injection (default: `wrapper`)
- `DD_LOG_CAPTURE_HOST` - Hostname of the log intake endpoint (required when transport injection is enabled)
- `DD_LOG_CAPTURE_PORT` - Port of the log intake endpoint (required when transport injection is enabled)
- `DD_LOG_CAPTURE_PROTOCOL` - Protocol for the log intake endpoint. Options: `http:`, `https:` (default: `http:`)
- `DD_LOG_CAPTURE_PATH` - HTTP path for the log intake endpoint (default: `/logs`)
- `DD_LOG_CAPTURE_FLUSH_INTERVAL_MS` - How often in milliseconds to flush buffered logs (default: `5000`)
- `DD_LOG_CAPTURE_MAX_BUFFER_SIZE` - Maximum number of log records to buffer before a forced flush (default: `1000`)
- `DD_LOG_CAPTURE_TIMEOUT_MS` - Timeout in milliseconds for each HTTP request to the intake endpoint (default: `5000`)

<h2 id="advanced-configuration">Advanced Configuration</h2>

<h3 id="tracer-settings">Tracer settings</h3>
Expand Down
63 changes: 63 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,69 @@ declare namespace tracer {
*/
logInjection?: boolean,

/**
* Whether to enable native transport injection for log capture.
* When enabled with `logCaptureMethod: 'transport'`, automatically injects
* HTTP transports into Winston, Bunyan, and Pino loggers.
* @default false
* @env DD_LOG_CAPTURE_ENABLED
*/
logCaptureEnabled?: boolean,

/**
* The hostname of the log intake endpoint.
* @env DD_LOG_CAPTURE_HOST
*/
logCaptureHost?: string,

/**
* The port of the log intake endpoint.
* @env DD_LOG_CAPTURE_PORT
*/
logCapturePort?: number,

/**
* The protocol for the log intake endpoint. Accepts `'http:'` or `'https:'`.
* @default 'http:'
* @env DD_LOG_CAPTURE_PROTOCOL
*/
logCaptureProtocol?: string,

/**
* The HTTP path for the log intake endpoint.
* @default '/logs'
* @env DD_LOG_CAPTURE_PATH
*/
logCapturePath?: string,

/**
* How often (in milliseconds) to flush buffered logs to the intake endpoint.
* @default 5000
* @env DD_LOG_CAPTURE_FLUSH_INTERVAL_MS
*/
logCaptureFlushIntervalMs?: number,

/**
* Maximum number of log records to buffer before a forced flush.
* @default 1000
* @env DD_LOG_CAPTURE_MAX_BUFFER_SIZE
*/
logCaptureMaxBufferSize?: number,

/**
* Timeout in milliseconds for each HTTP request to the log intake endpoint.
* @default 5000
* @env DD_LOG_CAPTURE_TIMEOUT_MS
*/
logCaptureTimeoutMs?: number,

/**
* The log capture method. Set to `'transport'` to enable native transport injection.
* @default 'wrapper'
* @env DD_LOG_CAPTURE_METHOD
*/
logCaptureMethod?: string,

/**
* Whether to enable startup logs.
* @default false
Expand Down
166 changes: 166 additions & 0 deletions integration-tests/network-transport-injection/benchmark-overhead.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env node

/**
* Benchmark to measure performance overhead of transport injection
* Compares logging performance with and without transport injection
*/

const { performance } = require('perf_hooks')

Check failure on line 8 in integration-tests/network-transport-injection/benchmark-overhead.js

View workflow job for this annotation

GitHub Actions / lint

Use the global form of 'use strict'

// Test configurations
const NUM_LOGS = 10000
const WARMUP_LOGS = 1000

console.log('\n=== Transport Injection Performance Benchmark ===\n')

Check failure on line 14 in integration-tests/network-transport-injection/benchmark-overhead.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.log(`Iterations: ${NUM_LOGS.toLocaleString()}`)

Check failure on line 15 in integration-tests/network-transport-injection/benchmark-overhead.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.log(`Warmup: ${WARMUP_LOGS.toLocaleString()}\n`)

Check failure on line 16 in integration-tests/network-transport-injection/benchmark-overhead.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

// Helper to measure performance
function benchmark(name, fn) {

Check failure on line 19 in integration-tests/network-transport-injection/benchmark-overhead.js

View workflow job for this annotation

GitHub Actions / lint

Missing space before function parentheses
// Warmup
for (let i = 0; i < WARMUP_LOGS; i++) {
fn()
}

// Force GC if available
if (global.gc) global.gc()

// Actual benchmark
const start = performance.now()
for (let i = 0; i < NUM_LOGS; i++) {
fn()
}
const end = performance.now()

const totalMs = end - start
const perLogUs = (totalMs * 1000) / NUM_LOGS

console.log(`${name}:`)

Check failure on line 38 in integration-tests/network-transport-injection/benchmark-overhead.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.log(` Total: ${totalMs.toFixed(2)}ms`)

Check failure on line 39 in integration-tests/network-transport-injection/benchmark-overhead.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.log(` Per-log: ${perLogUs.toFixed(3)}μs`)

Check failure on line 40 in integration-tests/network-transport-injection/benchmark-overhead.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.log(` Rate: ${(NUM_LOGS / (totalMs / 1000)).toFixed(0)} logs/sec`)

Check failure on line 41 in integration-tests/network-transport-injection/benchmark-overhead.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.log('')

Check failure on line 42 in integration-tests/network-transport-injection/benchmark-overhead.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

return { totalMs, perLogUs }
}

// ==================== WINSTON ====================
console.log('--- Winston ---\n')

// Without transport injection
const winston1 = require('winston')
const winstonLoggerBaseline = winston1.createLogger({
level: 'info',
format: winston1.format.json(),
transports: [new winston1.transports.Console({ silent: true })]
})

const winstonBaseline = benchmark('Winston (baseline)', () => {
winstonLoggerBaseline.info('test message', { userId: 12345, action: 'test' })
})

// With transport injection
process.env.DD_LOG_CAPTURE_ENABLED = 'true'
process.env.DD_LOG_CAPTURE_METHOD = 'transport'
process.env.DD_LOG_CAPTURE_HOST = 'localhost'
process.env.DD_LOG_CAPTURE_PORT = '9999' // Non-existent server (buffering only)
process.env.DD_LOG_CAPTURE_FLUSH_INTERVAL_MS = '999999' // Very long (no flushing during test)
process.env.DD_LOGS_INJECTION = 'true'

require('../../index').init({
service: 'benchmark-test',
env: 'test',
version: '1.0.0'
})

const winston2 = require('winston')
const winstonLoggerWithTransport = winston2.createLogger({
level: 'info',
format: winston2.format.json(),
transports: [new winston2.transports.Console({ silent: true })]
})

const winstonWithTransport = benchmark('Winston (with transport)', () => {
winstonLoggerWithTransport.info('test message', { userId: 12345, action: 'test' })
})

const winstonOverheadUs = winstonWithTransport.perLogUs - winstonBaseline.perLogUs
const winstonOverheadPct = ((winstonOverheadUs / winstonBaseline.perLogUs) * 100).toFixed(1)
console.log(`Winston Overhead: ${winstonOverheadUs.toFixed(3)}μs (${winstonOverheadPct}%)\n`)

// ==================== BUNYAN ====================
console.log('--- Bunyan ---\n')

// Without transport injection
delete require.cache[require.resolve('bunyan')]
const bunyan1 = require('bunyan')
const bunyanLoggerBaseline = bunyan1.createLogger({
name: 'benchmark-baseline',
level: 'info',
streams: [{ stream: { write: () => {} } }] // Null stream
})

const bunyanBaseline = benchmark('Bunyan (baseline)', () => {
bunyanLoggerBaseline.info({ userId: 12345, action: 'test' }, 'test message')
})

// With transport injection (already initialized tracer above)
delete require.cache[require.resolve('bunyan')]
const bunyan2 = require('bunyan')
const bunyanLoggerWithTransport = bunyan2.createLogger({
name: 'benchmark-with-transport',
level: 'info',
streams: [{ stream: { write: () => {} } }] // Null stream
})

const bunyanWithTransport = benchmark('Bunyan (with transport)', () => {
bunyanLoggerWithTransport.info({ userId: 12345, action: 'test' }, 'test message')
})

const bunyanOverheadUs = bunyanWithTransport.perLogUs - bunyanBaseline.perLogUs
const bunyanOverheadPct = ((bunyanOverheadUs / bunyanBaseline.perLogUs) * 100).toFixed(1)
console.log(`Bunyan Overhead: ${bunyanOverheadUs.toFixed(3)}μs (${bunyanOverheadPct}%)\n`)

// ==================== PINO ====================
console.log('--- Pino ---\n')

// Without transport injection
const pino1 = require('pino')
const pinoLoggerBaseline = pino1({ level: 'info' }, { write: () => {} }) // Null destination

const pinoBaseline = benchmark('Pino (baseline)', () => {
pinoLoggerBaseline.info({ userId: 12345, action: 'test' }, 'test message')
})

// With transport injection (already initialized tracer above)
delete require.cache[require.resolve('pino')]
const pino2 = require('pino')
const pinoLoggerWithTransport = pino2({ level: 'info' }, { write: () => {} })

const pinoWithTransport = benchmark('Pino (with transport)', () => {
pinoLoggerWithTransport.info({ userId: 12345, action: 'test' }, 'test message')
})

const pinoOverheadUs = pinoWithTransport.perLogUs - pinoBaseline.perLogUs
const pinoOverheadPct = ((pinoOverheadUs / pinoBaseline.perLogUs) * 100).toFixed(1)
console.log(`Pino Overhead: ${pinoOverheadUs.toFixed(3)}μs (${pinoOverheadPct}%)\n`)

// ==================== SUMMARY ====================
console.log('=== Summary ===\n')
console.log('Per-Log Overhead:')
console.log(` Winston: ${winstonOverheadUs.toFixed(3)}μs (${winstonOverheadPct}% increase)`)
console.log(` Bunyan: ${bunyanOverheadUs.toFixed(3)}μs (${bunyanOverheadPct}% increase)`)
console.log(` Pino: ${pinoOverheadUs.toFixed(3)}μs (${pinoOverheadPct}% increase)`)
console.log('')

// Memory info
const memUsage = process.memoryUsage()
console.log('Memory Usage:')
console.log(` Heap Used: ${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`)
console.log(` External: ${(memUsage.external / 1024 / 1024).toFixed(2)} MB`)
console.log('')

console.log('Note: Run with --expose-gc for accurate memory measurements')
console.log('Example: node --expose-gc benchmark-overhead.js\n')

process.exit(0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/bin/bash

# Test runner for all three logger transport injection implementations
#
# Usage:
# From this directory: ./run-transport-tests.sh
# From repo root: ./integration-tests/network-transport-injection/run-transport-tests.sh
#
# Prerequisites:
# 1. Start intake server in another terminal:
# node integration-tests/network-transport-injection/test-intake-server.js
# 2. Logger dependencies will be checked and installed if needed

set -e

# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"

# Change to repo root for npm commands
cd "$REPO_ROOT"

# Check and install logger dependencies if needed
echo "Checking logger dependencies..."
if ! npm list winston bunyan pino pino-pretty &>/dev/null; then
echo "Installing logger packages (winston, bunyan, pino, pino-pretty)..."
npm install --no-save winston bunyan pino pino-pretty
echo "✓ Dependencies installed"
else
echo "✓ All dependencies present"
fi
echo ""

# Change back to script directory to run tests
cd "$SCRIPT_DIR"

echo "=============================================="
echo " Logger Transport Injection Test Suite"
echo "=============================================="
echo ""

# Check if intake server is running
echo "Checking if intake server is running on port 8080..."
if ! nc -z localhost 8080 2>/dev/null; then
echo "⚠️ WARNING: Intake server not detected on port 8080"
echo " Start it with: node integration-tests/network-transport-injection/test-intake-server.js"
echo " Or from this directory: node test-intake-server.js"
echo ""
read -p "Continue anyway? (y/N) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
else
echo "✓ Intake server is running"
fi

echo ""
echo "=============================================="
echo " Test 1/3: Winston Transport Injection"
echo "=============================================="
node test-winston-transport.js

echo ""
echo "=============================================="
echo " Test 2/3: Bunyan Stream Injection"
echo "=============================================="
node test-bunyan-transport.js

echo ""
echo "=============================================="
echo " Test 3/4: Pino Simple (No User Transport)"
echo "=============================================="
node test-pino-simple.js

echo ""
echo "=============================================="
echo " Test 4/4: Pino with pino-pretty"
echo "=============================================="
node test-pino-transport.js

echo ""
echo "=============================================="
echo " All Tests Complete!"
echo "=============================================="
echo ""
echo "Check the intake server output to verify:"
echo " ✓ All 20 logs received (5 per test × 4 tests)"
echo " ✓ Trace correlation present (trace_id, span_id)"
echo " ✓ Service metadata included (service, env, version)"
echo ""
echo "Test coverage:"
echo " ✓ Winston: Native HTTP transport"
echo " ✓ Bunyan: Custom stream with timing fix"
echo " ✓ Pino Simple: Basic HTTP injection"
echo " ✓ Pino Pretty: Multistream auto-combination"
echo ""
Loading
Loading