Skip to content

OpenTelemetry Migration Guides

Doug Fennell edited this page Oct 1, 2025 · 1 revision

Migration Guides: From Current Logging to RDCP + OpenTelemetry

πŸš€ Enterprise-grade migration paths that maintain existing workflows while adding powerful capabilities

Migration Philosophy

βœ… Incremental Adoption - Start small, expand gradually
βœ… Backward Compatibility - Keep existing logging during transition
βœ… Zero Downtime - Deploy changes safely without service interruption
βœ… Team Onboarding - Clear patterns for team adoption


Scenario 1: "We have console.log everywhere"

Current State: Basic Console Logging

// Typical current code
app.get('/users/:id', async (req, res) => {
  console.log('Getting user', req.params.id)
  
  const user = await db.users.findById(req.params.id)
  console.log('User found:', user ? 'yes' : 'no')
  
  if (!user) {
    console.log('User not found, returning 404')
    return res.status(404).json({ error: 'Not found' })
  }
  
  console.log('Returning user data')
  res.json(user)
})

Phase 1: Add RDCP Alongside Console (5 minutes)

Goal: Introduce structured, categorized logging without removing existing logs

const { RDCPClient } = require('@rdcp/server')

const rdcp = new RDCPClient({
  apiKey: process.env.RDCP_API_KEY || 'dev-key-32-characters-minimum-length'
})

app.get('/users/:id', async (req, res) => {
  console.log('Getting user', req.params.id) // ← Keep existing
  rdcp.debug.api('User request started', { userId: req.params.id }) // ← Add structured
  
  const user = await db.users.findById(req.params.id)
  console.log('User found:', user ? 'yes' : 'no') // ← Keep existing
  rdcp.debug.database('User query completed', { // ← Add structured
    userId: req.params.id,
    found: !!user,
    table: 'users'
  })
  
  if (!user) {
    console.log('User not found, returning 404') // ← Keep existing
    rdcp.debug.api('User not found', { userId: req.params.id }) // ← Add structured
    return res.status(404).json({ error: 'Not found' })
  }
  
  console.log('Returning user data') // ← Keep existing
  rdcp.debug.api('User response sent', { userId: user.id }) // ← Add structured
  res.json(user)
})

Benefits Immediate:

  • βœ… Structured, searchable debug logs
  • βœ… Runtime enable/disable by category
  • βœ… Existing console.logs still work
  • βœ… Team can evaluate RDCP value

Phase 2: Add OpenTelemetry Correlation (3 minutes)

Goal: Connect debug logs to distributed traces

const { setupRDCPWithOpenTelemetry } = require('@rdcp.dev/otel-plugin')

// Add OpenTelemetry setup (see Framework Examples for complete setup)
setupRDCPWithOpenTelemetry(rdcp)

// Same code as Phase 1 - but now debug logs include trace correlation automatically!

Benefits Added:

  • βœ… Perfect trace β†’ debug log correlation
  • βœ… Click trace ID, find exact debug context
  • βœ… Distributed request tracking across services

Phase 3: Gradually Replace Console Logs (Ongoing)

Goal: Replace console.log with RDCP as team adopts

app.get('/users/:id', async (req, res) => {
  // console.log('Getting user', req.params.id) // ← Remove when comfortable
  rdcp.debug.api('User request started', { userId: req.params.id })
  
  const user = await db.users.findById(req.params.id)
  // console.log('User found:', user ? 'yes' : 'no') // ← Remove when comfortable
  rdcp.debug.database('User query completed', {
    userId: req.params.id,
    found: !!user,
    table: 'users'
  })
  
  if (!user) {
    // console.log('User not found, returning 404') // ← Remove when comfortable
    rdcp.debug.api('User not found', { userId: req.params.id })
    return res.status(404).json({ error: 'Not found' })
  }
  
  // console.log('Returning user data') // ← Remove when comfortable
  rdcp.debug.api('User response sent', { userId: user.id })
  res.json(user)
})

Migration Timeline:

  • Week 1-2: Phase 1 (Add RDCP alongside)
  • Week 3: Phase 2 (Add OpenTelemetry correlation)
  • Month 2-3: Phase 3 (Gradually replace console.log)

Scenario 2: "We use Winston/Pino structured logging"

Current State: Winston Logger

const winston = require('winston')

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'app.log' }),
    new winston.transports.Console()
  ]
})

app.get('/orders/:id', async (req, res) => {
  logger.info('Order request received', { orderId: req.params.id })
  
  const order = await db.orders.findById(req.params.id)
  logger.info('Database query completed', { 
    orderId: req.params.id, 
    found: !!order,
    duration: '45ms'
  })
  
  if (!order) {
    logger.warn('Order not found', { orderId: req.params.id })
    return res.status(404).json({ error: 'Order not found' })
  }
  
  logger.info('Order retrieved successfully', { orderId: order.id })
  res.json(order)
})

Phase 1: Add RDCP for Runtime Control (7 minutes)

Goal: Keep Winston for persistent logging, add RDCP for runtime debug control

const { RDCPClient } = require('@rdcp/server')

// Keep existing Winston logger
const logger = winston.createLogger({ /* existing config */ })

// Add RDCP for runtime-controllable debug logs
const rdcp = new RDCPClient({
  apiKey: process.env.RDCP_API_KEY || 'dev-key-32-characters-minimum-length'
})

app.get('/orders/:id', async (req, res) => {
  // Keep Winston for persistent logging
  logger.info('Order request received', { orderId: req.params.id })
  
  // Add RDCP for detailed debug info (can be enabled/disabled at runtime)
  rdcp.debug.api('Order request details', { 
    orderId: req.params.id,
    userAgent: req.headers['user-agent'],
    ip: req.ip,
    timestamp: new Date().toISOString()
  })
  
  const order = await db.orders.findById(req.params.id)
  
  // Winston for important events
  logger.info('Database query completed', { 
    orderId: req.params.id, 
    found: !!order,
    duration: '45ms'
  })
  
  // RDCP for detailed debug info
  rdcp.debug.database('Order query details', {
    orderId: req.params.id,
    table: 'orders',
    query: `SELECT * FROM orders WHERE id = ${req.params.id}`,
    duration: '45ms',
    indexUsed: 'idx_orders_id',
    found: !!order
  })
  
  if (!order) {
    logger.warn('Order not found', { orderId: req.params.id })
    rdcp.debug.api('Order not found details', { 
      orderId: req.params.id,
      searchedTables: ['orders', 'archived_orders'],
      suggestedAction: 'check_archive'
    })
    return res.status(404).json({ error: 'Order not found' })
  }
  
  logger.info('Order retrieved successfully', { orderId: order.id })
  rdcp.debug.api('Order response prepared', {
    orderId: order.id,
    responseSize: JSON.stringify(order).length,
    includesSensitiveData: false
  })
  
  res.json(order)
})

Benefits of Dual Approach:

  • βœ… Winston: Persistent logging, alerting, log aggregation
  • βœ… RDCP: Runtime-controllable detailed debugging
  • βœ… Best of Both: Production logs + on-demand debug detail

Phase 2: Add OpenTelemetry Correlation (3 minutes)

const { setupRDCPWithOpenTelemetry } = require('@rdcp.dev/otel-plugin')

setupRDCPWithOpenTelemetry(rdcp)

// Same code as Phase 1, but now RDCP debug logs include trace correlation!
// Winston logs remain unchanged

Phase 3: Enhanced Integration (Optional)

Goal: Bridge Winston and RDCP with trace correlation

// Custom Winston transport that includes trace context
const { RDCPWinstonTransport } = require('@rdcp/winston-transport') // Hypothetical

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'app.log' }),
    new winston.transports.Console(),
    new RDCPWinstonTransport({ rdcpClient: rdcp }) // Bridge to RDCP
  ]
})

// Now Winston logs also get trace correlation when available!

Scenario 3: "We already have OpenTelemetry traces"

Current State: Existing OpenTelemetry Setup

const { NodeSDK } = require('@opentelemetry/sdk-node')
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger')
const { trace } = require('@opentelemetry/api')

// Existing OpenTelemetry setup
const sdk = new NodeSDK({
  traceExporter: new JaegerExporter({
    endpoint: 'http://localhost:14268/api/traces'
  })
})
sdk.start()

app.get('/products/:id', async (req, res) => {
  const span = trace.getActiveSpan()
  
  span?.setAttributes({
    'product.id': req.params.id,
    'user.agent': req.headers['user-agent']
  })
  
  const product = await db.products.findById(req.params.id)
  
  span?.addEvent('Database query completed', {
    'product.found': !!product,
    'query.duration': '32ms'
  })
  
  if (!product) {
    span?.recordException(new Error('Product not found'))
    return res.status(404).json({ error: 'Product not found' })
  }
  
  span?.setAttributes({ 'product.name': product.name })
  res.json(product)
})

