New Relic Logs in Context設定ガイド - APMとログの統合監視

New RelicのLogs in Context機能は、アプリケーションパフォーマンス監視(APM)データとログデータを自動的に関連付け、問題の根本原因分析を大幅に向上させる強力な機能です。この統合により、エンジニアはパフォーマンス問題やエラーが発生した際に、関連するログエントリを同じ画面で確認でき、トラブルシューティングの効率を劇的に改善できます。

Logs in Contextとは

Logs in Contextは、APMエージェントとログ管理システムを連携させ、トレースIDやスパンIDなどの分散トレーシング情報をログエントリに自動的に付与する機能です。この関連付けにより、特定のリクエストやトランザクションに関連するすべてのログを素早く特定できます。

機能の核となる価値

Logs in Contextが提供する主要な価値は、以下の4つの要素から構成されています。

コンテキスト豊富なトラブルシューティングでは、パフォーマンス問題が発生した際に、関連するログエントリを自動的に表示します。エンジニアは複数のツール間を移動することなく、問題の全体像を把握できます。

分散システムでの可視性向上により、マイクロサービス環境において、リクエストがサービス間を移動する際のログを追跡できます。サービス間の依存関係やエラーの伝播を明確に可視化します。

検索時間の短縮により、特定のエラーやパフォーマンス問題に関連するログを手動で検索する時間を大幅に削減できます。関連ログへの直接アクセスが可能になります。

デバッグ効率の向上では、アプリケーションコードの問題箇所を特定する際に、実行時のログ情報を即座に確認できます。コード変更の影響や例外の詳細な分析が可能になります。

基本的な設定方法

Logs in Contextの設定は、使用するプログラミング言語とフレームワークによって異なります。以下に主要な言語での実装例を示します。

Java環境での設定

JavaアプリケーションでLogs in Contextを有効にするには、Logbackまたはlog4jの設定変更が必要です。

Logback設定

xml
<!-- logback-spring.xml -->
<configuration>
    <appender name="NEWRELIC" class="com.newrelic.logging.logback.NewRelicAppender">
        <layout class="ch.qos.logback.contrib.json.classic.JsonLayout">
            <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat>
            <timestampFormatTimezoneId>UTC</timestampFormatTimezoneId>
            <appendLineSeparator>true</appendLineSeparator>
            <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">
                <prettyPrint>false</prettyPrint>
            </jsonFormatter>
        </layout>
    </appender>
    
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="com.newrelic.logging.logback.NewRelicEncoder"/>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="NEWRELIC"/>
    </root>
</configuration>

依存関係の追加

xml
<!-- pom.xml -->
<dependency>
    <groupId>com.newrelic.logging</groupId>
    <artifactId>logback</artifactId>
    <version>2.3</version>
</dependency>

Node.js環境での設定

Node.jsアプリケーションでは、Winstonやpino などのログライブラリとの統合が可能です。

Winston設定

javascript
// logger.js
const winston = require('winston');
const newrelicFormatter = require('@newrelic/winston-enricher');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    newrelicFormatter(), // New Relicメタデータの追加
    winston.format.json()
  ),
  defaultMeta: { 
    service: 'user-service',
    environment: process.env.NODE_ENV 
  },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
    new winston.transports.Console({
      format: winston.format.simple()
    })
  ],
});

module.exports = logger;

使用例

javascript
// app.js
const logger = require('./logger');
const newrelic = require('newrelic');

app.get('/api/users/:id', async (req, res) => {
  const userId = req.params.id;
  
  logger.info('Fetching user data', {
    userId: userId,
    requestId: req.headers['x-request-id']
  });
  
  try {
    const user = await userService.getUser(userId);
    
    logger.info('User data retrieved successfully', {
      userId: userId,
      userFound: !!user
    });
    
    res.json(user);
  } catch (error) {
    logger.error('Failed to fetch user data', {
      userId: userId,
      error: error.message,
      stack: error.stack
    });
    
    res.status(500).json({ error: 'Internal server error' });
  }
});

Python環境での設定

Pythonアプリケーションでは、標準ライブラリのloggingモジュールとの統合が可能です。

Python Logging設定

python
# logger_config.py
import logging
import newrelic.agent
from newrelic.logging import NewRelicLogForwardingHandler

def setup_logger():
    # New Relic log forwarding handlerの設定
    handler = NewRelicLogForwardingHandler()
    handler.setLevel(logging.INFO)
    
    # フォーマッターの設定
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    handler.setFormatter(formatter)
    
    # ロガーの設定
    logger = logging.getLogger('myapp')
    logger.setLevel(logging.INFO)
    logger.addHandler(handler)
    
    # コンソールハンドラーの追加
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)
    
    return logger

# アプリケーションでの使用
logger = setup_logger()

@newrelic.agent.function_trace()
def process_order(order_id):
    logger.info(f"Processing order: {order_id}", extra={
        'order_id': order_id,
        'service': 'order-service'
    })
    
    try:
        # 注文処理ロジック
        result = perform_order_processing(order_id)
        
        logger.info(f"Order processed successfully: {order_id}", extra={
            'order_id': order_id,
            'result': result
        })
        
        return result
    except Exception as e:
        logger.error(f"Order processing failed: {order_id}", extra={
            'order_id': order_id,
            'error': str(e)
        }, exc_info=True)
        raise

手動ログ関連付け

APMエージェントが自動的に提供するメタデータに加えて、手動でトレーシング情報をログに追加することも可能です。

トレースIDとスパンIDの取得

java
// Java
import com.newrelic.api.agent.NewRelic;

public class OrderService {
    private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
    
    public void processOrder(String orderId) {
        // New Relicトレース情報の取得
        String traceId = NewRelic.getAgent().getTraceMetadata().getTraceId();
        String spanId = NewRelic.getAgent().getTraceMetadata().getSpanId();
        
        // ログにトレース情報を追加
        MDC.put("trace.id", traceId);
        MDC.put("span.id", spanId);
        MDC.put("order.id", orderId);
        
        logger.info("Starting order processing");
        
        try {
            // 処理ロジック
            performOrderProcessing(orderId);
            logger.info("Order processing completed");
        } catch (Exception e) {
            logger.error("Order processing failed", e);
            throw e;
        } finally {
            // MDCのクリア
            MDC.clear();
        }
    }
}
javascript
// Node.js
const newrelic = require('newrelic');

function processOrder(orderId) {
    const traceMetadata = newrelic.getTraceMetadata();
    
    logger.info('Starting order processing', {
        'trace.id': traceMetadata.traceId,
        'span.id': traceMetadata.spanId,
        'order.id': orderId
    });
    
    try {
        performOrderProcessing(orderId);
        
        logger.info('Order processing completed', {
            'trace.id': traceMetadata.traceId,
            'span.id': traceMetadata.spanId,
            'order.id': orderId
        });
    } catch (error) {
        logger.error('Order processing failed', {
            'trace.id': traceMetadata.traceId,
            'span.id': traceMetadata.spanId,
            'order.id': orderId,
            error: error.message
        });
        throw error;
    }
}

分散トレーシング環境での設定

マイクロサービス環境では、リクエストがサービス間を移動する際にトレーシング情報を適切に伝播させる必要があります。

HTTPヘッダーを使用したトレース伝播

java
// Java - HTTPクライアント側
@RestController
public class UserController {
    
    @Autowired
    private OrderServiceClient orderServiceClient;
    
    @GetMapping("/users/{userId}/orders")
    public ResponseEntity<List<Order>> getUserOrders(@PathVariable String userId) {
        // New Relicトレース情報の取得
        String traceId = NewRelic.getAgent().getTraceMetadata().getTraceId();
        String spanId = NewRelic.getAgent().getTraceMetadata().getSpanId();
        
        // HTTPヘッダーにトレース情報を追加
        HttpHeaders headers = new HttpHeaders();
        headers.set("X-Trace-Id", traceId);
        headers.set("X-Span-Id", spanId);
        headers.set("X-User-Id", userId);
        
        return orderServiceClient.getOrdersByUser(userId, headers);
    }
}

// Java - HTTPサーバー側
@RestController
public class OrderController {
    
    @GetMapping("/orders/user/{userId}")
    public ResponseEntity<List<Order>> getOrdersByUser(
            @PathVariable String userId,
            @RequestHeader(value = "X-Trace-Id", required = false) String traceId,
            @RequestHeader(value = "X-Span-Id", required = false) String spanId) {
        
        // 受信したトレース情報をログコンテキストに設定
        if (traceId != null) {
            MDC.put("trace.id", traceId);
        }
        if (spanId != null) {
            MDC.put("parent.span.id", spanId);
        }
        
        logger.info("Fetching orders for user: {}", userId);
        
        try {
            List<Order> orders = orderService.getOrdersByUser(userId);
            logger.info("Found {} orders for user: {}", orders.size(), userId);
            return ResponseEntity.ok(orders);
        } catch (Exception e) {
            logger.error("Failed to fetch orders for user: {}", userId, e);
            throw e;
        } finally {
            MDC.clear();
        }
    }
}

メッセージキューでのトレース伝播

java
// Java - メッセージ送信側
@Component
public class OrderEventPublisher {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void publishOrderEvent(OrderEvent event) {
        // トレース情報をメッセージヘッダーに追加
        String traceId = NewRelic.getAgent().getTraceMetadata().getTraceId();
        String spanId = NewRelic.getAgent().getTraceMetadata().getSpanId();
        
        MessageProperties properties = new MessageProperties();
        properties.getHeaders().put("X-Trace-Id", traceId);
        properties.getHeaders().put("X-Span-Id", spanId);
        properties.getHeaders().put("X-Service", "order-service");
        
        Message message = new Message(JSON.toJSONBytes(event), properties);
        
        rabbitTemplate.send("order.events", message);
        
        logger.info("Published order event: {}", event.getOrderId());
    }
}

// Java - メッセージ受信側
@RabbitListener(queues = "order.events")
public class OrderEventConsumer {
    
    public void handleOrderEvent(
            @Payload OrderEvent event,
            @Header Map<String, Object> headers) {
        
        // メッセージヘッダーからトレース情報を取得
        String traceId = (String) headers.get("X-Trace-Id");
        String spanId = (String) headers.get("X-Span-Id");
        
        if (traceId != null) {
            MDC.put("trace.id", traceId);
        }
        if (spanId != null) {
            MDC.put("parent.span.id", spanId);
        }
        
        logger.info("Processing order event: {}", event.getOrderId());
        
        try {
            processOrderEvent(event);
            logger.info("Order event processed successfully: {}", event.getOrderId());
        } catch (Exception e) {
            logger.error("Failed to process order event: {}", event.getOrderId(), e);
            throw e;
        } finally {
            MDC.clear();
        }
    }
}

ログの構造化とメタデータ最適化

Logs in Contextの効果を最大化するためには、適切なログ構造化とメタデータ設定が重要です。

推奨ログフィールド構造

json
{
  "timestamp": "2025-07-27T10:00:00.123Z",
  "level": "INFO",
  "message": "Order processing completed",
  "service": "order-service",
  "environment": "production",
  "version": "1.2.3",
  "trace.id": "abc123def456",
  "span.id": "789xyz012",
  "entity.name": "order-service-pod-123",
  "entity.type": "SERVICE",
  "order.id": "ORD-12345",
  "user.id": "user-67890",
  "duration_ms": 150,
  "status": "success",
  "thread": "http-nio-8080-exec-1",
  "class": "com.example.OrderService",
  "method": "processOrder"
}

カスタム属性の設定

java
// Java - カスタム属性の追加
public class OrderService {
    
