> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# جمع السجلات Logs مركزياً مع Grafana Loki
- URL: https://arabroot.io/articles/جمع-السجلات-logs-مركزيا-مع-grafana-loki/
- Published: 2026-10-22T14:15:00.000Z
- Updated: 2026-10-23T04:21:37.000Z
- Description: كيف تشغل Grafana Loki مع Alloy وGrafana، وتختار التسميات Labels التي لا توقف الإدخال، ثم تبحث وتنبه وتحذف بلغة LogQL.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps والتشغيل, الاستضافة الذاتية, Docker والحاويات, Grafana Loki

في [مسار السجل Log Pipeline](https://arabroot.io/articles/%D9%85%D8%B3%D8%A7%D8%B1-%D8%A7%D9%84%D8%B3%D8%AC%D9%84-log-pipeline-%D9%85%D9%86-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%A8%D8%AD%D8%AB-%D9%88%D8%A7%D9%84%D8%AA%D9%86%D8%A8%D9%8A%D9%87/) وصل السطر إلى الناقل Shipper، والناقل يحتاج مخزناً مركزياً يرسل إليه، ويبحث فيه الفريق وتخرج منه التنبيهات. وأكثر مخزن تجده اليوم في السيرفرات الصغيرة والمتوسطة هو Grafana Loki، والسبب في ذلك هو نفسه سبب أغلب الأخطاء التي يقع فيها من يبدأ معه: Loki لا يفهرس السطور، وإنما يفهرس التسميات Labels فقط، فإذا اخترت التسميات بشكل صحيح كان رخيصاً وسريعاً، وإذا اخترتها بشكل خاطئ توقف عن استقبال السجلات كما سوف نرى في المختبر.

وفي هذا المقال سوف نتناول:

- كيف يخزن Loki السطر: التسميات والسلاسل Streams والأجزاء المضغوطة Chunks، والرخصة.
- تشغيل Loki مع Grafana Alloy وGrafana في Docker، وإدخال مئتي ألف سطر، والسطور التي لا تظهر في البحث، والسطور التي يرفضها لأنها قديمة.
- لغة الاستعلام LogQL بأمثلة حقيقية، والبيانات الوصفية المنظمة Structured Metadata لمعرف الطلب.
- ماذا يحدث إذا جعلت معرف الطلب تسمية، وكيف تقرأ الخطأ.
- تعدد المستأجرين Multi-tenancy، والاحتفاظ Retention، وحذف سجلات مستخدم محدد.
- التنبيه Alerting من السجلات بال Ruler، والتخزين على Object Storage في الإنتاج.

## كيف يخزن Loki السطر؟

كل سطر يصل إلى Loki يصل ومعه مجموعة تسميات، مثل `{service="payments", level="error"}`، وكل مجموعة مختلفة من قيم التسميات تسمى سلسلة Stream، والسطور داخل كل سلسلة تجمع بترتيب الوقت في أجزاء مضغوطة Chunks تحفظ كما هي بدون فهرسة، أما الفهرس Index فلا يحمل إلا التسميات وأين توجد أجزاء كل سلسلة. والصورة التالية تبين ذلك:

![ثلاث محطات مرقمة من اليمين: سطور تصل مع تسمياتها service وlevel، ثم سلسلة Stream لكل مجموعة تسميات مثل payments error وpayments info وفي كل منها أجزاء مضغوطة Chunks، ثم التخزين: الأجزاء نص مضغوط بدون فهرسة، وفهرس صغير TSDB للتسميات فقط، وتحتها الاستعلام service يساوي payments ثم البحث عن timeout، والجزء الأول يختار السلاسل من الفهرس وهو رخيص، والثاني يفك ضغط السطور ويبحث فيها وهنا يذهب الوقت](https://arabroot.io/content/images/2026/10/loki-01-streams.webp)

التسميات تختار السلاسل، والسطور نفسها يبحث فيها Loki وقت الاستعلام

وهذا التصميم له نتيجتان عليك أن تبني عليهما كل قرار:

- **التخزين رخيص:** لأن السطر يضغط ولا يبنى له فهرس كلمات، لذلك يخزن Loki الكثير على قرص صغير أو على Object Storage، وفي المختبر أخذ ملف حجمه 61 MB أقل من 10 MB بعد الضغط.
- **البحث يعتمد على التسميات:** فالاستعلام الذي يحدد سلسلة أو سلاسل قليلة سريع، والاستعلام الذي يبحث عن كلمة في كل السلاسل يفك ضغط كل شيء في الفترة التي طلبتها، لذلك فالبحث عن معرف طلب في شهر كامل أبطأ بكثير منه في مخزن يفهرس كل كلمة مثل [OpenSearch](https://arabroot.io/articles/opensearch-%D8%A7%D9%84%D8%A8%D8%AF%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D9%81%D8%AA%D9%88%D8%AD-%D9%84-elasticsearch/).

وLoki برخصة [AGPL-3.0](https://github.com/grafana/loki/blob/main/LICENSE?ref=arabroot.io) منذ [أبريل 2021](https://grafana.com/blog/2021/04/20/grafana-loki-tempo-relicensing-to-agplv3/?ref=arabroot.io)، وهذا لا يؤثر عليك إذا شغلته لسجلاتك كما هو، أما إذا عدلت كوده وقدمته خدمة لغيرك عبر الشبكة فعليك أن تنشر تعديلاتك، وتفاصيل ذلك في [مقال الرخص](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%8A-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A7%D8%AA-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D9%88%D8%AA%D8%B1%D8%A7%D8%AE%D9%8A%D8%B5%D9%87%D8%A7/).

## المختبر: Loki وAlloy وGrafana

نشغل Loki 3.7.9 بوضع البرنامج الواحد Monolithic، أي كل المكونات في Container واحد، وهذا الوضع يكفي لسيرفر واحد وحجم يومي صغير إلى متوسط. ومعه Grafana Alloy 1.21.1 ينقل سجلات تطبيق صغير، وGrafana 13.2.4 للبحث، ولا يوجد منفذ مفتوح على الجهاز إلا Grafana على `127.0.0.1`.

### ملف إعداد Loki

الملف `loki.yaml`:

```yaml
auth_enabled: true

server:
  http_listen_port: 3100
  grpc_listen_port: 9096
  log_level: warn

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

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

runtime_config:
  file: /etc/loki/overrides.yaml

limits_config:
  ingestion_rate_mb: 32
  ingestion_burst_size_mb: 64
  retention_period: 744h
  allow_structured_metadata: true

compactor:
  working_directory: /loki/compactor
  retention_enabled: true
  delete_request_store: filesystem
  compaction_interval: 1m
  delete_request_cancel_period: 1m

ruler:
  storage:
    type: local
    local:
      directory: /etc/loki/rules
  rule_path: /tmp/rules
  enable_api: true
  evaluation_interval: 30s
```

في الإعداد أعلاه لاحظ التالي:

- `auth_enabled: true` يفعل تعدد المستأجرين، فكل طلب يجب أن يحمل الترويسة `X-Scope-OrgID`، وهذه الترويسة ليست تسجيل دخول، لأن Loki لا يتحقق من أحد، وإنما تحدد مساحة البيانات، لذلك ضع أمامه Reverse Proxy يتحقق من المستخدم ويضع الترويسة بنفسه.
- `schema: v13` مع `store: tsdb` هو المخطط Schema الذي يوصي به التوثيق اليوم، والتاريخ في `from` يجب ألا يتغير بعد أن تبدأ الكتابة.
- `ingestion_rate_mb` رفعناه إلى 32، والقيمة الافتراضية بحسب [مرجع الإعدادات](https://grafana.com/docs/loki/latest/configure/?ref=arabroot.io) هي 4 MB في الثانية لكل مستأجر، وهي تكفي غالباً، ولكن إدخال ملف كبير دفعة واحدة يتجاوزها فيرد Loki بالرمز 429.
- `retention_period` مع `retention_enabled` في ال Compactor هما ما يحذف البيانات القديمة فعلاً، والقيمة الافتراضية للأول صفر، أي أن Loki لا يحذف شيئاً حتى تحددها.
- `delete_request_cancel_period` قللناه إلى دقيقة لنرى الحذف بسرعة، وقيمته الافتراضية 24 ساعة يستطيع فيها من طلب الحذف أن يلغيه.

والملف `overrides.yaml` يعطي مستأجراً بعينه مدة احتفاظ مختلفة، ويقرؤه Loki ويعيد قراءته دون إعادة تشغيل:

```yaml
overrides:
  clinic:
    retention_period: 168h
```

ثم نشغل Loki وContainer صغيراً فيه curl نرسل منه الطلبات داخل الشبكة، والمجلد `rules` فيه قاعدة التنبيه التي سوف نشرحها لاحقاً:

```bash
docker network create obs2-net
docker run -d --name obs2-loki --network obs2-net \
  -v "$PWD/loki.yaml:/etc/loki/config.yaml:ro" -v "$PWD/overrides.yaml:/etc/loki/overrides.yaml:ro" \
  -v "$PWD/rules:/etc/loki/rules:ro" -v obs2-loki-data:/loki \
  grafana/loki:3.7.9 -config.file=/etc/loki/config.yaml
docker run -d --name obs2-curl --network obs2-net curlimages/curl:8.11.1 sleep infinity
docker exec obs2-curl curl -s http://obs2-loki:3100/ready
```

وعندما يرد الأمر الأخير بالكلمة `ready` يكون Loki جاهزاً.

### إدخال مئتي ألف سطر، والسطور التي لم تظهر

البيانات من الملف `gen.py` في ملفات المختبر: 200 ألف حدث JSON لمتجر فيه ثماني خدمات، موزعة على 12 ساعة من يوم 22 أكتوبر 2026، وحجم الملف 61 MB، وكل سطر فيه الخدمة والمستوى والعميل Tenant ومعرف الطلب والمستخدم والمدة، مثل هذا السطر:

```json
{"ts": "2026-10-22T06:00:00.000Z", "level": "info", "service": "auth", "tenant": "t03", "event": "auth.request", "request_id": "12287b7a0facd7784694b5eaf3cbe2f6", "user_id": "u1082", "http_route": "/login", "duration_ms": 45.9, "http_status": 200, "message": "request completed in 45.9 ms"}
```

ونرسلها بأداة Vector، والملف `vector-loki.yaml` يجعل الخدمة والمستوى تسميتين فقط، ويرسل السطر كما هو:

```yaml
data_dir: /var/lib/vector
sources:
  dataset:
    type: file
    include: ["/data/dataset.jsonl"]
    read_from: beginning
transforms:
  prep:
    type: remap
    inputs: ["dataset"]
    source: |
      p = parse_json!(.message)
      .service = p.service
      .level = p.level
      .timestamp = parse_timestamp!(p.ts, "%+")
sinks:
  loki:
    type: loki
    inputs: ["prep"]
    endpoint: http://obs2-loki:3100
    tenant_id: shop
    encoding: { codec: text }
    labels:
      service: "{{ service }}"
      level: "{{ level }}"
```

```bash
docker run -d --name obs2-vector --network obs2-net -v "$PWD/vector-loki.yaml:/etc/vector/vector.yaml:ro" \
  -v "$PWD/data:/data:ro" timberio/vector:0.60.0-debian
```

وبعد أن انتهى Vector من الإرسال سألنا Loki كم سطراً عنده، ثم قرأنا عداد السطور التي استقبلها:

```bash
docker exec obs2-curl curl -s -G -H 'X-Scope-OrgID: shop' http://obs2-loki:3100/loki/api/v1/query \
  --data-urlencode 'query=sum(count_over_time({service=~".+"}[13h]))' --data-urlencode 'time=2026-10-22T12:30:00Z'
docker exec obs2-curl curl -s http://obs2-loki:3100/metrics | grep '^loki_distributor_lines_received_total'
```

والمخرج سوف يكون كما يلي، وقد اختصرنا الرد الأول إلى حقل القيمة:

```text
"value":[1792672200,"179657"]
loki_distributor_lines_received_total{format="loki",is_internal_stream="false",policy="",tenant="shop"} 200000
```

استقبل Loki مئتي ألف سطر، ولكن البحث يرى 179657 فقط، ولم يظهر أي خطأ في السجلات. والسبب في الإعداد `query_ingesters_within`، وقيمته الافتراضية 3 ساعات، ووصفه في مرجع الإعدادات: «أقصى فترة إلى الوراء لا ترسل بعدها الاستعلامات إلى ال Ingester». فالسطور تبقى في ذاكرة ال Ingester حتى تمتلئ أجزاؤها أو تمر عليها مدة ثم تكتب إلى التخزين، والاستعلام عن بيانات أقدم من 3 ساعات لا يسأل ال Ingester أصلاً، وبياناتنا من الأمس. وهذا لا يحدث مع السجلات الحية، وإنما عندما تدخل سجلات قديمة، مثل نقل أرشيف إلى Loki. والحل أن تطلب كتابة ما في الذاكرة بالنقطة `/flush`، ثم تعيد السؤال بعد ثوان:

```bash
docker exec obs2-curl curl -s -X POST http://obs2-loki:3100/flush -w '%{http_code}\n'
docker exec obs2-curl curl -s -G -H 'X-Scope-OrgID: shop' http://obs2-loki:3100/loki/api/v1/query \
  --data-urlencode 'query=sum(count_over_time({service=~".+"}[13h]))' --data-urlencode 'time=2026-10-22T12:30:00Z'
```

```text
204
"value":[1792672200,"200000"]
```

وبعد الكتابة كان مجلد الأجزاء `/loki/chunks` قرابة 9.7 MB، أي قرابة ستة أضعاف أصغر من الملف الأصلي، ولكن لاحظ أن مجلد ال WAL، وهو سجل الكتابة Write-Ahead Log الذي يحمي ما في الذاكرة من الضياع إذا توقف Loki فجأة، كان قرابة 63 MB، أي أكبر من البيانات نفسها، لذلك احسب له مساحة على القرص المحلي حتى لو كانت الأجزاء في Object Storage.

### سطر أقدم من أسبوع يرفض

وقد يتساءل البعض: إذا كانت السجلات القديمة تتأخر في الظهور فقط، فهل أستطيع أن أنقل أرشيف السنة الماضية إلى Loki؟ والإجابة لا بالإعداد الافتراضي، لأن `reject_old_samples` مفعل والحد `reject_old_samples_max_age` أسبوع واحد. والأمر التالي يرسل سطراً بتاريخ قبل عشرة أيام:

```bash
docker exec obs2-curl sh -c 'curl -s -w "\nHTTP %{http_code}\n" -H "X-Scope-OrgID: shop" -H "Content-Type: application/json" \
  http://obs2-loki:3100/loki/api/v1/push \
  -d "{\"streams\":[{\"stream\":{\"service\":\"payments\",\"level\":\"info\"},\"values\":[[\"$(( $(date +%s) - 10*86400 ))000000000\",\"archived line from ten days ago\"]]}]}"'
```

```text
entry for stream '{level="info", service="payments", service_name="payments"}' has timestamp too old: 2026-10-13T00:58:51Z, oldest acceptable timestamp is: 2026-10-16T00:58:51Z

HTTP 400
```

لذلك إذا أردت إدخال أرشيف قديم فارفع هذا الحد للمستأجر الذي سوف يستقبله في ملف ال Overrides، ثم أعده بعد انتهاء النقل، أو ابق الأرشيف في Object Storage كما هو وابحث فيه عند الحاجة فقط.

### نقل سجلات تطبيق حي بأداة Alloy

البيانات أعلاه من الأمس، ولنجرب التنبيه نحتاج سجلات حية، لذلك نشغل تطبيقاً صغيراً `app.py` يكتب حدثاً كل 200 مللي ثانية، وبعد دقيقتين يصبح ربعها أخطاء:

```python
# Writes one JSON event every 200 ms to /logs/app.log; from minute 2 on, a quarter of them are errors.
import json, random, time, uuid
from datetime import datetime, timezone
start = time.time()
with open("/logs/app.log", "a", buffering=1) as f:
    while True:
        bad = time.time() - start > 120 and random.random() < 0.25
        ev = {"ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
              "level": "error" if bad else "info", "service": "orders-api", "event": "order.checkout",
              "request_id": uuid.uuid4().hex, "customer_id": f"cus_{random.randint(1000, 1059)}",
              "duration_ms": round(random.uniform(20, 300), 1), "error_code": "PSP_TIMEOUT" if bad else None}
        f.write(json.dumps(ev) + "\n")
        time.sleep(0.2)
```

وينقل سجله [Grafana Alloy](https://grafana.com/docs/alloy/latest/?ref=arabroot.io)، وهو بديل Promtail كما شرحنا في [أدوات النقل](https://arabroot.io/articles/%D9%85%D8%B3%D8%A7%D8%B1-%D8%A7%D9%84%D8%B3%D8%AC%D9%84-log-pipeline-%D9%85%D9%86-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%A8%D8%AD%D8%AB-%D9%88%D8%A7%D9%84%D8%AA%D9%86%D8%A8%D9%8A%D9%87/#shippers)، فقد [انتهى دعم Promtail](https://grafana.com/docs/loki/latest/send-data/promtail/?ref=arabroot.io) في 2 مارس 2026\. والملف `config.alloy`:

```text
local.file_match "app" {
  path_targets = [{"__path__" = "/logs/*.log", "job" = "orders"}]
}

loki.source.file "app" {
  targets    = local.file_match.app.targets
  forward_to = [loki.process.app.receiver]
}

loki.process "app" {
  stage.json {
    expressions = {level = "level", service = "service", ts = "ts", request_id = "request_id"}
  }
  stage.timestamp {
    source = "ts"
    format = "RFC3339"
  }
  stage.labels {
    values = {level = "", service = ""}
  }
  stage.structured_metadata {
    values = {request_id = ""}
  }
  forward_to = [loki.write.local.receiver]
}

loki.write "local" {
  endpoint {
    url       = "http://obs2-loki:3100/loki/api/v1/push"
    tenant_id = "shop"
  }
}
```

في الإعداد أعلاه لاحظ التالي:

- إعداد Alloy مكونات Components، كل مكون له اسم ويرسل مخرجه إلى مكون آخر بالحقل `forward_to`، فتقرأ الإعداد كمسار من الملف إلى Loki.
- `stage.timestamp` يجعل وقت السطر هو الحقل `ts`، وليس وقت قراءة Alloy له.
- `stage.labels` يجعل المستوى والخدمة تسميتين، و`stage.structured_metadata` يضع `request_id` في البيانات الوصفية المنظمة Structured Metadata، وهي حقول ترافق السطر ولا تنشئ سلاسل جديدة.

```bash
docker run -d --name obs2-app --network obs2-net -v "$PWD/app.py:/app.py:ro" -v "$PWD/applogs:/logs" \
  python:3.13.16-slim python -u /app.py
docker run -d --name obs2-alloy --network obs2-net -v "$PWD/config.alloy:/etc/alloy/config.alloy:ro" \
  -v "$PWD/applogs:/logs:ro" grafana/alloy:v1.21.1 run /etc/alloy/config.alloy --storage.path=/var/lib/alloy/data
```

ونتأكد أن `request_id` ليس تسمية بسؤال Loki عن أسماء التسميات، ثم بطلب سطر واحد مع فصل التسميات عن البيانات الوصفية بالترويسة `X-Loki-Response-Encoding-Flags`:

```bash
docker exec obs2-curl curl -s -G -H X-Scope-OrgID:shop http://obs2-loki:3100/loki/api/v1/labels
docker exec obs2-curl curl -s -G -H X-Scope-OrgID:shop http://obs2-loki:3100/loki/api/v1/query_range \
  --data-urlencode 'query={service="orders-api", job="orders"}' --data-urlencode limit=1 \
  -H 'X-Loki-Response-Encoding-Flags: categorize-labels' | head -c 900
```

```json
{"status":"success","data":["filename","job","level","service","service_name"]}
{"status":"success","data":{"resultType":"streams","encodingFlags":["categorize-labels"],"result":[{"stream":{"filename":"/logs/app.log","job":"orders","level":"info","service":"orders-api","service_name":"orders-api"},"values":[["1792717177154000000","{\"ts\": \"2026-10-23T00:59:37.154Z\", \"level\": \"info\", \"service\": \"orders-api\", \"event\": \"order.checkout\", \"request_id\": \"dea0b2d9d2d743ec98e1f8e09ac3ac10\", \"customer_id\": \"cus_1002\", \"duration_ms\": 139.5, \"error_code\": null}",{"structuredMetadata":{"detected_level":"info","request_id":"dea0b2d9d2d743ec98e1f8e09ac3ac10"}}]]}],"stats":{"summary":{"bytesProcessedPerSecond":5879389,"linesProcessedPerSecond":24733,"totalBytesProcessed":38272,"totalLinesProcessed":161,"execTime":0.00651,…
```

لاحظ أن `request_id` في `structuredMetadata` وليس بين التسميات، وأن Loki أضاف من نفسه التسمية `service_name` والحقل `detected_level`، وهما من ميزات اكتشاف الخدمة والمستوى في Loki 3\. وبدون الترويسة يدمج الرد البيانات الوصفية مع التسميات في `stream`، فيبدو كأن `request_id` تسمية وهو ليس كذلك. والبحث بمعرف الطلب يكون بفلتر بعد المحدد، مثل `{service="orders-api"} | request_id="dea0b2d9d2d743ec98e1f8e09ac3ac10"`، وقد أعاد هذا الاستعلام السطر الوحيد الذي يحمل المعرف.

## لغة الاستعلام LogQL

كل استعلام في [LogQL](https://grafana.com/docs/loki/latest/query/?ref=arabroot.io) يبدأ بمحدد السلاسل Stream Selector بين قوسين معقوفين، ثم مراحل يفصلها الرمز `|`، وأهمها كما يلي:

| المرحلة                           | أمثلة                                                   | ماذا تفعل                                               |
| --------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- |
| فلتر السطر Line Filter            | \|= "timeout" و!= "healthz" و|\~ "regex"                | يبحث في نص السطر، وهو الأسرع بعد المحدد، لذلك ضعه أولاً |
| المحلل Parser                     | \| json و| logfmt و| pattern                            | يحول السطر إلى حقول وقت الاستعلام                       |
| فلتر الحقول Label Filter          | \| duration\_ms > 1000                                  | يصفي بالحقول بعد المحلل                                 |
| التنسيق Formatting                | \| line\_format                                         | يغير شكل السطر المعروض                                  |
| استعلامات المقاييس Metric Queries | count\_over\_time وrate وquantile\_over\_time مع unwrap | تحول السطور إلى أرقام ورسوم                             |

والأمثلة التالية على بيانات المتجر. الأول طلبات payments التي أخذت أكثر من ثانية مع تنسيق السطر:

```bash
docker exec obs2-curl curl -s -G -H X-Scope-OrgID:shop http://obs2-loki:3100/loki/api/v1/query_range \
  --data-urlencode 'query={service="payments"} | json | duration_ms > 1000 | line_format "{{.ts}} {{.tenant}} {{.request_id}} {{.duration_ms}}"' \
  --data-urlencode start=2026-10-22T00:00:00Z --data-urlencode end=2026-10-22T12:30:00Z --data-urlencode limit=3 \
  | grep -o '"values":\[\[[^]]*\]'
```

```text
"values":[["1792658919600000000","2026-10-22T08:48:39.600Z t08 29cd632df6d7458211282e39901554e6 1034.3"]
"values":[["1792629183528000000","2026-10-22T00:33:03.528Z t17 523e8aff1bfb921a1e806f4c156f32b2 1023.4"]
"values":[["1792665051624000000","2026-10-22T10:30:51.624Z t02 350c224dbe0324a3e8691ce687fad462 1674.7"]
```

وهي ثلاثة طلبات فقط، وهو العدد نفسه الذي حسبناه من الملف الأصلي. والثاني زمن p95 للطلبات الناجحة لكل خدمة، باستخراج الحقل `duration_ms` رقماً بالمرحلة `unwrap`:

```bash
docker exec obs2-curl curl -s -G -H X-Scope-OrgID:shop http://obs2-loki:3100/loki/api/v1/query \
  --data-urlencode 'query=quantile_over_time(0.95, {level=~"info|warn"} | json | unwrap duration_ms [13h]) by (service)' \
  --data-urlencode time=2026-10-22T12:30:00Z | grep -o '"result":.*\]\}\]'
```

```text
"result":[{"metric":{"service":"auth"},"value":[1792672200,"206.3"]},{"metric":{"service":"catalog"},"value":[1792672200,"204.12000000000006"]},{"metric":{"service":"notifications"},"value":[1792672200,"199.00499999999994"]},{"metric":{"service":"orders-api"},"value":[1792672200,"204.4"]},{"metric":{"service":"payments"},"value":[1792672200,"209.04999999999998"]},{"metric":{"service":"reports"},"value":[1792672200,"201.4"]},{"metric":{"service":"search"},"value":[1792672200,"203.975"]},{"metric":{"service":"shipping"},"value":[1792672200,"205.2299999999999"]}]
```

والثالث أكثر ثلاثة عملاء Tenants أخطاء، والحقل `tenant` ليس تسمية وإنما يستخرجه `| json` وقت الاستعلام:

```bash
docker exec obs2-curl curl -s -G -H X-Scope-OrgID:shop http://obs2-loki:3100/loki/api/v1/query \
  --data-urlencode 'query=topk(3, sum by (tenant) (count_over_time({level="error"} | json [13h])))' \
  --data-urlencode time=2026-10-22T12:30:00Z | grep -o '"result":.*\]\}\]'
```

```text
"result":[{"metric":{"tenant":"t10"},"value":[1792672200,"334"]},{"metric":{"tenant":"t16"},"value":[1792672200,"317"]},{"metric":{"tenant":"t17"},"value":[1792672200,"326"]}]
```

ولاحظ أن `topk` يختار الثلاثة الأعلى ولكنه لا يرتبها في الرد، فرتبها أنت عند العرض. وهذا الاستعلام فك ضغط كل أخطاء اليوم وحللها، وهو مقبول لستة آلاف سطر، أما إذا كان سؤالاً يتكرر كل يوم على ملايين السطور فالأفضل أن يصبح مقياساً Metric يكتبه التطبيق.

وفي Grafana تكتب الاستعلامات نفسها في صفحة Explore بعد أن تضيف Loki مصدر بيانات Data Source، والملف `grafana-datasource.yaml` يضيفه تلقائياً مع ترويسة المستأجر:

```yaml
apiVersion: 1
datasources:
  - name: Loki (shop)
    uid: loki-shop
    type: loki
    url: http://obs2-loki:3100
    isDefault: true
    jsonData:
      httpHeaderName1: X-Scope-OrgID
    secureJsonData:
      httpHeaderValue1: shop
```

```bash
docker run -d --name obs2-grafana --network obs2-net -p 127.0.0.1:34000:3000 \
  -e GF_AUTH_ANONYMOUS_ENABLED=true -e GF_AUTH_ANONYMOUS_ORG_ROLE=Admin -e GF_AUTH_DISABLE_LOGIN_FORM=true \
  -v "$PWD/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/loki.yaml:ro" grafana/grafana:13.2.4
```

والدخول المجهول Anonymous بصلاحية Admin في الأمر أعلاه لتسهيل المختبر على جهازك فقط، فلا تستخدمه على سيرفر يصل إليه غيرك. والصورتان التاليتان من المختبر، الأولى أخطاء التطبيق الحي في آخر عشر دقائق، والثانية عدد الأخطاء لكل خدمة في بيانات المتجر:

![صفحة Explore في Grafana 13 مع مصدر البيانات Loki shop والاستعلام service يساوي orders-api وlevel يساوي error ثم json ثم line_format، وتحته رسم حجم السجلات باللون الأحمر يبدأ بعد دقيقتين من تشغيل التطبيق بمجموع 254 سطراً، ثم السطور بالوقت والعميل والرمز PSP_TIMEOUT، وقائمة الحقول المستخرجة](https://arabroot.io/content/images/2026/10/loki-03-grafana-explore.webp)

أخطاء التطبيق الحي بعد أن بدأ يكتبها، والحقول استخرجها المحلل وقت الاستعلام

![رسم في Grafana للاستعلام sum by service count_over_time level يساوي error لمدة 30 دقيقة، ثمانية خطوط للخدمات بين 20 و50 خطأ في كل نصف ساعة على مدى 12 ساعة](https://arabroot.io/content/images/2026/10/loki-04-grafana-metric.webp)

استعلام مقياس على السجلات يرسم عدد الأخطاء لكل خدمة دون أي قاعدة بيانات مقاييس

ولاحظ أن الوقت في Grafana بمنطقة المتصفح الزمنية Time Zone، وهي هنا UTC+3، لذلك يبدأ الرسم الثاني عند 03:00.

## ماذا يحدث إذا أصبح معرف الطلب تسمية Label؟

شرحنا في [مقال السجلات للمطورين](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#stores) أن معرف الطلب لا يكون تسمية، والآن نرى ما يحدث فعلاً إذا فعلت. نرسل أول 20 ألف سطر من البيانات إلى مستأجر جديد اسمه `bad`، والفرق الوحيد عن الإعداد السابق سطران يجعلان `request_id` تسمية، والملف كاملاً `vector-bad-labels.yaml` في ملفات المختبر:

```yaml
      .request_id = p.request_id
…
    labels:
      service: "{{ service }}"
      level: "{{ level }}"
      request_id: "{{ request_id }}"
```

وبعد أربعين ثانية من تشغيل Vector قرأنا سجله وعدادات Loki:

```bash
docker logs obs2-vector 2>&1 | grep -E "WARN|ERROR" | head -4
docker exec obs2-curl curl -s http://obs2-loki:3100/metrics \
  | grep -E 'loki_discarded_samples_total\{.*tenant="bad"|loki_ingester_memory_streams\{|loki_distributor_lines_received_total\{.*tenant="bad"'
```

```text
2026-10-23T00:59:54.118655Z  WARN sink{component_kind="sink" component_id=loki component_type=loki}:request{request_id=3}: vector::sinks::util::retries: Retrying after error. error=Server responded with an error: 429 Too Many Requests
2026-10-23T00:59:54.649209Z  WARN sink{component_kind="sink" component_id=loki component_type=loki}:request{request_id=3}: vector::sinks::util::retries: Internal log [Retrying after error.] is being suppressed to avoid flooding.
2026-10-23T01:00:13.687432Z  WARN sink{component_kind="sink" component_id=loki component_type=loki}:request{request_id=3}: vector::sinks::util::retries: Internal log [Retrying after error.] has been suppressed 6 times.
2026-10-23T01:00:13.687501Z  WARN sink{component_kind="sink" component_id=loki component_type=loki}:request{request_id=3}: vector::sinks::util::retries: Retrying after error. error=Server responded with an error: 429 Too Many Requests
loki_discarded_samples_total{format="",policy="",reason="stream_limit",retention_hours="744",tenant="bad"} 2552
loki_distributor_lines_received_total{format="loki",is_internal_stream="false",policy="",tenant="bad"} 17737
loki_ingester_memory_streams{tenant="bad"} 5000
loki_ingester_memory_streams{tenant="shop"} 1
```

في المخرج أعلاه لاحظ التالي:

- وصل المستأجر `bad` إلى 5000 سلسلة، وهي القيمة الافتراضية للإعداد `max_global_streams_per_user`، وظهر أول رد 429 في سجل Vector في الثانية الأولى من الإرسال، ثم صار Loki يرفض كل دفعة فيها سطر يحتاج سلسلة جديدة.
- العداد `loki_discarded_samples_total` بالسبب `stream_limit` يحسب السطور المرفوضة مع إعادات المحاولة، فهو لا يساوي عدد السطور الضائعة بالضبط، ولكن أي قيمة فوق الصفر تعني أن هناك مشكلة.
- المستأجر `shop` عنده سلسلة واحدة في الذاكرة، لأن بيانات الأمس كتبت إلى التخزين، والتطبيق الحي كله سلسلة واحدة حتى تلك اللحظة.

وإذا أرسلت سطراً بنفسك ترى نص الخطأ كاملاً:

```bash
docker exec obs2-curl sh -c 'curl -s -w "\nHTTP %{http_code}\n" -H "X-Scope-OrgID: bad" -H "Content-Type: application/json" \
  http://obs2-loki:3100/loki/api/v1/push \
  -d "{\"streams\":[{\"stream\":{\"service\":\"payments\",\"request_id\":\"ffff0000\"},\"values\":[[\"$(date +%s)000000000\",\"test\"]]}]}"'
```

```text
maximum active stream limit exceeded when trying to create stream {request_id="ffff0000", service="payments", service_name="payments"}, reduce the number of active streams (reduce labels or reduce label values), or contact your Loki administrator to see if the limit can be increased, user: 'bad'

HTTP 429
```

والصورة التالية تقارن الحالتين:

![لوحتان: في اليمين تسميات قليلة القيم service وlevel، ثماني خدمات في ثلاثة مستويات تساوي 24 سلسلة لمئتي ألف سطر وrequest_id داخل السطر أو في Structured Metadata، وفي اليسار معرف الطلب كتسمية، سلسلة لكل طلب بسطر واحد، 5000 سلسلة ثم يرفض Loki الباقي، HTTP 429 وstream_limit و2552 خلال 40 ثانية من بدء إرسال 20000 سطر، والسطور المرفوضة تبقى عند الناقل يعيد المحاولة](https://arabroot.io/content/images/2026/10/loki-02-cardinality.webp)

الحل أن تخرج القيمة كثيرة الاحتمالات من التسميات، وليس أن ترفع الحد

وقد يتساءل البعض: لماذا لا نرفع الحد إلى مليون؟ والإجابة أن كل سلسلة نشطة تأخذ ذاكرة في ال Ingester ومدخلاً في الفهرس، وتوثيق Loki عن [الكاردينالية Cardinality](https://grafana.com/docs/loki/latest/get-started/labels/cardinality/?ref=arabroot.io) يقول إنها تبطئ الإدخال والاستعلام معاً، والحد موجود ليحمي Loki من هذا الخطأ تحديداً. لذلك اجعل التسمية لقيمة تعدها على أصابع اليد، مثل الخدمة والبيئة والمستوى، وضع المعرفات كلها داخل السطر أو في البيانات الوصفية المنظمة.

## تعدد المستأجرين Multi-tenancy

مع `auth_enabled: true` كل مستأجر مساحة منفصلة بحدودها ومدة احتفاظها، والطلبات التالية تبين ذلك:

```bash
docker exec obs2-curl sh -c '
curl -s -w " HTTP %{http_code}\n" -G http://obs2-loki:3100/loki/api/v1/labels
curl -s -G -H "X-Scope-OrgID: clinic" http://obs2-loki:3100/loki/api/v1/labels; echo
curl -s -G -H "X-Scope-OrgID: shop" http://obs2-loki:3100/loki/api/v1/labels; echo
curl -s -G -H "X-Scope-OrgID: shop|bad" http://obs2-loki:3100/loki/api/v1/labels'
```

```text
no org id
 HTTP 401
{"status":"success"}

{"status":"success","data":["filename","job","level","service","service_name"]}

multiple org IDs present
```

بدون الترويسة يرفض Loki الطلب، والمستأجر `clinic` لا يرى شيئاً من بيانات `shop`، والاستعلام عن مستأجرين معاً مرفوض لأنه يحتاج تفعيل `multi_tenant_queries_enabled` في ال Querier، وقيمته الافتراضية `false`. ولاحظ مرة أخرى أن هذا عزل للبيانات وليس صلاحيات Permissions، فمن يستطيع أن يرسل طلباً إلى Loki يستطيع أن يكتب أي اسم في الترويسة، والأساس العام لعزل بيانات العملاء شرحناه في [مقال تعدد المستأجرين](https://arabroot.io/articles/%D8%AA%D8%B9%D8%AF%D8%AF-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%A7%D8%AC%D8%B1%D9%8A%D9%86-multi-tenancy-%D9%81%D9%8A-saas-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

## الاحتفاظ Retention وحذف سجلات مستخدم

الحذف في Loki يقوم به ال Compactor في حالتين:

- **بعد مدة الاحتفاظ:** بحسب `retention_period` العامة أو الخاصة بكل مستأجر في ملف ال Overrides، كما في [توثيق الاحتفاظ](https://grafana.com/docs/loki/latest/operations/storage/retention/?ref=arabroot.io).
- **بطلب حذف Delete Request:** لسجلات محددة باستعلام، مثل مستخدم طلب حذف بياناته بحسب [نظام حماية البيانات الشخصية](https://laws.boe.gov.sa/BoeLaws/Laws/LawDetails/b7cfae89-828e-4994-b167-adaa00e37188/1?ref=arabroot.io).

ولنجرب الثاني، نعد سطور المستخدم `u1082` ثم نطلب حذفها، والوقتان في الطلب بالثواني منذ 1970 ليوم 22 أكتوبر من منتصف الليل حتى 12:30:

```bash
docker exec obs2-curl curl -s -G -H X-Scope-OrgID:shop http://obs2-loki:3100/loki/api/v1/query \
  --data-urlencode 'query=sum(count_over_time({service=~".+"} |= "\"user_id\": \"u1082\"" [13h]))' --data-urlencode time=2026-10-22T12:30:00Z
docker exec obs2-curl curl -s -w "HTTP %{http_code}\n" -X POST -G -H X-Scope-OrgID:shop http://obs2-loki:3100/loki/api/v1/delete \
  --data-urlencode 'query={service=~".+"} |= "\"user_id\": \"u1082\""' --data-urlencode start=1792627200 --data-urlencode end=1792672200
docker exec obs2-curl curl -s -H X-Scope-OrgID:shop http://obs2-loki:3100/loki/api/v1/delete
```

```text
"value":[1792672200,"48"]
HTTP 204
[{"request_id":"2674b3d5","start_time":1792627200,"end_time":1792672200,"query":"{service=~\".+\"} |= \"\\\"user_id\\\": \\\"u1082\\\"\"","status":"received","created_at":1792717293.211,"user_id":""}]
```

وبعد دقيقتين أصبحت حالة الطلب `processed`، ولكن العد بعدها مباشرة ما زال 48، ولم يختف المستخدم من نتائج البحث إلا بعد قرابة ثماني دقائق من طلب الحذف:

```text
[{"request_id":"2674b3d5","start_time":1792627200,"end_time":1792672200,"query":"{service=~\".+\"} |= \"\\\"user_id\\\": \\\"u1082\\\"\"","status":"processed","created_at":1792717293.211,"user_id":""}]

"result":[]
"value":[1792672380,"199952"]
```

والعدد الكلي للمستأجر صار 199952، أي 200000 ناقص 48\. والدرس هنا أن تتحقق من الحذف بالبحث نفسه وليس بحالة الطلب فقط، وأن تحسب للحذف مدة الإلغاء `delete_request_cancel_period` ومدة التنفيذ عندما تعد العميل بموعد. ولاحظ أن الحقل `user_id` في الرد هو من طلب الحذف وليس المستخدم المحذوف، وأن الحذف لا يصل إلى النسخ الاحتياطية Backups من مجلد Loki إن وجدت.

## التنبيه Alerting من السجلات بال Ruler

ال Ruler في Loki يقيم استعلامات LogQL على فترات ويطلق تنبيهات بصيغة Prometheus، والملف `rules/shop/rules.yaml`، واسم المجلد هو اسم المستأجر:

```yaml
groups:
  - name: orders
    rules:
      - alert: OrdersErrorBurst
        expr: sum by (service) (count_over_time({service="orders-api", level="error"}[1m])) > 5
        for: 1m
        labels:
          severity: page
        annotations:
          summary: "orders-api wrote {{ $value }} errors in the last minute"
```

وبعد أن بدأ التطبيق يكتب الأخطاء سألنا ال Ruler عن التنبيهات، فكان التنبيه `pending` أولاً لأن الشرط يجب أن يستمر دقيقة كاملة بحسب `for`، ثم صار `firing`:

```bash
docker exec obs2-curl curl -s -H X-Scope-OrgID:shop http://obs2-loki:3100/prometheus/api/v1/alerts
```

```json
{"status":"success","data":{"alerts":[{"labels":{"alertname":"OrdersErrorBurst","service":"orders-api","severity":"page"},"annotations":{"summary":"orders-api wrote 71 errors in the last minute"},"state":"firing","activeAt":"2026-10-23T01:01:32.867607243Z","value":"7.1e+01"}]},"errorType":"","error":""}
```

ولم نربط ال Ruler بأداة إرسال، لذلك يبقى التنبيه في واجهته البرمجية فقط، وفي الإنتاج تضع في إعداده `alertmanager_url` فيرسل التنبيه إلى Alertmanager، أو تكتب القاعدة نفسها في تنبيهات Grafana. ولاحظ أن التنبيه من السجلات مناسب للأحداث التي ليس لها مقياس، أما معدل الأخطاء العام فمكانه الطبيعي المقاييس Metrics، كما سوف نشرح في مقالات المراقبة القادمة بإذن الله.

## التخزين في الإنتاج: Object Storage

التخزين على نظام الملفات في المختبر مناسب لسيرفر واحد فقط، أما في الإنتاج فيوصي التوثيق بأن تكون الأجزاء والفهرس في Object Storage متوافق مع S3، والذي يتغير في الإعداد هو `object_store: s3` في المخطط وقسم `storage_config` بعنوان التخزين واسم ال Bucket ومفاتيحه. ولم نشغل ذلك في هذا المختبر، أما تثبيت تخزين S3 على سيرفرك فشرحناه في [دليل تخزين الكائنات S3](https://arabroot.io/articles/%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-%D8%A7%D9%84%D9%83%D8%A7%D8%A6%D9%86%D8%A7%D8%AA-s3-%D8%B9%D9%84%D9%89-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D9%83-%D8%A7%D9%84%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-%D9%88%D8%A7%D9%84%D8%A8%D9%86%D9%8A%D8%A9/)، والمقارنة بين أدواته في [أي تخزين S3 يناسبك](https://arabroot.io/articles/%D8%A7%D9%8A-%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-s3-%D9%85%D8%B3%D8%AA%D8%B6%D8%A7%D9%81-%D8%B0%D8%A7%D8%AA%D9%8A%D8%A7-%D9%8A%D9%86%D8%A7%D8%B3%D8%A8%D9%83/). وتذكر أن Loki مع Object Storage يحتاج أيضاً قرصاً محلياً لل WAL ولذاكرة الفهرس المؤقتة، وقد رأينا أن ال WAL وحده كان أكبر من البيانات المضغوطة.

## التشغيل اليومي Operations

وفيما يلي ما تراقبه في Loki بعد التثبيت:

- **الرفض:** العداد `loki_discarded_samples_total` بحسب السبب `reason`، فأي قيمة فوق الصفر تعني سطوراً لم تحفظ، سواءً بسبب حد السلاسل أو حد المعدل أو السطور القديمة.
- **السلاسل:** `loki_ingester_memory_streams` لكل مستأجر، فارتفاعه المفاجئ يعني تسمية جديدة كثيرة القيم أضافها أحد الفرق.
- **الذاكرة Memory:** في المختبر كان Loki قرابة 380 MiB مع التطبيق الحي والتنبيه وبيانات اليوم السابق، وAlloy قرابة 54 MiB، وGrafana قرابة 481 MiB.
- **القرص:** احسب مساحة ال WAL على القرص المحلي، فقد كان قرابة 63 MB مقابل أقل من 10 MB للأجزاء المضغوطة.
- **النسخ الاحتياطي Backup:** مع Object Storage تكون البيانات في ال Bucket، فاحمه بالإصدارات Versioning أو بالنسخ إلى مكان آخر، ومع نظام الملفات انسخ مجلد `/loki` كاملاً.
- **الوصول Access:** لا تفتح منفذ Loki 3100 للإنترنت أبداً، والسبب أنه لا يتحقق من أحد.

## أخطاء شائعة Common Mistakes

- **معرف الطلب أو المستخدم أو عنوان IP تسمية:** فيصل المستأجر إلى حد السلاسل ويتوقف الإدخال.
- **إدخال أرشيف قديم ثم البحث فوراً:** فلا تظهر السطور التي في ذاكرة ال Ingester حتى تكتب إلى التخزين أو تطلب `/flush`، وما كان أقدم من أسبوع يرفض أصلاً.
- **نسيان `retention_enabled` في ال Compactor:** فلا تحذف أي بيانات مهما كانت `retention_period`.
- **الاعتقاد أن `X-Scope-OrgID` تسجيل دخول:** وهو مجرد اسم مساحة يكتبه من يرسل الطلب.
- **استعلام على كل السلاسل لفترة طويلة:** مثل `{service=~".+"} |= "…"` على شهر كامل، فيفك ضغط كل شيء.
- **البدء بأداة Promtail اليوم:** وقد انتهى دعمها في 2 مارس 2026، فابدأ بأداة Alloy.

## تدريب عملي

- أضف تسمية `tenant` إلى إعداد Vector، واحسب عدد السلاسل الناتج قبل أن ترسل، ثم قارنه بالعداد `loki_ingester_memory_streams`.
- اكتب قاعدة تنبيه تطلق عندما يظهر الرمز `PSP_TIMEOUT` أكثر من عشر مرات في خمس دقائق لأي خدمة.
- اجعل مدة الاحتفاظ يوماً واحداً للمستأجر `bad` في ملف ال Overrides، وراقب حجم مجلد `chunks`.

## حذف المختبر

```bash
docker rm -f obs2-loki obs2-app obs2-alloy obs2-grafana obs2-vector obs2-curl
docker volume rm obs2-loki-data
docker network rm obs2-net
docker rmi grafana/loki:3.7.9 grafana/alloy:v1.21.1 grafana/grafana:13.2.4
```

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- Loki يفهرس التسميات فقط ويضغط السطور، لذلك أخذ ملف حجمه 61 MB أقل من 10 MB، والبحث السريع يبدأ دائماً بتسميات تختار سلاسل قليلة.
- التسمية لقيمة قليلة الاحتمالات مثل الخدمة والمستوى، ومعرف الطلب في السطر أو في البيانات الوصفية المنظمة، وإلا رفض Loki الإدخال عند 5000 سلسلة كما رأينا.
- السجلات القديمة لا تظهر في البحث حتى تكتب من ذاكرة ال Ingester، والأقدم من أسبوع يرفض بالإعداد الافتراضي.
- تعدد المستأجرين يعزل البيانات ولا يتحقق من أحد، والاحتفاظ والحذف بطلب يقوم بهما ال Compactor ويحتاجان وقتاً، وال Ruler يطلق تنبيهات من استعلامات LogQL.

[![](https://arabroot.io/content/images/2026/10/log-pipeline-cover-cover.webp)دليلمسار السجل Log Pipeline من التطبيق إلى البحث والتنبيهكيف ينتقل السجل من التطبيق إلى المخزن عبر الناقل Shipper، وكيف تخفي البيانات الشخصية وتوجه الأخطاء وتحمي السجلات إذا توقفت الوجهة.](https://arabroot.io/articles/%D9%85%D8%B3%D8%A7%D8%B1-%D8%A7%D9%84%D8%B3%D8%AC%D9%84-log-pipeline-%D9%85%D9%86-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%A8%D8%AD%D8%AB-%D9%88%D8%A7%D9%84%D8%AA%D9%86%D8%A8%D9%8A%D9%87/)[![](https://arabroot.io/content/images/2026/10/logging-basics-cover-cover.webp)دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/graylog-cover-cover.webp)دليلالسجلات المركزية Logs مع Graylog وOpenSearchكيف تثبت Graylog فوق OpenSearch، وتوجه الرسائل بال Streams وتخفي بياناتها بال Pipelines وتنبه منها، وما الذي لا يجده بحثه العربي.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-logs-%D9%85%D8%B9-graylog-%D9%88-opensearch/)[![](https://arabroot.io/content/images/2026/10/opensearch-cover-cover.webp)دليلOpenSearch: البديل المفتوح لـ Elasticsearch وقصة الترخيصلماذا ولد OpenSearch من Elasticsearch، وكيف تشغله على سيرفرك بصلاحيات دقيقة وتنبيهات حقيقية على سجلاتك.](https://arabroot.io/articles/opensearch-%D8%A7%D9%84%D8%A8%D8%AF%D9%8A%D9%84-%D8%A7%D9%84%D9%85%D9%81%D8%AA%D9%88%D8%AD-%D9%84-elasticsearch/)[![](https://arabroot.io/content/images/2026/10/s3-self-hosted-cover-cover.webp)دليلتخزين الكائنات S3 على سيرفرك: التثبيت والبنيةكيف تثبت ثلاث أدوات تخزين S3 مفتوحة المصدر في إعداد صغير يتحمل الأعطال، وماذا يحدث فعلاً عندما يتوقف سيرفر أو يتلف قرص.](https://arabroot.io/articles/%D8%AA%D8%AE%D8%B2%D9%8A%D9%86-%D8%A7%D9%84%D9%83%D8%A7%D8%A6%D9%86%D8%A7%D8%AA-s3-%D8%B9%D9%84%D9%89-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D9%83-%D8%A7%D9%84%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-%D9%88%D8%A7%D9%84%D8%A8%D9%86%D9%8A%D8%A9/)

## سجل التحديثات

- أكتوبر 2026: كتابة الدليل وتنفيذ المختبر بإصدارات Loki 3.7.9 وGrafana Alloy 1.21.1 وGrafana 13.2.4 وVector 0.60.0.