Migration: Add RDCP Debug Logs (2 minutes!)

Goal: Add structured debug logs that automatically correlate with existing traces

// Keep existing OpenTelemetry setup exactly as-is
const { RDCPClient } = require('@rdcp/server')
const { setupRDCPWithOpenTelemetry } = require('@rdcp.dev/otel-plugin')

const rdcp = new RDCPClient({
  apiKey: process.env.RDCP_API_KEY || 'dev-key-32-characters-minimum-length'
})

// ✨ Magic: This connects to your existing OpenTelemetry traces
setupRDCPWithOpenTelemetry(rdcp)

app.get('/products/:id', async (req, res) => {
  const span = trace.getActiveSpan()
  
  // Keep existing span attributes
  span?.setAttributes({
    'product.id': req.params.id,
    'user.agent': req.headers['user-agent']
  })
  
  // Add RDCP debug logs - automatically correlated with traces!
  rdcp.debug.api('Product request received', {
    productId: req.params.id,
    userAgent: req.headers['user-agent'],
    requestSource: 'web'
  })
  
  const product = await db.products.findById(req.params.id)
  
  // Keep existing span events
  span?.addEvent('Database query completed', {
    'product.found': !!product,
    'query.duration': '32ms'
  })
  
  // Add detailed RDCP debug info
  rdcp.debug.database('Product query executed', {
    productId: req.params.id,
    found: !!product,
    table: 'products',
    query: `SELECT * FROM products WHERE id = ${req.params.id}`,
    duration: '32ms',
    cacheChecked: true,
    cacheHit: false
  })
  
  if (!product) {
    span?.recordException(new Error('Product not found'))
    
    rdcp.debug.api('Product not found', {
      productId: req.params.id,
      searchAttempts: 1,
      suggestedActions: ['check_inventory', 'verify_product_id']
    })
    
    return res.status(404).json({ error: 'Product not found' })
  }
  
  span?.setAttributes({ 'product.name': product.name })
  
  rdcp.debug.cache('Product cache operations', {
    productId: product.id,
    cacheKey: `product:${product.id}`,
    cacheMiss: true,
    willCache: true,
    ttl: 3600
  })
  
  rdcp.debug.api('Product response prepared', {
    productId: product.id,
    productName: product.name,
    responseSize: JSON.stringify(product).length
  })
  
  res.json(product)
})

Immediate Benefits:

  • βœ… Perfect Correlation: Debug logs include trace IDs automatically
  • βœ… Enhanced Context: More detailed information than span events
  • βœ… Runtime Control: Enable/disable debug categories without touching traces
  • βœ… Zero Disruption: Existing OpenTelemetry setup unchanged

The Power: Click on any trace in Jaeger β†’ Search logs by trace ID β†’ See exactly what your application was doing!


Advanced Migration Patterns

Pattern 1: Gradual Team Adoption

// Feature flag approach for team adoption
const enableRDCP = process.env.ENABLE_RDCP === 'true' || process.env.NODE_ENV === 'development'

if (enableRDCP) {
  const { RDCPClient } = require('@rdcp/server')
const { setupRDCPWithOpenTelemetry } = require('@rdcp.dev/otel-plugin')
  
  const rdcp = new RDCPClient({
    apiKey: process.env.RDCP_API_KEY || 'dev-key-32-characters-minimum-length'
  })
  
  setupRDCPWithOpenTelemetry(rdcp)
}

// Usage with conditional debugging
app.get('/api/data', async (req, res) => {
  // Existing logging always works
  logger.info('Data request', { endpoint: '/api/data' })
  
  // RDCP logging only when enabled
  if (enableRDCP) {
    rdcp.debug.api('Detailed request info', {
      endpoint: '/api/data',
      headers: req.headers,
      query: req.query,
      timestamp: Date.now()
    })
  }
  
  const data = await fetchData()
  res.json(data)
})

Pattern 2: Service-by-Service Migration

// Microservice migration strategy
const serviceName = process.env.SERVICE_NAME || 'unknown'
const enableRDCPForService = ['user-service', 'order-service'].includes(serviceName)

if (enableRDCPForService) {
  // Enable RDCP + OpenTelemetry for specific services
  const rdcp = new RDCPClient({
    apiKey: process.env.RDCP_API_KEY,
    tags: { service: serviceName }
  })
  
  setupRDCPWithOpenTelemetry(rdcp)
}

Pattern 3: Environment-Based Migration

