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
copytruncatekombinasyonunu 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.