    @NewRelic.Trace
    public void processOrder(Order order) {
        // New Relicカスタム属性の追加
        NewRelic.addCustomParameter("order.id", order.getId());
        NewRelic.addCustomParameter("order.amount", order.getAmount());
        NewRelic.addCustomParameter("customer.tier", order.getCustomer().getTier());
        
        // ログにも同様の情報を追加
        MDC.put("order.id", order.getId());
        MDC.put("order.amount", String.valueOf(order.getAmount()));
        MDC.put("customer.tier", order.getCustomer().getTier());
        
        logger.info("Processing order for customer tier: {}", 
                   order.getCustomer().getTier());
        
        // 処理ロジック
    }
}

可視化とダッシュボード活用

Logs in Contextで関連付けられたデータを効果的に活用するための可視化手法について説明します。

APM画面でのログ確認

New RelicのAPM画面では、以下の方法でログを確認できます:

  1. Transaction traces: 特定のトランザクションの詳細画面で、関連するログエントリが自動表示される
  2. Error analytics: エラー発生時に、関連するログとスタックトレースが統合表示される
  3. Service maps: サービス間の依存関係とともに、各サービスのログ情報を確認できる

カスタムダッシュボードの作成

sql
-- NRQL クエリ例:エラー率とログエラーの相関分析
SELECT count(*) as 'Total Requests',
       filter(count(*), WHERE error IS true) as 'Error Count',
       filter(count(*), WHERE error IS true) / count(*) * 100 as 'Error Rate'
FROM Transaction, Log 
WHERE appName = 'order-service' 
  AND timestamp >= 1 hour ago 
FACET entity.name 
TIMESERIES 5 minutes

-- パフォーマンス問題とログの関連分析
SELECT average(duration) as 'Avg Response Time',
       count(*) as 'Log Entries'
FROM Transaction, Log 
WHERE appName = 'order-service'
  AND trace.id IS NOT NULL
  AND timestamp >= 1 hour ago
FACET level

トラブルシューティングとベストプラクティス

Logs in Contextの実装と運用における一般的な課題と解決方法について説明します。

一般的な問題と解決方法

ログとAPMデータの関連付けができない場合は、以下を確認してください:

  1. APMエージェントのバージョンがLogs in Contextに対応しているか
  2. ログライブラリの設定が正しく行われているか
  3. トレースIDが適切にログに含まれているか

パフォーマンスへの影響を最小限にするため:

  1. ログレベルを適切に設定し、不要なログを削減
  2. 非同期ログ出力を使用してブロッキングを回避
  3. バッファリングを適切に設定してI/O効率を向上

大量ログによるコスト増加を防ぐため:

  1. サンプリングレートの調整
  2. 重要度に応じたログレベル設定
  3. 不要なデバッグログの本番環境での無効化

監視とアラート設定

sql
-- ログエラー率の監視
SELECT filter(count(*), WHERE level = 'ERROR') / count(*) * 100 as 'Error Rate'
FROM Log 
WHERE service = 'order-service'
  AND timestamp >= 5 minutes ago

-- トレース情報が欠落しているログの監視
SELECT count(*) as 'Logs without trace'
FROM Log 
WHERE service = 'order-service'
  AND trace.id IS NULL
  AND timestamp >= 10 minutes ago

セキュリティとコンプライアンス

Logs in Context機能を本番環境で運用する際のセキュリティ考慮事項について説明します。

機密情報の保護

java
// Java - 機密情報の自動マスキング設定
@Component
public class SecureLoggingFilter {
    private static final Logger logger = LoggerFactory.getLogger(SecureLoggingFilter.class);
    
    @Autowired
    private SensitiveDataMasker dataMasker;
    
    public void logSecurely(String message, Object... params) {
        // トレース情報の取得
        String traceId = NewRelic.getAgent().getTraceMetadata().getTraceId();
        String spanId = NewRelic.getAgent().getTraceMetadata().getSpanId();
        
        // 機密情報のマスキング
        String maskedMessage = dataMasker.maskSensitiveData(message);
        Object[] maskedParams = dataMasker.maskParameters(params);
        
        // セキュアなログ出力
        MDC.put("trace.id", traceId);
        MDC.put("span.id", spanId);
        MDC.put("security.masked", "true");
        
        logger.info(maskedMessage, maskedParams);
        
        MDC.clear();
    }
}

コンプライアンス対応

javascript
// Node.js - GDPR準拠のログ管理
const winston = require('winston');
const newrelicFormatter = require('@newrelic/winston-enricher');

const gdprLogger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    newrelicFormatter(),
    winston.format.printf(({ timestamp, level, message, ...meta }) => {
      // PII情報の自動検出と除外
      const sanitizedMessage = sanitizePII(message);
      const sanitizedMeta = sanitizeMetadata(meta);
      
      return JSON.stringify({
        timestamp,
        level,
        message: sanitizedMessage,
        ...sanitizedMeta,
        compliance: {
          gdpr_compliant: true,
          data_classification: classifyData(message),
          retention_policy: getRetentionPolicy(level)
        }
      });
    })
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'secure-app.log' })
  ]
});

function sanitizePII(message) {
  return message
    .replace(/\b[\w._%+-]+@[\w.-]+\.[A-Z]{2,}\b/gi, '[EMAIL]')
    .replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN]')
    .replace(/\b\d{4}-\d{4}-\d{4}-\d{4}\b/g, '[CREDIT_CARD]');
}

コスト最適化戦略

ログレベル別最適化

python
# Python - 環境別ログレベル最適化
import logging
import newrelic.agent
from newrelic.logging import NewRelicLogForwardingHandler

class CostOptimizedLogger:
    def __init__(self, environment='production'):
        self.environment = environment
        self.logger = self._setup_logger()
        
    def _setup_logger(self):
        logger = logging.getLogger(f'app-{self.environment}')
        
        # 環境別ログレベル設定
        if self.environment == 'production':
            logger.setLevel(logging.WARNING)  # 本番環境はWARNING以上のみ
        elif self.environment == 'staging':
            logger.setLevel(logging.INFO)     # ステージング環境はINFO以上
        else:
            logger.setLevel(logging.DEBUG)    # 開発環境は全てのログ
        
        # New Relic handler の設定
        nr_handler = NewRelicLogForwardingHandler()
        
        # コスト効率を考慮したサンプリング
        if self.environment == 'production':
            nr_handler.addFilter(self._production_filter)
        
        logger.addHandler(nr_handler)
        return logger
    
    def _production_filter(self, record):
        # 本番環境での高頻度ログのサンプリング
        if record.levelno == logging.INFO:
            return hash(record.getMessage()) % 10 == 0  # 10%のみ送信
        return True  # WARNING以上は全て送信
    
    @newrelic.agent.function_trace()
    def log_with_context(self, level, message, **kwargs):
        # コスト効率を考慮したログ出力
        extra = {
            'environment': self.environment,
            'cost_tier': self._get_cost_tier(level),
            'retention_days': self._get_retention_days(level),
            **kwargs
        }
        
        self.logger.log(level, message, extra=extra)
    
    def _get_cost_tier(self, level):
        if level >= logging.ERROR:
            return 'premium'    # 長期保存、高可用性
        elif level >= logging.WARNING:
            return 'standard'   # 標準保存
        else:
            return 'economy'    # 短期保存、低コスト
    
    def _get_retention_days(self, level):
        cost_tier = self._get_cost_tier(level)
        return {
            'premium': 90,
            'standard': 30,
            'economy': 7
        }.get(cost_tier, 7)

動的サンプリング

java
// Java - 動的サンプリング実装
@Component
public class DynamicSamplingLogger {
    private static final Logger logger = LoggerFactory.getLogger(DynamicSamplingLogger.class);
    private final AtomicLong errorCount = new AtomicLong(0);
    private final AtomicLong totalCount = new AtomicLong(0);
    
    @Value("${logging.sampling.error-threshold:0.1}")
    private double errorThreshold;
    
