OpenTelemetry ile Uygulama Loglarını Merkezi Sisteme Aktarma

Mikroservis mimarisine geçtiğinizde ya da birden fazla sunucuda uygulama koşturmaya başladığınızda, en çok baş ağrısı yaratan konulardan biri log yönetimidir. Hangi serviste hata oluştu? Bu hata hangi request’ten kaynaklandı? 50 container’ın log’una tek tek bakmak mı gerekiyor? İşte tam bu noktada OpenTelemetry devreye giriyor ve hayatı ciddi ölçüde kolaylaştırıyor.

OpenTelemetry Nedir ve Neden Önemli?

OpenTelemetry (kısaca OTel), CNCF bünyesinde geliştirilen açık kaynaklı bir gözlemlenebilirlik (observability) framework’üdür. Traces, metrics ve logs olmak üç temel sinyal tipini standart bir şekilde toplamak, işlemek ve iletmek için tasarlanmıştır.

Eski dünyada her vendor kendi agent’ını, kendi SDK’sını dayatırdı. Datadog kullanıyorsanız Datadog agent, New Relic kullanıyorsanız New Relic agent kurardınız. Vendor değiştirmek istediğinizde tüm enstrümantasyon kodunu baştan yazmanız gerekirdi. OpenTelemetry bu sorunu ortadan kaldırıyor: bir kez enstrüman et, istediğin yere gönder.

Günümüzde OpenTelemetry’nin log sinyali de stabil hale geldi ve artık production ortamlarında güvenle kullanılabiliyor. Bu yazıda, uygulama loglarını OpenTelemetry Collector aracılığıyla merkezi bir sisteme (Loki + Grafana stack) nasıl aktaracağınızı adım adım göstereceğim.

Genel Mimari

Senaryo şu: Birden fazla uygulama sunucusunuz var, her birinde farklı servisler koşuyor. Tüm bu servislerden gelen logları tek bir noktada toplamak, sorgulamak ve alarm üretmek istiyorsunuz.

Kullanacağımız stack:

  • OpenTelemetry Collector: Log toplama ve yönlendirme katmanı
  • Grafana Loki: Log depolama backend’i
  • Grafana: Görselleştirme ve sorgulama
  • Promtail veya OTLP: Collector’a log gönderme yöntemi

Akış şu şekilde işliyor: Uygulama -> OTLP/stdout -> OTel Collector -> Loki -> Grafana

Ortamın Kurulması

Önce Docker Compose ile temel altyapıyı ayağa kaldıralım. Bu yapıyı merkezi log sunucunuzda çalıştıracaksınız.

mkdir -p /opt/logging-stack/{otelcol,loki,grafana}
cd /opt/logging-stack

Loki konfigürasyonu:

cat > /opt/logging-stack/loki/loki-config.yaml << 'EOF'
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

common:
  instance_addr: 127.0.0.1
  path_prefix: /tmp/loki
  storage:
    filesystem:
      chunks_directory: /tmp/loki/chunks
      rules_directory: /tmp/loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true
        max_size_mb: 100

schema_config:
  configs:
    - from: 2020-10-24
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

ruler:
  alertmanager_url: http://localhost:9093
EOF

OpenTelemetry Collector konfigürasyonu:

cat > /opt/logging-stack/otelcol/otel-collector-config.yaml << 'EOF'
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

  filelog:
    include:
      - /var/log/apps/*.log
    start_at: beginning
    include_file_name: true
    include_file_path: true
    operators:
      - type: json_parser
        timestamp:
          parse_from: attributes.timestamp
          layout: '%Y-%m-%dT%H:%M:%S.%LZ'
      - type: move
        from: attributes.msg
        to: body
      - type: add
        field: resource["environment"]
        value: production

processors:
  batch:
    send_batch_size: 10000
    timeout: 5s

  resource:
    attributes:
      - key: deployment.environment
        value: production
        action: upsert

  resourcedetection:
    detectors: [env, system]
    timeout: 2s

exporters:
  loki:
    endpoint: http://loki:3100/loki/api/v1/push
    default_labels_enabled:
      exporter: false
      job: true
    headers:
      "X-Custom-Header": "otel-logs"

  debug:
    verbosity: detailed

service:
  pipelines:
    logs:
      receivers: [otlp, filelog]
      processors: [batch, resource, resourcedetection]
      exporters: [loki, debug]
EOF

Docker Compose dosyası:

cat > /opt/logging-stack/docker-compose.yaml << 'EOF'
version: '3.8'

services:
  loki:
    image: grafana/loki:2.9.0
    ports:
      - "3100:3100"
    volumes:
      - ./loki/loki-config.yaml:/etc/loki/local-config.yaml
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - logging

  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.91.0
    ports:
      - "4317:4317"
      - "4318:4318"
      - "8888:8888"
    volumes:
      - ./otelcol/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml
      - /var/log/apps:/var/log/apps:ro
    command: ["--config=/etc/otelcol-contrib/config.yaml"]
    depends_on:
      - loki
    networks:
      - logging

  grafana:
    image: grafana/grafana:10.2.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=sysadmin123
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-storage:/var/lib/grafana
    depends_on:
      - loki
    networks:
      - logging

volumes:
  grafana-storage:

networks:
  logging:
    driver: bridge
EOF

docker compose up -d

Uygulama Tarafında OTLP ile Log Gönderme

Python Uygulaması

Bir Python Flask uygulamasını OpenTelemetry ile nasıl enstrümaniye edeceğimize bakalım. Bu senaryo, bir e-ticaret sitesinin order servisini simüle ediyor.

pip install opentelemetry-sdk 
    opentelemetry-exporter-otlp-proto-grpc 
    opentelemetry-instrumentation-logging 
    flask
cat > order_service.py << 'EOF'
import logging
import os
from flask import Flask, request, jsonify

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.sdk.resources import Resource

# Resource tanımla - bu bilgiler Loki'de label olarak görünecek
resource = Resource.create({
    "service.name": "order-service",
    "service.version": "1.2.0",
    "deployment.environment": "production",
    "host.name": os.uname().nodename
})

# Log provider kur
logger_provider = LoggerProvider(resource=resource)
set_logger_provider(logger_provider)

otlp_log_exporter = OTLPLogExporter(
    endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"),
    insecure=True
)
logger_provider.add_log_record_processor(
    BatchLogRecordProcessor(otlp_log_exporter)
)

# Python logging ile entegre et
handler = LoggingHandler(level=logging.DEBUG, logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)

logger = logging.getLogger(__name__)
app = Flask(__name__)

@app.route('/orders', methods=['POST'])
def create_order():
    data = request.get_json()
    
    logger.info(
        "Yeni siparis alindi",
        extra={
            "order_id": data.get("order_id"),
            "customer_id": data.get("customer_id"),
            "amount": data.get("amount"),
            "items_count": len(data.get("items", []))
        }
    )
    
    # İş mantığı simülasyonu
    if not data.get("customer_id"):
        logger.error(
            "Gecersiz siparis: musteri ID eksik",
            extra={"order_data": str(data)}
        )
        return jsonify({"error": "customer_id required"}), 400
    
    logger.info(
        "Siparis basariyla olusturuldu",
        extra={"order_id": data.get("order_id"), "status": "created"}
    )
    
    return jsonify({"status": "created", "order_id": data.get("order_id")}), 201

if __name__ == '__main__':
    logger.info("Order service baslatiliyor", extra={"port": 5000})
    app.run(host='0.0.0.0', port=5000, debug=False)
EOF

Node.js Uygulaması

Aynı senaryoyu Node.js tarafında da görelim. Bu sefer bir authentication servisi:

npm install @opentelemetry/sdk-node 
    @opentelemetry/auto-instrumentations-node 
    @opentelemetry/exporter-logs-otlp-grpc 
    @opentelemetry/sdk-logs 
    winston 
    @opentelemetry/winston-transport
cat > auth-service/tracing.js << 'EOF'
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-grpc');
const { SimpleLogRecordProcessor } = require('@opentelemetry/sdk-logs');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');

const logExporter = new OTLPLogExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
});

const sdk = new NodeSDK({
    resource: new Resource({
        [SemanticResourceAttributes.SERVICE_NAME]: 'auth-service',
        [SemanticResourceAttributes.SERVICE_VERSION]: '2.1.0',
        'deployment.environment': process.env.NODE_ENV || 'production',
    }),
    logRecordProcessor: new SimpleLogRecordProcessor(logExporter),
    instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

process.on('SIGTERM', () => {
    sdk.shutdown()
        .then(() => console.log('Telemetry kapatildi'))
        .catch(console.error);
});

module.exports = sdk;
EOF

Dosya Tabanlı Log Toplama

Bazı durumlarda uygulamanıza müdahale edemeyebilirsiniz. Legacy bir Java uygulaması ya da üçüncü parti bir yazılım olabilir. Bu durumda filelog receiver devreye giriyor.

# Uygulama log dizinini oluştur
mkdir -p /var/log/apps

# Test için örnek log üretelim
cat > /usr/local/bin/generate-test-logs.sh << 'EOF'
#!/bin/bash

LOG_FILE="/var/log/apps/legacy-app.log"
LEVELS=("INFO" "WARN" "ERROR" "DEBUG")
SERVICES=("payment" "inventory" "notification" "user")

while true; do
    LEVEL=${LEVELS[$RANDOM % ${#LEVELS[@]}]}
    SERVICE=${SERVICES[$RANDOM % ${#SERVICES[@]}]}
    TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")
    REQUEST_ID=$(cat /proc/sys/kernel/random/uuid)
    
    echo "{"timestamp":"$TIMESTAMP","level":"$LEVEL","service":"$SERVICE","request_id":"$REQUEST_ID","msg":"Operation completed","duration_ms":$((RANDOM % 500))}" >> $LOG_FILE
    
    sleep 0.5
done
EOF

chmod +x /usr/local/bin/generate-test-logs.sh

Filelog receiver için daha gelişmiş bir konfigürasyon, çok satırlı Java stack trace’lerini handle etmek için:

cat >> /opt/logging-stack/otelcol/otel-collector-config.yaml << 'EOF'

  filelog/java:
    include:
      - /var/log/apps/java-app.log
    multiline:
      line_start_pattern: '^d{4}-d{2}-d{2}'
    operators:
      - type: regex_parser
        regex: '^(?P<timestamp>d{4}-d{2}-d{2} d{2}:d{2}:d{2},d{3})s+(?P<level>w+)s+(?P<logger>[w.]+)s+-s+(?P<message>.*)'
        timestamp:
          parse_from: attributes.timestamp
          layout: '%Y-%m-%d %H:%M:%S,%L'
        severity:
          parse_from: attributes.level
      - type: add
        field: resource["service.name"]
        value: java-legacy-app
EOF

Collector’ın İzlenmesi

OTel Collector kendi metriklerini de expose ediyor. Collector’ın sağlıklı çalışıp çalışmadığını kontrol etmek için:

# Collector health check
curl -s http://localhost:13133/

# Collector metrikleri (Prometheus formatında)
curl -s http://localhost:8888/metrics | grep otelcol_exporter

# Kaç log record işlendiğini gör
curl -s http://localhost:8888/metrics | grep otelcol_processor_batch_batch_send_size

# Loki'ye başarıyla gönderilen kayıt sayısı
curl -s http://localhost:8888/metrics | grep otelcol_exporter_sent_log_records

Collector log seviyesini geçici olarak artırmak için:

# Collector container'ına bağlan ve log seviyesini değiştir
curl -X POST http://localhost:55679/debug/loglevel 
    -H 'Content-Type: application/json' 
    -d '{"level": "debug"}'

Loki’de Sorgulama

Loglar Loki’ye ulaştıktan sonra Grafana’dan LogQL ile sorgulayabilirsiniz. Birkaç pratik sorgu örneği:

# Grafana'yı açmadan CLI üzerinden Loki'yi sorgula
# Son 1 saatteki ERROR logları
curl -G -s "http://localhost:3100/loki/api/v1/query_range" 
    --data-urlencode 'query={service_name="order-service"} |= "ERROR"' 
    --data-urlencode 'start=1h' 
    --data-urlencode 'limit=50' | jq '.data.result[].values[][1]'

# Belirli bir request_id'yi tüm servisler üzerinde ara
curl -G -s "http://localhost:3100/loki/api/v1/query_range" 
    --data-urlencode 'query={job=~".+"} |= "req-abc-123"' 
    --data-urlencode 'start=6h' | jq '.data.result[] | {stream: .stream, logs: .values[][1]}'

# Son 5 dakikadaki log hacmini servis bazında özetle
curl -G -s "http://localhost:3100/loki/api/v1/query" 
    --data-urlencode 'query=sum by (service_name) (count_over_time({job=~".+"}[5m]))' 
    | jq '.data.result[] | {service: .metric.service_name, count: .value[1]}'

Production’da Dikkat Edilmesi Gerekenler

Güvenlik

Collector’ı production’a almadan önce TLS konfigürasyonu şart:

# Self-signed sertifika üret (production için Let's Encrypt kullanın)
openssl req -x509 -newkey rsa:4096 -keyout /opt/logging-stack/otelcol/key.pem 
    -out /opt/logging-stack/otelcol/cert.pem -days 365 -nodes 
    -subj "/CN=otel-collector.internal"

# Collector konfigürasyonuna TLS ekle
cat >> /opt/logging-stack/otelcol/otel-collector-config.yaml << 'TLSEOF'
# receivers altindaki otlp'yi su sekilde guncelle:
# receivers:
#   otlp:
#     protocols:
#       grpc:
#         endpoint: 0.0.0.0:4317
#         tls:
#           cert_file: /etc/otelcol/cert.pem
#           key_file: /etc/otelcol/key.pem
TLSEOF

Batch ve Retry Ayarları

Yüksek yük altında log kaybını önlemek için:

# Collector konfigürasyonuna eklenecek processor
# processors:
#   batch:
#     send_batch_size: 10000
#     send_batch_max_size: 11000
#     timeout: 10s
#
#   memory_limiter:
#     check_interval: 1s
#     limit_mib: 512
#     spike_limit_mib: 128
#
# exporters:
#   loki:
#     endpoint: http://loki:3100/loki/api/v1/push
#     retry_on_failure:
#       enabled: true
#       initial_interval: 5s
#       max_interval: 30s
#       max_elapsed_time: 300s
#     sending_queue:
#       enabled: true
#       num_consumers: 10
#       queue_size: 5000

echo "Yukaridaki ayarlari otel-collector-config.yaml dosyasina ekleyin"

Log Rotasyonu

Collector log dosyalarını okurken rotasyon sorunları yaşamamak için:

cat > /etc/logrotate.d/app-logs << 'EOF'
/var/log/apps/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
    postrotate
        # OTel Collector checkpoint dosyalarini temizle
        find /var/lib/otelcol/file_storage -name "*.log*" -mtime +1 -delete 2>/dev/null
    endscript
}
EOF

Systemd ile Collector Servis Olarak Çalıştırma

Container kullanmak istemediğiniz durumlarda binary olarak da kurabilirsiniz:

# Binary indir
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.91.0/otelcol-contrib_0.91.0_linux_amd64.tar.gz
tar -xzf otelcol-contrib_0.91.0_linux_amd64.tar.gz -C /usr/local/bin/

# Systemd servis dosyası
cat > /etc/systemd/system/otelcol.service << 'EOF'
[Unit]
Description=OpenTelemetry Collector
After=network.target

[Service]
Type=simple
User=otelcol
Group=otelcol
ExecStart=/usr/local/bin/otelcol-contrib --config=/etc/otelcol/config.yaml
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
Environment=GOMAXPROCS=2

[Install]
WantedBy=multi-user.target
EOF

useradd -r -s /bin/false otelcol
mkdir -p /etc/otelcol
systemctl daemon-reload
systemctl enable --now otelcol
systemctl status otelcol

Sonuç

OpenTelemetry ile log yönetimi, başlangıçta karmaşık görünse de bir kez kurulduğunda inanılmaz bir rahatlık sağlıyor. Vendor lock-in olmadan, standart bir format üzerinden tüm uygulama loglarınızı merkezi bir noktada toplayabiliyorsunuz.

Bu yazıda anlattığımız yaklaşımın en büyük avantajı tek bir collector üzerinden birden fazla hedefe log gönderebilmeniz. Bugün Loki kullandınız, yarın Elasticsearch’e geçmek istediniz; sadece exporter konfigürasyonunu değiştirmeniz yeterli, uygulama koduna dokunmanıza gerek yok.

Production’a almadan önce mutlaka şunlara dikkat edin:

  • Memory limiter processor ekleyin, aksi halde yüksek yük altında collector OOM’dan ölür
  • Retry ve queue ayarlarını yapılandırın, ağ kesintilerinde log kaybetmeyin
  • Collector metriklerini Prometheus ile izleyin, kör gitmeyin
  • TLS olmadan collector’ı internete açmayın
  • Log rotasyonu ve copytruncate kombinasyonunu doğru ayarlayın

Bir sonraki adım olarak bu log’lara Grafana’da alert kuralları eklemek ve anormal kalıpları otomatik olarak tespit etmek için LogQL ile çalışmayı derinleştirmenizi öneririm. Distributed tracing ile logları ilişkilendirdiğinizde ise sistem davranışı konusunda gerçekten tam bir görünürlük elde etmiş olacaksınız.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir