Logo

Personal Ops Runbook

Personal runbook covering infrastructure operations for Cloud, Kubernetes, OpenStack, and Ceph environments. Includes deployment and teardown procedures, node management, cluster monitoring setup, and incident response workflows compiled from day-to-day operational work. Intended strictly for personal reference — configurations and scripts are environment-specific and not guaranteed to work as-is elsewhere.

✨ Log Monitoring Pipeline with OpenTelemetry, Loki, Grafana, and Telegram Alerts

⚙️ System Overview

+------------------+
|  /var/log/*.log  |     <-- log file
+--------+---------+
         |
         v
+-----------------------+
|  OpenTelemetry Agent  |     <-- thu thập & parse log
| (filelog receiver)    |
+-----------------------+
         |
         v
+------------------+
|     Loki         |     <-- lưu trữ log (time-series)
+------------------+
         |
         v
+------------------+
|     Grafana      |     <-- hiển thị log, cảnh báo
+------------------+
         |
         v
+------------------+
|  Alertmanager +  |
|  Telegram Alerts |
+------------------+

🛠️ Install OpenTelemetry Collector

mkdir /opt/observiq-otel-collector
cd /opt/observiq-otel-collector
wget -O otelcol.tar.gz "https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.117.0/otelcol-contrib_0.117.0_linux_amd64.tar.gz"
tar -xvf otelcol.tar.gz -C .

🖊️ Config: /opt/observiq-otel-collector/config.yml

cat > /opt/observiq-otel-collector/config.yml << 'OEF'
receivers:
  filelog/syslog:
    include:
      - /var/log/syslog
    start_at: end
    attributes:
      service: otel_agent
      farm: lab
      host_name: k3s-master1-100-151
      host_ip: 192.168.100.151
      log_type: syslog
    operators:
      - type: regex_parser
        regex: '^(?P<timestamp>[A-Za-z]{3}.*\d{1,2} \d{2}:\d{2}:\d{2}) (?P<hostname>[^\s]+)\s(?P<appname>[^\[]+)(?:\[(?P<process_id>\d+)\])?:\s(?P<message>.+)$'
        timestamp:
          parse_from: attributes.timestamp
          layout_type: strptime
          layout: '%b %e %H:%M:%S'

exporters:
  loki:
    endpoint: http://192.168.100.151:3100/loki/api/v1/push

  debug:
    verbosity: detailed

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 30
    spike_limit_percentage: 20

  batch:
    timeout: 5s
    send_batch_size: 8192
    send_batch_max_size: 20000000

  attributes/loki-labels:
    actions:
      - action: insert
        key: loki.attribute.labels
        value: hostname, appname, process_id, message, exporter
      - action: insert
        key: exporter
        value: OTLP

  resource/loki-labels:
    attributes:
      - action: insert
        key: loki.resource.labels
        value: farm, host_name, host_ip, log_type, service
      - action: insert
        key: loki.format
        value: raw

service:
  pipelines:
    logs/syslog:
      receivers: [filelog/syslog]
      processors: [memory_limiter, batch, resource/loki-labels, attributes/loki-labels]
      exporters: [loki, debug]
OEF

Run it:

/opt/observiq-otel-collector/otelcol-contrib --config /opt/observiq-otel-collector/config.yml

🧱️ Install Loki + Grafana

📁 Prepare folders

mkdir -p ./loki/tmp/loki/{chunks,index,cache,wal,compactor}
mkdir -p ./loki/rules
mkdir -p ./grafana
chown -R 472:472 ./grafana
chown -R 10001:10001 ./loki

📄 docker-compose.loki.yml

cat > ./docker-compose.yml << 'OEF'
version: '3'

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

  grafana:
    image: grafana/grafana:10.2.3
    ports:
      - "3000:3000"
    volumes:
      - ./grafana:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
OEF

📄 ./loki/local-config.yaml

cat > ./loki/local-config.yaml << 'OEF'
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9095

ingester:
  wal:
    enabled: true
    dir: /tmp/loki/wal
  lifecycler:
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
  chunk_idle_period: 5m
  max_chunk_age: 1h

schema_config:
  configs:
    - from: 2022-01-01
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 24h

storage_config:
  boltdb_shipper:
    active_index_directory: /tmp/loki/index
    cache_location: /tmp/loki/cache
    shared_store: filesystem
  filesystem:
    directory: /tmp/loki/chunks

compactor:
  working_directory: /tmp/loki/compactor
  shared_store: filesystem

limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h

chunk_store_config:
  max_look_back_period: 0s

table_manager:
  retention_deletes_enabled: true
  retention_period: 4320h

ruler:
  storage:
    type: local
    local:
      directory: /etc/loki/rules
  rule_path: /etc/loki/rules
  alertmanager_url: http://192.168.100.151:9093
  enable_alertmanager_v2: true
  enable_api: true
OEF

🚀 Run stack

docker-compose -f docker-compose.yml down
docker-compose -f docker-compose.yml up -d

🚧 Install Alertmanager + Telegram

📄 /etc/alertmanager/config.yml

global:
  resolve_timeout: 1m

route:
  receiver: telegram
  group_wait: 10s
  group_interval: 30s
  repeat_interval: 2h

receivers:
  - name: telegram
    telegram_configs:
      - bot_token: "<your_bot_token>"
        chat_id: <your_chat_id>
        parse_mode: HTML

Start Alertmanager:

docker run -d --name alertmanager \
  -v /etc/alertmanager:/etc/alertmanager \
  -p 9093:9093 \
  prom/alertmanager \
  --config.file=/etc/alertmanager/config.yml

🔒 Loki Alert Rule Example

📄 ./loki/rules/syslog-rules.yaml

cat > ./loki/rules/fake/syslog-rules.yaml << 'OEF'
namespace: storage.syslog
groups:
- name: system_log_alerts
  interval: 1m
  rules:
  - alert: SimulatedDiskError
    expr: |
      count_over_time({appname="blk_update_request"} |= "error" [1m]) > 0
    for: 0m
    labels:
      severity: critical
      product: vStorage
      category: disk_simulation
      oncall: true
      chain: alert-with-opsgenie
    annotations:
      summary: "Simulated Disk Error Detected"
      description: |
        🚨 Log from {{ $labels.hostname }} (PID {{ $labels.process_id }}):
        {{ $labels.message }}
      contact: thienln, hoanghd3, chuongdn
      priority: P3
OEF

🚧 Log Generator Script for Testing

📄 test.sh

#!/bin/bash
LOG_FILE="/var/log/kern1.log"

while true; do
  TIMESTAMP="$(date '+[%a %b %d %T %Y]')"
  echo "$TIMESTAMP blk_update_request: critical medium error, dev sdj" >> "$LOG_FILE"
  sleep 1
done
chmod +x ./test.sh
./test.sh

🖉 Bash Audit Logging to Syslog

if ! grep -q "BEGIN - AUDIT COMMAND" /etc/bash.bashrc; then
  cat <<EOF >> /etc/bash.bashrc
# BEGIN - AUDIT COMMAND
function log2syslog {
  declare COMMAND
  COMMAND=\$(fc -ln -0)
  logger -p local1.notice -t command -i -- "\${USER}:\${SSH_CONNECTION}:\${SUDO_USER}:\${COMMAND}"
}
trap log2syslog DEBUG
# END - AUDIT COMMAND
EOF
fi

source /etc/bash.bashrc

📲 Trigger test log:

logger -t blk_update_request "critical medium error, dev sdj, sector 1468989224 op 0x0:(READ) flags 0x0 phys_seg 9 prio class 0 (test alert)"
logger -t command "demo alert logs"

🔍 Check loaded rules

apt install jq -y
curl -s http://localhost:3100/loki/api/v1/rules | jq

You should see alert groups and rules listed if Loki loaded them properly.


✅ Alert Rule for Disk Errors

- alert: DemoAlertLog
  expr: |
    sum by(appname, hostname, message) (
      vector(1)
      and ({appname="command"} |= "demo alert logs")
    )
  for: 0m
  labels:
    severity: info
  annotations:
    summary: "Triggered log: {{ $labels.message }}"
    description: |
      Log from {{ $labels.hostname }} (app={{ $labels.appname }}):
      {{ $labels.message }}


- alert: SimulatedDiskError
  expr: |
    sum by(appname, hostname, process_id, message) (
      vector(1) and ({appname="blk_update_request"} |= "error")
    )
  for: 0m
  labels:
    severity: warning
    category: disk_simulation
  annotations:
    summary: "Simulated Disk Error Detected"
    description: |
      🚨 Log from {{ $labels.hostname }} (PID {{ $labels.process_id }}):
      {{ $labels.message }}

✅ Final alert Rule for Disk Errors

namespace: storage.syslog
groups:
- name: system_log_alerts
  interval: 1m
  rules:
  - alert: SimulatedDiskError
    expr: |
      count_over_time({appname="blk_update_request"} |= "error" [1m]) > 0
    for: 0m
    labels:
      severity: critical
      product: vStorage
      category: disk_simulation
      oncall: true
      chain: alert-with-opsgenie
    annotations:
      summary: "Simulated Disk Error Detected"
      description: |
        🚨 Log from {{ $labels.hostname }} (PID {{ $labels.process_id }}):
        {{ $labels.message }}
      contact: thienln, hoanghd3, chuongdn
      priority: P3

System now fully wired: OpenTelemetry → Loki → Alertmanager → Telegram
with full log labeling and real-time alerting ✅