    public void logWithDynamicSampling(String level, String message, Object... params) {
        totalCount.incrementAndGet();
        
        boolean shouldLog = false;
        double currentErrorRate = getCurrentErrorRate();
        
        // エラー率に基づく動的サンプリング
        if ("ERROR".equals(level)) {
            errorCount.incrementAndGet();
            shouldLog = true;  // エラーは常に記録
        } else if ("WARN".equals(level)) {
            // エラー率が高い場合、警告ログの記録率を上げる
            shouldLog = currentErrorRate > errorThreshold ? 
                Math.random() < 0.5 : Math.random() < 0.1;
        } else {
            // 通常ログは低頻度でサンプリング
            shouldLog = Math.random() < 0.05;
        }
        
        if (shouldLog) {
            // New Relicトレース情報を追加
            String traceId = NewRelic.getAgent().getTraceMetadata().getTraceId();
            String spanId = NewRelic.getAgent().getTraceMetadata().getSpanId();
            
            MDC.put("trace.id", traceId);
            MDC.put("span.id", spanId);
            MDC.put("error_rate", String.valueOf(currentErrorRate));
            MDC.put("sampling_reason", getSamplingReason(level, currentErrorRate));
            
            logger.info(message, params);
            MDC.clear();
        }
    }
    
    private double getCurrentErrorRate() {
        long total = totalCount.get();
        return total > 0 ? (double) errorCount.get() / total : 0.0;
    }
    
    private String getSamplingReason(String level, double errorRate) {
        if ("ERROR".equals(level)) return "always_log_errors";
        if (errorRate > errorThreshold) return "high_error_rate";
        return "normal_sampling";
    }
}

運用監視とアラート

統合監視ダッシュボード

sql
-- NRQL: Logs in Context 効果測定
SELECT 
  count(*) as total_logs,
  filter(count(*), WHERE trace.id IS NOT NULL) as logs_with_trace,
  filter(count(*), WHERE trace.id IS NOT NULL) / count(*) * 100 as context_coverage_percent
FROM Log 
WHERE appName = 'production-app'
  AND timestamp >= 1 hour ago
FACET service
TIMESERIES 5 minutes

-- NRQL: トラブルシューティング効率測定
SELECT 
  average(time_to_resolution) as avg_resolution_time,
  percentile(time_to_resolution, 95) as p95_resolution_time
FROM (
  SELECT 
    earliest(timestamp) as incident_start,
    latest(timestamp) as incident_end,
    latest(timestamp) - earliest(timestamp) as time_to_resolution
  FROM Log, Transaction
  WHERE error IS true
    AND trace.id IS NOT NULL
    AND timestamp >= 24 hours ago
  FACET trace.id
)
FACET service

自動アラート設定

sql
-- アラート条件: Logs in Context カバレッジ低下
SELECT filter(count(*), WHERE trace.id IS NOT NULL) / count(*) * 100 as coverage_percent
FROM Log 
WHERE appName = 'production-app'
  AND timestamp >= 5 minutes ago
HAVING coverage_percent < 80  -- 80%未満でアラート

-- アラート条件: エラー相関分析の失敗
SELECT count(*) as uncorrelated_errors
FROM Log
WHERE level = 'ERROR'
  AND trace.id IS NULL
  AND service IN ('critical-service-1', 'critical-service-2')
  AND timestamp >= 10 minutes ago
HAVING uncorrelated_errors > 5

まとめ

New RelicのLogs in Context機能により、APMデータとログデータの統合的な監視と分析が可能になります。適切な設定により、トラブルシューティングの効率が大幅に向上し、問題の根本原因を迅速に特定できます。

セキュリティとコンプライアンス対応を適切に実装することで、機密情報を保護しながら包括的な可視性を維持できます。コスト最適化戦略として、動的サンプリングや環境別ログレベル設定により、大規模環境でも経済的な運用が可能です。

分散システム環境では、トレーシング情報の適切な伝播と構造化ログの活用により、サービス間の依存関係とエラーの伝播を明確に可視化できます。統合監視ダッシュボードと自動アラートにより、継続的な監視と最適化を実現し、高品質なアプリケーション運用を維持できます。

これで06-logs-monitoring セクションのすべての記事が完成しました。次は他のNew Relicコンテンツセクションの作成や、既存コンテンツの拡充を進めていきましょう。


関連記事: New Relicログ監視概要関連記事: New Relicログ転送設定の完全ガイド