Метрики, распределённый трейсинг и observability в 12-Factor приложениях
Метрики, распределённый трейсинг и observability в 12-Factor приложениях
Telemetry — современное расширение 12-Factor App (13-й фактор). Принцип гласит:
Приложение должно предоставлять телеметрию: метрики производительности, распределённый трейсинг и health checks для полной наблюдаемости (observability) в облачных средах.**
┌─────────────────────────────────────────────────────────────────┐
│ Три столпа Observability │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Метрики │ │ Логи │ │ Трейсы │ │
│ │ (Metrics) │ │ (Logs) │ │ (Traces) │ │
│ │ │ │ │ │ │ │
│ │ - Задержка │ │ - События │ │ - Путь │ │
│ │ - Ошибки │ │ - Контекст │ │ запроса │ │
│ │ - Трафик │ │ - Детали │ │ - Спаны │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Health Checks: readiness, liveness, startup │
└─────────────────────────────────────────────────────────────────┘
from prometheus_client import Counter, Histogram, Gauge, Summary
# Counter — счётчик (только увеличивается)
# Пример: количество запросов, ошибок
http_requests_total = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
# Histogram — гистограмма (распределение значений)
# Пример: время ответа, размер ответа
http_request_duration_seconds = Histogram(
'http_request_duration_seconds',
'HTTP request duration in seconds',
['method', 'endpoint'],
buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
)
# Gauge — измеритель (может увеличиваться и уменьшаться)
# Пример: количество активных соединений, использование памяти
active_connections = Gauge(
'active_connections',
'Number of active connections'
)
# Summary — сводка (квантили)
# Пример: p50, p95, p99 времени ответа
http_request_duration_summary = Summary(
'http_request_duration_summary_seconds',
'HTTP request duration summary',
['method', 'endpoint']
)from flask import Flask, request, g
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
import time
import functools
app = Flask(__name__)
# Middleware для сбора метрик
@app.before_request
def before_request():
g.start_time = time.time()
g.request_id = request.headers.get('X-Request-ID', str(uuid.uuid4()))
@app.after_request
def after_request(response):
if hasattr(g, 'start_time'):
duration = time.time() - g.start_time
http_request_duration_seconds.labels(
method=request.method,
endpoint=request.endpoint or 'unknown'
).observe(duration)
http_requests_total.labels(
method=request.method,
endpoint=request.endpoint or 'unknown',
status=response.status_code
).inc()
response.headers['X-Request-ID'] = getattr(g, 'request_id', '')
return response
# Endpoint для Prometheus
@app.route('/metrics')
def metrics():
return generate_latest(), 200, {'Content-Type': CONTENT_TYPE_LATEST}
# Health check endpoint
@app.route('/health')
def health():
return {'status': 'healthy'}, 200# RED: Rate, Errors, Duration
from prometheus_client import Counter, Histogram
# Rate — количество запросов в секунду
requests_per_second = Counter(
'service_requests_total',
'Total requests',
['service', 'method', 'status']
)
# Errors — количество ошибок
errors_per_second = Counter(
'service_errors_total',
'Total errors',
['service', 'error_type']
)
# Duration — время ответа
duration = Histogram(
'service_request_duration_seconds',
'Request duration',
['service', 'method'],
buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
)
# Использование
def track_request(service, method):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
try:
result = func(*args, **kwargs)
requests_per_second.labels(
service=service,
method=method,
status='success'
).inc()
return result
except Exception as e:
requests_per_second.labels(
service=service,
method=method,
status='error'
).inc()
errors_per_second.labels(
service=service,
error_type=type(e).__name__
).inc()
raise
finally:
duration.labels(
service=service,
method=method
).observe(time.time() - start)
return wrapper
return decorator# USE: Utilization, Saturation, Errors
# Utilization — процент использования ресурса
cpu_utilization = Gauge(
'node_cpu_utilization',
'CPU utilization percentage',
['node']
)
memory_utilization = Gauge(
'node_memory_utilization',
'Memory utilization percentage',
['node']
)
# Saturation — степень заполненности очереди
disk_saturation = Gauge(
'node_disk_saturation',
'Disk saturation percentage',
['node', 'device']
)
# Errors — ошибки оборудования
disk_errors = Counter(
'node_disk_errors_total',
'Total disk errors',
['node', 'device']
)from opentelemetry import trace
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
# Настройка трейсера
trace.set_tracer_provider(TracerProvider())
# Экспорт в Jaeger
jaeger_exporter = JaegerExporter(
agent_host_name='jaeger',
agent_port=6831,
)
span_processor = BatchSpanProcessor(jaeger_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)
# Автоматическое инструментирование Flask
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
# Автоматическое инструментирование HTTP-запросов
RequestsInstrumentor().instrument()
# Ручное создание спанов
tracer = trace.get_tracer(__name__)
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
with tracer.start_as_current_span('get_user') as span:
span.set_attribute('user.id', user_id)
# Вложенный спан для запроса к БД
with tracer.start_as_current_span('database_query') as db_span:
db_span.set_attribute('db.table', 'users')
user = db.query('SELECT * FROM users WHERE id = ?', user_id)
if not user:
span.set_attribute('user.found', False)
return {'error': 'User not found'}, 404
span.set_attribute('user.found', True)
return userfrom opentelemetry import context, baggage
#Propagation контекста между сервисами
from opentelemetry.propagate import inject, extract
# Извлечение контекста из входящих заголовков
@app.before_request
def extract_context():
ctx = extract(request.headers)
context.attach(ctx)
# Инъекция контекста в исходящие запросы
import requests
def make_request(url):
headers = {}
inject(headers) # Добавляет traceparent, tracestate
return requests.get(url, headers=headers)
# Работа с baggage (дополнительные данные)
@app.route('/api/process')
def process():
# Добавление данных в baggage
ctx = baggage.set_baggage('user.id', '123')
ctx = baggage.set_baggage('tenant.id', 'acme')
token = context.attach(ctx)
try:
# Все спаны в этом контексте будут иметь baggage
with tracer.start_as_current_span('process'):
# ...
pass
finally:
context.detach(token)# docker-compose.yml с Jaeger
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:1.52
ports:
- "5775:5775/udp" # Thrift compact
- "6831:6831/udp" # Thrift binary
- "6832:6832/udp" # Thrift binary
- "5778:5778" # Config
- "16686:16686" # UI
- "14268:14268" # HTTP
- "14250:14250" # gRPC
environment:
- COLLECTOR_ZIPKIN_HOST_PORT=:9411┌─────────────────────────────────────────────────────────────────┐
│ Jaeger UI Trace │
│ │
│ Trace ID: abc123def456 │
│ Service: myapp │
│ Operation: GET /api/users │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ myapp: GET /api/users [████████] │ │
│ │ ├─ database: SELECT users [████] │ │
│ │ ├─ redis: GET session [██] │ │
│ │ └─ external-api: GET /data [██████] │ │
│ │ └─ database: SELECT data [████] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Duration: 245ms │
│ Spans: 5 │
└─────────────────────────────────────────────────────────────────┘
from flask import Flask, jsonify
from datetime import datetime
import psycopg2
import redis
app = Flask(__name__)
@app.route('/health/live')
def liveness():
"""
Liveness probe — приложение живо?
Kubernetes убивает под, если liveness fails.
Используется для обнаружения deadlock.
"""
return jsonify({
'status': 'alive',
'timestamp': datetime.utcnow().isoformat()
}), 200
@app.route('/health/ready')
def readiness():
"""
Readiness probe — приложение готово принимать трафик?
Kubernetes не отправляет трафик, если readiness fails.
Используется во время startup и shutdown.
"""
checks = {}
# Проверка БД
try:
conn = psycopg2.connect(app.config['DATABASE_URL'])
conn.close()
checks['database'] = 'ok'
except Exception as e:
checks['database'] = f'error: {str(e)}'
# Проверка Redis
try:
r = redis.from_url(app.config['REDIS_URL'])
r.ping()
checks['redis'] = 'ok'
except Exception as e:
checks['redis'] = f'error: {str(e)}'
# Определение статуса
all_ok = all(v == 'ok' for v in checks.values())
status_code = 200 if all_ok else 503
return jsonify({
'status': 'ready' if all_ok else 'not_ready',
'checks': checks,
'timestamp': datetime.utcnow().isoformat()
}), status_code
@app.route('/health/startup')
def startup():
"""
Startup probe — приложение завершило инициализацию?
Kubernetes не проверяет liveness/readiness, пока startup fails.
Используется для медленной инициализации.
"""
# Проверка завершения миграций
if not migrations_complete():
return jsonify({
'status': 'starting',
'reason': 'migrations in progress'
}), 503
return jsonify({
'status': 'started',
'timestamp': datetime.utcnow().isoformat()
}), 200
def migrations_complete():
# Логика проверки миграций
return True# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: web
image: myapp:latest
ports:
- containerPort: 5000
# Liveness probe
livenessProbe:
httpGet:
path: /health/live
port: 5000
initialDelaySeconds: 10 # Ждать 10с перед первой проверкой
periodSeconds: 10 # Проверять каждые 10с
timeoutSeconds: 5 # Таймаут 5с
failureThreshold: 3 # 3 неудачи = перезапуск
# Readiness probe
readinessProbe:
httpGet:
path: /health/ready
port: 5000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1 # 1 успех = готов
# Startup probe (для медленной инициализации)
startupProbe:
httpGet:
path: /health/startup
port: 5000
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30 # До 150с на инициализацию# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'myapp'
static_configs:
- targets: ['myapp:5000']
metrics_path: /metrics
scrape_interval: 5s # Более частый скрейп для приложения
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)# alerting_rules.yml
groups:
- name: myapp
rules:
# Высокая задержка
- alert: HighLatency
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "High latency detected"
description: "95th percentile latency is {{ $value }}s"
# Высокий уровень ошибок
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value | humanizePercentage }}"
# Под не готов
- alert: PodNotReady
expr: kube_pod_status_ready{condition="true"} == 0
for: 1m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} is not ready"
# Мало реплик
- alert: LowReplicas
expr: kube_deployment_status_replicas_available < kube_deployment_spec_replicas
for: 5m
labels:
severity: warning
annotations:
summary: "Deployment {{ $labels.deployment }} has fewer replicas than desired"{
"dashboard": {
"title": "MyApp Overview",
"panels": [
{
"title": "Request Rate",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{ method }} {{ endpoint }}"
}
],
"type": "graph"
},
{
"title": "Error Rate",
"targets": [
{
"expr": "rate(http_requests_total{status=~\"5..\"}[5m]) / rate(http_requests_total[5m]) * 100",
"legendFormat": "Error %"
}
],
"type": "graph",
"alert": {
"conditions": [
{
"evaluator": {"params": [5], "type": "gt"},
"query": {"params": ["A", "5m", "now"]},
"reducer": {"type": "avg"}
}
]
}
},
{
"title": "Latency (p50, p95, p99)",
"targets": [
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "p50"
},
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "p95"
},
{
"expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "p99"
}
],
"type": "graph"
},
{
"title": "Active Connections",
"targets": [
{
"expr": "active_connections",
"legendFormat": "Connections"
}
],
"type": "stat"
}
]
}
}# ❌ Неправильно: нет лейблов для фильтрации
request_count = Counter('request_count', 'Total requests')
# Невозможно разбить по endpoint, method, statusРешение:
# ✅ Правильно: лейблы для измерения
request_count = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)# ❌ Неправильно: user_id как лейбл (высокая кардинальность)
requests_by_user = Counter(
'requests_by_user_total',
'Requests by user',
['user_id'] # Миллионы пользователей!
)Проблема: Миллионы уникальных user_id создают миллионы временных рядов, что перегружает Prometheus.
Решение:
# ✅ Правильно: агрегация без высокой кардинальности
requests_total = Counter(
'http_requests_total',
'Total requests',
['method', 'endpoint'] # user_id логируется, не метрикуется
)# ❌ Неправильно: нет health check endpoint
# Kubernetes не может определить готовность подаРешение:
# ✅ Правильно: все три типа health checks
@app.route('/health/live')
def liveness(): ...
@app.route('/health/ready')
def readiness(): ...
@app.route('/health/startup')
def startup(): ...Задайте себе вопросы:
# ❌ Приложение без телеметрии
from flask import Flask
app = Flask(__name__)
@app.route('/api/users')
def get_users():
# Нет метрик, нет трейсинга
users = db.query("SELECT * FROM users")
return users
# Нет health checks
# Нет метрик
# Нет трейсингаПроблемы:
# ✅ Приложение с телеметрией
from flask import Flask, g
from prometheus_client import Counter, Histogram
from opentelemetry import trace
import time
app = Flask(__name__)
# Метрики
http_requests = Counter('http_requests_total', 'Total requests', ['method', 'endpoint', 'status'])
http_duration = Histogram('http_request_duration_seconds', 'Duration', ['method', 'endpoint'])
# Трейсинг
tracer = trace.get_tracer(__name__)
@app.before_request
def before_request():
g.start_time = time.time()
@app.after_request
def after_request(response):
duration = time.time() - g.start_time
http_requests.labels(
method=request.method,
endpoint=request.endpoint,
status=response.status_code
).inc()
http_duration.labels(
method=request.method,
endpoint=request.endpoint
).observe(duration)
return response
@app.route('/api/users')
def get_users():
with tracer.start_as_current_span('get_users'):
users = db.query("SELECT * FROM users")
return users
@app.route('/health/live')
def liveness():
return {'status': 'alive'}, 200
@app.route('/health/ready')
def readiness():
# Проверка зависимостей
try:
db.health_check()
redis.ping()
return {'status': 'ready'}, 200
except Exception:
return {'status': 'not_ready'}, 503Результат:
Ключевой вывод: Телеметрия (метрики, трейсинг, health checks) обеспечивает полную наблюдаемость приложения в облачных средах. Это позволяет быстро обнаруживать и диагностировать проблемы, автоматически масштабироваться и обеспечивать высокую доступность.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.