// Different logging strategies per environment
const config = {
  development: {
    useRDCP: true,
    useWinston: true,
    rdcpCategories: ['api', 'database', 'cache', 'validation']
  },
  staging: {
    useRDCP: true,
    useWinston: true,
    rdcpCategories: ['api', 'database'] // Reduced categories
  },
  production: {
    useRDCP: false, // Disable until confident
    useWinston: true,
    rdcpCategories: []
  }
}

const env = process.env.NODE_ENV || 'development'
const { useRDCP, useWinston, rdcpCategories } = config[env]

Migration Checklist

Phase 1: Planning (1 week)

  • Audit Current Logging - Document existing console.log/Winston/Pino usage
  • Identify Key Endpoints - Pick 2-3 critical endpoints for initial migration
  • Set Up Development Environment - Install RDCP SDK and test basic functionality
  • Team Alignment - Present migration plan to team, get buy-in

Phase 2: Pilot Implementation (1 week)

  • Add RDCP to Development - Implement alongside existing logging
  • Configure Categories - Set up debug categories matching your domain
  • Test Runtime Control - Verify enable/disable functionality works
  • Measure Overhead - Confirm performance impact is acceptable

Phase 3: OpenTelemetry Integration (3 days)

  • Set Up OpenTelemetry - Configure tracing for your stack
  • Enable RDCP Correlation - Add setupRDCPWithOpenTelemetry(rdcp)
  • Verify Correlation - Confirm trace IDs appear in debug logs
  • Test End-to-End - Verify correlation works across service boundaries

Phase 4: Staged Rollout (2-4 weeks)

  • Deploy to Staging - Test with realistic traffic patterns
  • Monitor Performance - Watch for any performance degradation
  • Train Team - Show developers how to use RDCP debugging
  • Graduate Endpoints - Migrate additional endpoints based on success

Phase 5: Production Deployment (2-4 weeks)

  • Feature Flag Deployment - Deploy with RDCP disabled by default
  • Gradual Enablement - Enable categories one at a time
  • Monitor and Tune - Adjust based on production behavior
  • Document Best Practices - Create team guidelines for RDCP usage

Success Metrics

Technical Metrics

  • Migration Progress: % of endpoints using RDCP
  • Performance Impact: Overhead measurement (target: <1% latency increase)
  • Trace Correlation: % of debug logs with trace IDs
  • Debug Efficiency: Time to diagnose issues (before/after comparison)

Team Adoption Metrics

  • Developer Usage: # of developers actively using RDCP debug controls
  • Debug Categories: # of active debug categories per service
  • Incident Resolution: Mean time to resolution for production issues

Business Impact

  • Production Incidents: Reduction in debugging time
  • Development Velocity: Faster development due to better debugging
  • Customer Impact: Reduced issue resolution time

Common Migration Challenges & Solutions

Challenge: "Too much debug output"

Solution: Start with minimal categories, expand gradually

// Start conservative
const rdcp = new RDCPClient({
  defaultCategories: ['api'], // Only API calls initially
  autoEnable: false // Require explicit enablement
})

// Expand as team gets comfortable
// rdcp.enable('database')
// rdcp.enable('cache')

Challenge: "Performance concerns"

Solution: Measure and tune, use conditional debugging

// Performance monitoring
const debugOverhead = process.env.MEASURE_DEBUG_OVERHEAD === 'true'

if (debugOverhead) {
  const start = process.hrtime.bigint()
  rdcp.debug.api('Request processed', data)
  const end = process.hrtime.bigint()
  console.log('Debug overhead:', Number(end - start) / 1_000_000, 'ms')
}

Challenge: "Team resistance to change"

Solution: Gradual introduction, clear value demonstration

// Optional usage pattern
const debugEnabled = process.env.DEVELOPER_DEBUG === 'true'

// Developers can opt-in individually
if (debugEnabled) {
  rdcp.debug.api('Enhanced debugging for developer', {
    developerId: process.env.USER,
    debugLevel: 'verbose'
  })
}

Next Steps

  1. Choose Your Migration Path - Pick the scenario that matches your current setup
  2. Start Small - Begin with 1-2 endpoints in development
  3. Measure Impact - Document performance and developer experience improvements
  4. Scale Gradually - Expand to more endpoints and services based on success
  5. Configure Backends - Set up your observability platform with Backend Configurations

🎯 Result: Enterprise-grade observability that enhances rather than replaces your existing logging infrastructure.

Ready to start? Pick your scenario above and follow the phase-by-phase guide!

Clone this wiki locally