DevOps Academy

Monitoring & Logging

Prometheus, Alertmanager, PromQL, journald, Grafana, ו-Distributed Tracing — כפי שנראים ב-production

תיאוריה

⏱ ~55 דקות

ב-2026, כמעט כל חברת DevOps ישראלית בגודל בינוני ומעלה רצה Prometheus + Grafana. Datadog ו-New Relic פופולריים בסטארטאפים עם תקציב, אבל בראיונות עבודה ובסביבות ייצור — Prometheus הוא שם המשחק. ה-market_frequency של הנושא עומד על 2.4 בשוק הנוכחי, מה שמשמעותו שאנשי DevOps מתחילים מופתעים כמה מהר מצופה מהם לכתוב alerting rules מהיום הראשון.

הגישה הנפוצה בישראל: Prometheus scrapes metrics מ-exporters ומ-application endpoints, Grafana מציג dashboards, ו-Alertmanager שולח התראות ל-PagerDuty, Slack, או OpsGenie. AWS CloudWatch מופעל בצד עבור ה-infra ה-managed (RDS, EKS nodes).

נקודת כשל שכיחה: צוות שמקים Prometheus ו-Grafana, יוצר dashboards יפים, אבל לא מגדיר אף alert rule. המוניטורינג קיים, אבל לא מזהיר על כלום. התגלית הזו קורית בדרך כלל ב-3 לפנות בוקר.

Prometheus פועל על מודל pull: הוא שולח בקשת HTTP GET ל-/metrics endpoint של כל target, ומאחסן את התוצאות ב-TSDB (Time Series Database) הפנימי שלו. זאת ההיפוך ממערכות push כמו StatsD.

RETENTION ברירת המחדל היא 15 יום. ב-production תמצאו --storage.tsdb.retention.time=30d או יותר, ולעיתים Thanos או Cortex לאחסון ארוך טווח.

המבנה:
- **prometheus.yml** מגדיר את scrape_configs — רשימת targets, intervals, relabeling
- **Exporters** חושפים /metrics עבור תוכנות שלא תומכות ב-Prometheus natively (node_exporter, blackbox_exporter, postgres_exporter)
- **Pushgateway** עבור jobs קצרי מועד (batch jobs, cron) שלא ניתן ל-scrape אותם
- **Alertmanager** מקבל alerts מ-Prometheus ומנתב אותם לנמענים

דוגמת prometheus.yml בסיסית:

PromQL היא שפת השאילתות של Prometheus. טעות שכיחה של מתחילים: משתמשים ב-raw counter במקום ב-rate(). Counter רק עולה — אם השרת restart, הוא מתאפס, והגרף קופץ. rate() מחשב שינוי לשנייה בתוך חלון זמן ומטפל ב-resets אוטומטית.

**rate() vs irate()**
- rate(metric[5m]) — ממוצע על 5 דקות, מחליק spikes, טוב ל-alerts ו-dashboards
- irate(metric[5m]) — רגע אחרון בתוך 5 דקות, רגיש ל-spikes, טוב ל-real-time debugging

**increase()** — כמה עלה counter בחלון זמן (sum, לא per-second):

increase(http_requests_total[1h])  # סה"כ בקשות בשעה האחרונה

**histogram_quantile()** — לחישוב percentiles מ-histogram:

histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

זה חישוב ה-p99 latency. הטעות השכיחה: לשכוח את [le] label ב-by() clause כאשר מחשבים per-label.

**label matching ו-vector operations**:

# error rate as ratio:
rate(http_requests_total{status=~"5.."}[5m])
/
rate(http_requests_total[5m])

חשוב: שני vectors חייבים להיות matchable על ה-labels. אם labels שונים — משתמשים ב-on() או ignoring().

**label_replace()** — לשנות label values בשאילתה:

label_replace(up, "short_name", "$1", "job", "(.+)-service")

SLI (Service Level Indicator) הוא מדד שמייצג את בריאות השירות. SLO (Service Level Objective) הוא היעד שהסכמנו עליו. בישראל, SLO קלאסי: 99.9% availability ב-30 יום, p99 latency < 500ms.

**PromQL לאחוז ה-availability** (מבוסס על probe/blackbox_exporter):

# availability over 30-day window (requires recording rules for efficiency)
avg_over_time(up{job="api"}[30d]) * 100

**error_rate SLI**:

(
  1 - (
    rate(http_requests_total{status!~"5.."}[5m])
    /
    rate(http_requests_total[5m])
  )
) * 100

**Recording Rules**: שאילתות SLO כבדות עדיף לחשב כ-recording rules — Prometheus מחשב אותן מראש ושומר כ-metric חדש:

groups:
  - name: slo_recording
    interval: 1m
    rules:
      - record: job:http_request_duration_seconds:p99
        expr: |
          histogram_quantile(0.99,
            sum(rate(http_request_duration_seconds_bucket[5m]))
            by (le, job)
          )
      - record: job:http_error_rate:ratio
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m])) by (job)
          /
          sum(rate(http_requests_total[5m])) by (job)

**Error Budget**: אחוז זמן שנותר מה-error budget:

# error budget remaining % (99.9% SLO)
(1 - (
  sum(rate(http_requests_total{status=~"5.."}[30d]))
  /
  sum(rate(http_requests_total[30d]))
)) / 0.001 * 100

כאשר הערך מגיע ל-0%, ה-error budget אזל — הצוות צריך להפסיק feature work ולהתמקד ב-reliability.

Alert rule היא PromQL expression שאם היא מחזירה ערכים — Prometheus יוצר alert. ה-for: duration מגדיר כמה זמן ה-condition חייב להיות אמיתי לפני שה-alert יורה — זה מונע flapping על spikes קצרים.

מבנה של alert rule תקין:

groups:
  - name: api_alerts
    rules:
      - alert: HighErrorRate
        expr: |
          (
            rate(http_requests_total{status=~"5.."}[5m])
            /
            rate(http_requests_total[5m])
          ) > 0.05
        for: 2m
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "High error rate on {{ $labels.job }}"
          description: "Error rate is {{ $value | humanizePercentage }} (threshold: 5%)"
          runbook_url: "https://wiki.example.com/runbooks/high-error-rate"

**חוקי for: duration בייצור**:
- for: 0m — alert מיידי, רק לדברים קריטיים שצריכים תגובה מידית (disk full at 99%)
- for: 2m - for: 5m — ברירת מחדל סבירה לרוב ה-alerts
- for: 15m — לסטטיסטיקות איטיות כמו memory growth

**Labels וחשיבותם**: ה-labels בתוך alert rule הם מה ש-Alertmanager משתמש בו לניתוב. אם severity=critical הולך ל-PagerDuty, ו-severity=warning הולך ל-Slack — זה קורה בגלל label matching.

**Annotations**: אינפורמציה הניתנת לקריאה על ידי אנשים. summary קצר (כותרת), description מפורט, runbook_url — הקישור שה-on-call engineer יפתח ב-3 לפנות בוקר. ה-runbook_url הוא לא nice-to-have, הוא חובה.

**Template variables**: בתוך annotations, ניתן להשתמש ב-Go template syntax — {{ $labels.instance }}, {{ $value | humanize }}.

Alert fatigue הוא המצב בו צוות מקבל כל כך הרבה התראות שהם מתחילים להתעלם מהן. זה לא תיאורטי: 200 alerts ביום כאשר 95% הם noise מביא לכך שהצוות מוחה את ה-Slack alert-channel ב-DND, וה-critical alerts האמיתיים מוחמצים.

**Alertmanager מנגנוני ניהול fatigue**:

1. **Grouping**: מרכז alerts דומים לתוך notification אחת
2. **inhibit_rules**: מדכא alerts נגזרים כאשר alert ראשי כבר פועל
3. **group_wait** / **group_interval** / **repeat_interval**: שולט תזמון notifications
4. **Silences**: השתקה ידנית לחלון זמן (בזמן maintenance)

**מבנה alertmanager.yml**:

global:
  resolve_timeout: 5m
  slack_api_url: 'https://hooks.slack.com/services/...'

route:
  group_by: ['alertname', 'job']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'slack-low'
  routes:
    - match:
        severity: critical
      receiver: 'pagerduty'
      repeat_interval: 1h
    - match:
        severity: warning
      receiver: 'slack-warnings'

receivers:
  - name: 'pagerduty'
    pagerduty_configs:
      - routing_key: '<pagerduty-key>'
  - name: 'slack-warnings'
    slack_configs:
      - channel: '#alerts-warnings'
        title: '{{ .GroupLabels.alertname }}'
        text: '{{ .CommonAnnotations.summary }}'
  - name: 'slack-low'
    slack_configs:
      - channel: '#alerts-info'

inhibit_rules:
  - source_match:
      alertname: 'InstanceDown'
    target_match_re:
      alertname: 'HighErrorRate|SlowP99Latency'
    equal: ['instance']

**inhibit_rules בפירוט**: כאשר InstanceDown פועל על instance X — כל alert אחר על אותו instance (HighErrorRate, SlowP99Latency) מושתק. בלי זה, כשל בשרת אחד מייצר 10+ alerts שכולם מגיעים ל-PagerDuty — alert fatigue קלאסי.

**repeat_interval**: כמה זמן להמתין לפני שולחים reminder על alert שממשיך. 4h לרוב ה-warnings זה סביר — אחרת Slack מתמלא כל שעה.

**group_wait**: כמה זמן לחכות לפני שולחים את ה-notification ראשונה, כדי לאגד alerts שמגיעים יחד (דוגמה: deployment שמוריד 5 שירותים — מגיע כ-notification אחת).

כל שירות systemd כותב logs ל-journald. ב-Ubuntu/Debian production servers, זה אומר שאם אתם מחפשים logs של nginx, api, postgres — הם ב-journal, לא בהכרח ב-/var/log/.

**אחסון journal**: ברירת המחדל היא volatile (נמחק ב-reboot). ל-persistent storage:

mkdir -p /var/log/journal
systemctl restart systemd-journald

**journalctl flags חיוניים**:

# logs של unit ספציפי
journalctl -u nginx.service

# logs מ-timeframe ספציפי
journalctl -u nginx --since "2026-04-17 03:00:00" --until "2026-04-17 03:30:00"

# שורות אחרונות + follow
journalctl -u api -n 100 -f

# רק errors (PRIORITY <= 3: emerg, alert, crit, err)
journalctl -u nginx -p err

# PRIORITY levels: 0=emerg, 1=alert, 2=crit, 3=err, 4=warning, 5=notice, 6=info, 7=debug
journalctl -u nginx -p 4  # warning ומעלה (כלומר, severity 4 ומטה)

# JSON output לעיבוד
journalctl -u nginx -o json --since "1 hour ago" | jq '.MESSAGE'

# חיפוש לפי שדה מובנה
journalctl _SYSTEMD_UNIT=nginx.service _PID=1234

# גודל journal
journalctl --disk-usage

# ניקוי journal ישן
journalctl --vacuum-time=7d

**JSON output fields**:
כאשר משתמשים ב--o json, כל שורה היא JSON עם fields כגון:
- MESSAGE — תוכן הלוג
- PRIORITY — מספר 0-7 (7=debug, 0=emerg)
- _SYSTEMD_UNIT — שם ה-unit
- _PID — process ID
- __REALTIME_TIMESTAMP — epoch microseconds
- SYSLOG_IDENTIFIER — שם התהליך

**Scenario ב-production**: שירות קרס ב-3 לפנות בוקר:

# מצא מתי בדיוק קרס
journalctl -u api --since "2026-04-17 02:50:00" --until "2026-04-17 03:10:00" -p err

# הבא JSON לעיבוד
journalctl -u api --since "2026-04-17 02:50:00" -o json | \
  python3 -c "
import sys, json
for line in sys.stdin:
    entry = json.loads(line)
    if int(entry.get('PRIORITY', 7)) <= 3:
        ts = entry.get('__REALTIME_TIMESTAMP', '')
        msg = entry.get('MESSAGE', '')
        print(f'[{ts}] {msg}')
"

**journald + logrotate**: journald מנהל את ה-rotation שלו (SystemMaxUse ב-journald.conf). ל-log files שה-service כותב ל-/var/log/ בנפרד (לא דרך journald) — logrotate נדרש, כפי שלמדנו ב-Linux Advanced.

Grafana מחבר בין Prometheus לבין visibility. הגדרת data source בסיסית בממשק: Settings → Data sources → Add → Prometheus → URL: http://prometheus:9090.

בייצור, Grafana מוגדר as-code:

# grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090
    isDefault: true
    editable: false
    jsonData:
      timeInterval: '15s'
      httpMethod: POST

**Dashboard Variables**: מאפשרים פאנלים דינמיים — בחר instance, job, או namespace מ-dropdown:

# Variable definition (type: query)
label_values(up{job="api"}, instance)

אז בשאילתה: rate(http_requests_total{instance="$instance"}[5m])

**Repeating Panels**: פאנל שמשכפל את עצמו לכל ערך של variable — מאד שימושי לבנות service overview dashboard שמכסה כל ה-instances.

**Alert Rules ב-Grafana**: מ-Grafana 8+, ניתן להגדיר alert rules ישירות ב-Grafana (Unified Alerting). ב-production רוב הצוותים מעדיפים Prometheus-native alerts (שנשמרים ב-git), אבל Grafana alerts שימושיים לצוותים שעובדים יותר ב-UI.

**Dashboard as Code**: ייצוא dashboard ל-JSON ושמירה ב-git. grafana-dashboard-watcher / Terraform grafana provider מאפשרים GitOps לדashboards.

Metrics עונים על 'מה קורה'. Logs עונים על 'מה קרה'. Traces עונות על 'למה הבקשה הזו הייתה איטית / נכשלה'.

**מושגים בסיסיים**:
- **Trace**: ייצוג מלא של מסע בקשה אחת דרך המערכת (מ-client ועד DB ובחזרה)
- **Span**: יחידת עבודה אחת בתוך trace (call ל-service, query ל-DB). לכל span יש start_time, end_time, operation_name, ו-attributes
- **Trace ID**: מזהה ייחודי לכל trace (128-bit hex)
- **Span ID**: מזהה ייחודי לכל span (64-bit hex)
- **Parent Span ID**: קשר בין spans — יוצר עץ

**W3C traceparent header**: הסטנדרט לפרופגציה של trace context:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
           |  |                                |               |  |
           |  version                          trace-id        |  flags (01=sampled)
                                                               span-id

כל service שמקבל בקשה HTTP חייב:
1. לקרוא את ה-traceparent header
2. ליצור span חדש עם parent_span_id = ה-span-id מה-header
3. להעביר traceparent header מעודכן לכל call שהוא שולח downstream

**OpenTelemetry (OTel)**: הסטנדרט המוביל ל-2026. SDK ל-Python, Node.js, Java, Go. מייצא לבחירה: Jaeger, Zipkin, Grafana Tempo, AWS X-Ray.

דוגמת instrumentation ב-Python:

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

provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

tracer = trace.get_tracer(__name__)

@app.route('/api/users/<id>')
def get_user(id):
    with tracer.start_as_current_span("get-user") as span:
        span.set_attribute("user.id", id)
        result = db.query(f"SELECT * FROM users WHERE id={id}")
        span.set_attribute("db.rows_returned", len(result))
        return jsonify(result)

**Sampling**: ב-production, tracing 100% של בקשות זה יקר. Sampling strategies:
- **Head-based**: החלטה בתחילת trace (פשוטה, מחמיצה errors נדירים)
- **Tail-based**: החלטה בסוף trace — שומר 100% של errors, מדגם את ה-rest (מורכב יותר, OTel Collector נדרש)

**Grafana Tempo**: backend לאחסון traces. מתחבר ל-Grafana ומאפשר: מ-dashboard panel של Prometheus (high latency) → drill-down לexemplar → פתיחת trace ב-Tempo — הכל בממשק אחד.

CloudWatch הוא ה-built-in monitoring של AWS. ב-production, Prometheus + CloudWatch משלימים זה את זה: Prometheus לאפליקציה, CloudWatch ל-AWS infra (RDS, EKS nodes, Lambda).

**CloudWatch Alarms**:
- Alarm מוגדר על metric + threshold + period + evaluation_periods
- State: OK → ALARM → INSUFFICIENT_DATA
- Actions: SNS notification → Lambda / PagerDuty / Slack

דוגמה ב-AWS CLI:

aws cloudwatch put-metric-alarm \
  --alarm-name "api-cpu-high" \
  --alarm-description "API CPU above 80% for 5 minutes" \
  --metric-name CPUUtilization \
  --namespace AWS/ECS \
  --dimensions Name=ServiceName,Value=api \
  --period 300 \
  --evaluation-periods 2 \
  --threshold 80 \
  --comparison-operator GreaterThanThreshold \
  --statistic Average \
  --alarm-actions arn:aws:sns:us-east-1:123456789:alerts

**CloudWatch Log Insights**: SQL-like query language לניתוח logs:

fields @timestamp, @message
| filter @message like /ERROR/
| sort @timestamp desc
| limit 50

**Metric Filters**: יוצרים CloudWatch metric מ-log patterns:

aws logs put-metric-filter \
  --log-group-name /aws/ecs/api \
  --filter-name "error-count" \
  --filter-pattern "[level=ERROR]" \
  --metric-transformations \
    metricName=ErrorCount,metricNamespace=Custom/API,metricValue=1,defaultValue=0

**CloudWatch + Prometheus**: AWS Managed Prometheus (AMP) ו-AWS Managed Grafana (AMG) מאפשרים להריץ Prometheus/Grafana fully managed ב-AWS. ADOT (AWS Distro for OpenTelemetry) מאפשר לשלוח traces ל-AWS X-Ray.

**Cross-service integration**: ב-EKS, CloudWatch Container Insights + Prometheus Operator + Grafana Tempo יחד מייצרים observability stack מלא: metrics + logs + traces — מה שמכונה 'The Three Pillars of Observability'.


תרגול מעשי

1.

2.

3.


שאלות חיבור

חבר את מה שלמדת בנושא זה לנושאים קודמים. אין תשובה אחת נכונה — חשיבה ביקורתית היא המטרה.

שאלת חיבור 1

systemd (Linux Advanced, topic #1) ו-journald (Monitoring, topic #10) עובדים יחד. הסבר: (1) כיצד systemd service שכתבנו ב-Linux Advanced מייצר logs שנכנסים ל-journald, (2) כיצד לחלץ logs של שירות ספציפי עם journalctl מ-unit שהגדרנו, ו-(3) כיצד logrotate (Linux Advanced) ו-journald מנהלים retention בצורה שונה.

מחבר ל:
Linux Advanced

שאלת חיבור 2

Kubernetes (topic #9) מריץ Pods ב-cluster — כיצד Prometheus monitoring משתלב? הסבר: (1) כיצד Prometheus scrapes metrics מPods ב-Kubernetes (service discovery), (2) מה ה-annotations שמוסיפים ל-Pod/Service כדי שPrometheus יגלה אותם אוטומטית, ו-(3) כיצד alerting rule על p99 latency משתלב עם HPA (HorizontalPodAutoscaler) שלמדנו.

מחבר ל:
Kubernetes

מוכן לבחינה?

בצע הערכה תיאורטית, תרגול CLI ושאלות חיבור כדי לסיים את הנושא.