> ## 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 مع Graylog وOpenSearch
- URL: https://arabroot.io/articles/السجلات-المركزية-logs-مع-graylog-و-opensearch/
- Published: 2026-10-22T14:30:00.000Z
- Updated: 2026-10-23T04:21:38.000Z
- Description: كيف تثبت Graylog فوق OpenSearch، وتوجه الرسائل بال Streams وتخفي بياناتها بال Pipelines وتنبه منها، وما الذي لا يجده بحثه العربي.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps والتشغيل, الاستضافة الذاتية, Docker والحاويات, Graylog

عندما يطلب فريق التشغيل بحثاً في السجلات وتنبيهات وصلاحيات لكل فريق من واجهة واحدة، دون أن يبني ذلك بنفسه من Kibana أو Grafana، فغالباً سوف يصل إلى Graylog. فهو يستقبل السجلات بمدخلات Inputs جاهزة مثل GELF وSyslog، ويوجهها إلى Streams، ويعدلها بال Pipelines، ويطلق التنبيهات، ويخزن الرسائل في OpenSearch. وقد شرحنا في [دليل Grafana Loki](https://arabroot.io/articles/%D8%AC%D9%85%D8%B9-%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logs-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A7-%D9%85%D8%B9-grafana-loki/) المخزن الذي يفهرس التسميات فقط، أما هنا فنثبت البديل الذي يفهرس كل حقل، ونرى ما تكسبه منه وما تدفعه مقابله، بالبيانات نفسها.

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

- مكونات Graylog: الخادم وMongoDB وOpenSearch أو ال Data Node، وطريق الرسالة بينها، وجدول التوافق Compatibility.
- الرخصة SSPL، وما الذي في Graylog Open وما الذي في الإصدارات المدفوعة.
- التثبيت في Docker، وإدخال مئتي ألف رسالة بمدخل GELF، والبحث بالنص العربي وما الذي لا يجده.
- ال Streams وال Pipelines بمثال حقيقي يخفي معرف المستخدم، ولماذا لم تعد تحتاج ال Extractors.
- تنبيه من تعريف حدث Event Definition، ثم نوقف OpenSearch ونرى ماذا يحدث للرسائل.

## مكونات Graylog وطريق الرسالة

Graylog ليس مخزناً بنفسه، وإنما خادم يقف بين المصادر والمخزن، ويحتاج برنامجين معه:

- **Graylog Server:** برنامج Java يستقبل الرسائل ويعالجها ويقدم الواجهة والواجهة البرمجية API.
- **MongoDB:** قاعدة بيانات Database يحفظ فيها الإعدادات فقط، مثل المستخدمين والمدخلات وال Streams، وليس الرسائل.
- **OpenSearch:** يحفظ الرسائل نفسها ويبحث فيها، وهو إما OpenSearch تديره أنت، أو ال [Data Node](https://hub.docker.com/r/graylog/graylog-datanode?ref=arabroot.io)، وهو OpenSearch تغلفه Graylog وتتولى إعداده وشهاداته وانضمام عقده Nodes من واجهتها.

والصورة التالية تبين طريق الرسالة داخل الخادم:

![مخطط من اليمين: تطبيقات وأجهزة ترسل إلى Graylog Server، وفيه خمس محطات مرقمة: المدخلات GELF وSyslog وBeats وRaw، ثم ال Journal على القرص يحفظ الرسالة قبل المعالجة، ثم ال Streams توجه بقواعد على الحقول، ثم ال Pipelines مربوطة بال Streams، ثم OpenSearch يحفظ الرسائل في Index Sets، ومعه MongoDB للإعدادات فقط، وتحت المحطات تعريفات الأحداث Event Definitions تبحث في ال Streams على فترات وتطلق تنبيهاً ترسله الإشعارات](https://arabroot.io/content/images/2026/10/graylog-01-architecture.webp)

الرسالة لا تظهر في البحث حتى تمر بالمحطات كلها

وأهم ما في الصورة ال Journal، فهو ملف على قرص الخادم تكتب فيه كل رسالة فور وصولها قبل أي معالجة، فإذا أبطأ OpenSearch أو توقف تتراكم الرسائل في ال Journal ولا تضيع، وهذا يشبه [الذاكرة المؤقتة على القرص](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/#buffering) في الناقل، ولكنه داخل Graylog نفسه، وسوف نجربه في نهاية المختبر.

### ال Data Node أم OpenSearch تديره أنت؟

ملف Docker Compose الرسمي في [مستودع Graylog](https://github.com/Graylog2/docker-compose?ref=arabroot.io) يبدأ مع ال Data Node، وأول تشغيل له يمر بواجهة إعداد أولى Preflight في المتصفح تصدر منها الشهادات وتضيف العقد، وهذا مناسب إذا لم يكن عندك OpenSearch وتريد أن تتولى Graylog إدارته. أما في هذا الدليل فاخترنا OpenSearch تديره أنت، لأن كثيراً من الفرق عندها OpenSearch أصلاً كما في [دليل 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/)، ولأن الإعداد كله يكون في متغيرات البيئة فتعيده بأمر واحد.

وقبل أن تختار الإصدارات افتح [جدول التوافق](https://go2docs.graylog.org/current/downloading%5Fand%5Finstalling%5Fgraylog/compatibility%5Fmatrix.htm?ref=arabroot.io)، فبحسبه اليوم يقبل Graylog 7.2 مع OpenSearch تديره أنت ما يلي:

| المكون     | أقل إصدار | أعلى إصدار |
| ---------- | --------- | ---------- |
| MongoDB    | 7.x       | 8.2.x      |
| OpenSearch | 1.1.x     | 3.7.x      |

ولاحظ أن آخر إصدار من OpenSearch عند كتابة هذا الدليل هو 3.9.0، وآخر MongoDB هو 9.0، وكلاهما خارج الجدول، لذلك لا تحدث OpenSearch أو MongoDB في سيرفر Graylog قبل أن يظهر الإصدار الجديد في الجدول، والسبب أن Graylog يتعامل مع واجهة OpenSearch البرمجية مباشرة، وأي تغيير فيها قد يوقف الكتابة. ويذكر الجدول أيضاً أن OpenSearch 3 مقبول للتثبيت الجديد، وللترقية إذا لم تكن هناك فهارس Indices أنشئت قبل الإصدار 2.0.

## الرخصة License: SSPL، وما الذي تدفع ثمنه

غيرت Graylog رخصة الخادم من GPL إلى SSPL في [ملف الرخصة](https://github.com/Graylog2/graylog2-server/blob/master/LICENSE?ref=arabroot.io) في 16 نوفمبر 2020 مع الإصدار 4.0، وهي الرخصة نفسها التي استخدمتها MongoDB ثم Elasticsearch، ولا تعدها مبادرة المصدر المفتوح OSI رخصة مفتوحة، وتفاصيل ذلك في [قصة رخصة Elasticsearch](https://arabroot.io/articles/%D9%85%D8%AC%D9%85%D9%88%D8%B9%D8%A9-elk-elasticsearch-%D9%88-logstash-%D9%88-kibana/#license). وعملياً لا تؤثر عليك إذا شغلت Graylog لسجلات جهتك، أما إذا قدمته خدمة لعملاء خارجيين فاقرأ شروطها. وتقول صفحة [Graylog Open](https://graylog.org/products/source-available/?ref=arabroot.io) (اطلعنا عليها في أكتوبر 2026) إنه مجاني «بدون رسوم لكل مستخدم، وبدون حد للإدخال، وبدون فترة تجربة محدودة»، وإن الإصدارات المدفوعة تضيف ما يلي:

- **Graylog Enterprise:** بحيرة بيانات Data Lake مع متابعة استهلاكها، ودعم فني على مدار الساعة.
- **Graylog Security:** قدرات SIEM مثل قواعد الربط Correlation Rules والتحليل السلوكي ومحتوى كشف جاهز وإدارة التحقيق في الحوادث.

ولاحظ في الواجهة أن قائمتي Enterprise وSecurity ظاهرتان في النسخة المفتوحة، وأن Graylog تسأل عند أول دخول هل تسمح بإرسال بيانات استخدام مجهولة، فاختر Cancel إذا كانت سياسة جهتك تمنع ذلك.

## المختبر: Graylog 7.2 مع OpenSearch 3.7 وMongoDB 8.0

واجهة Graylog على `127.0.0.1:34001` فقط، وباقي المنافذ داخل شبكة Docker. والملف `up.sh` يشغل الثلاثة وينشئ مدخلين: GELF TCP على 12201 للبيانات الكبيرة، وGELF HTTP على 12202 للتجارب الصغيرة:

```bash
#!/bin/bash
# Starts OpenSearch, MongoDB and Graylog for the lab and creates two inputs: GELF TCP (12201) and GELF HTTP (12202).
export MSYS_NO_PATHCONV=1
docker network create obs2-net >/dev/null 2>&1
docker run -d --name obs2-curl --network obs2-net curlimages/curl:8.11.1 sleep infinity >/dev/null 2>&1
SEC=$(head -c 72 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 96)
SHA=$(printf 'lab-Graylog-2026!' | sha256sum | cut -d' ' -f1)
docker run -d --name obs2-os --network obs2-net -e discovery.type=single-node -e DISABLE_SECURITY_PLUGIN=true \
  -e DISABLE_INSTALL_DEMO_CONFIG=true -e OPENSEARCH_JAVA_OPTS="-Xms1g -Xmx1g" -e action.auto_create_index=false \
  -v obs2-os-data:/usr/share/opensearch/data opensearchproject/opensearch:3.7.0 >/dev/null
docker run -d --name obs2-mongo --network obs2-net -v obs2-mongo-data:/data/db mongo:8.0.34 >/dev/null
until docker exec obs2-curl curl -sf http://obs2-os:9200/_cluster/health >/dev/null; do sleep 3; done
docker run -d --name obs2-graylog --network obs2-net -p 127.0.0.1:34001:9000 \
  -e GRAYLOG_PASSWORD_SECRET="$SEC" -e GRAYLOG_ROOT_PASSWORD_SHA2="$SHA" \
  -e GRAYLOG_HTTP_BIND_ADDRESS=0.0.0.0:9000 -e GRAYLOG_HTTP_EXTERNAL_URI=http://127.0.0.1:34001/ \
  -e GRAYLOG_ELASTICSEARCH_HOSTS=http://obs2-os:9200 -e GRAYLOG_MONGODB_URI=mongodb://obs2-mongo:27017/graylog \
  -e GRAYLOG_SERVER_JAVA_OPTS="-Xms1g -Xmx1g" -v obs2-graylog-data:/usr/share/graylog/data graylog/graylog:7.2.1 >/dev/null
until docker exec obs2-curl curl -sf -u 'admin:lab-Graylog-2026!' http://obs2-graylog:9000/api/system/lbstatus >/dev/null 2>&1; do sleep 5; done
until docker exec obs2-curl curl -sf "http://obs2-os:9200/graylog_0" >/dev/null 2>&1; do sleep 3; done
docker exec obs2-curl sh -c '
G="curl -s -u admin:lab-Graylog-2026! -H X-Requested-By:lab -H Content-Type:application/json"
$G http://obs2-graylog:9000/api/system/inputs -d "{\"title\":\"GELF TCP\",\"type\":\"org.graylog2.inputs.gelf.tcp.GELFTCPInput\",\"global\":true,\"configuration\":{\"bind_address\":\"0.0.0.0\",\"port\":12201,\"recv_buffer_size\":1048576,\"number_worker_threads\":2,\"use_null_delimiter\":true,\"max_message_size\":2097152,\"decompress_size_limit\":8388608,\"tls_enable\":false}}"; echo
$G http://obs2-graylog:9000/api/system/inputs -d "{\"title\":\"GELF HTTP\",\"type\":\"org.graylog2.inputs.gelf.http.GELFHttpInput\",\"global\":true,\"configuration\":{\"bind_address\":\"0.0.0.0\",\"port\":12202,\"recv_buffer_size\":1048576,\"number_worker_threads\":2,\"max_chunk_size\":65536,\"decompress_size_limit\":8388608,\"tls_enable\":false,\"enable_cors\":true,\"idle_writer_timeout\":60}}"; echo'
```

والمخرج سوف يكون معرفي المدخلين، وقد أخذ التشغيل كله قرابة دقيقتين ونصف:

```json
{"id":"6adab8a5146a5dafb633ca5e"}
{"id":"6adab8a5146a5dafb633ca62"}
```

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

- `GRAYLOG_PASSWORD_SECRET` سر عشوائي بطول 96 حرفاً يشفر به Graylog كلمات المرور والقيم الحساسة في MongoDB، فاحفظه في مكان آمن، والسبب أنك إذا فقدته أو غيرته لا يستطيع Graylog قراءة ما شفره به.
- `GRAYLOG_ROOT_PASSWORD_SHA2` هاش Hash كلمة مرور المدير الأول، ولا تغيرها من الواجهة وإنما من الإعداد نفسه.
- `GRAYLOG_ELASTICSEARCH_HOSTS` يشير إلى OpenSearch، ووجوده هو ما يجعل Graylog يتجاوز واجهة ال Preflight الخاصة بال Data Node.
- عطلنا أمان OpenSearch لأنه في شبكة Docker داخلية لا يصل إليها غير Graylog، أما في الإنتاج فشغله بالشهادات والحسابات كما في [دليل 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/#lab-security).
- أنشأنا المدخلات من الواجهة البرمجية بالترويسة `X-Requested-By` التي يشترطها Graylog لكل طلب يغير شيئاً، والطريقة نفسها من الواجهة في System ثم Inputs، كما في الصورة التالية:

![صفحة Inputs في Graylog 7.2 وفيها مدخلان يعملان: GELF HTTP على المنفذ 12202 وGELF TCP على المنفذ 12201 لكل عقد Graylog، مع أزرار Stop input وMore](https://arabroot.io/content/images/2026/10/graylog-02-inputs.webp)

المدخلان اللذان أنشأهما الملف up.sh

### إدخال مئتي ألف رسالة بمدخل GELF

صيغة [GELF](https://go2docs.graylog.org/current/getting%5Fin%5Flog%5Fdata/gelf.html?ref=arabroot.io) هي صيغة Graylog الأصلية، وهي JSON فيه حقول ثابتة مثل `host` و`short_message` و`timestamp`، وكل حقل إضافي يبدأ بالشرطة السفلية مثل `_service`. والبيانات هي نفسها التي أدخلناها في [دليل Loki](https://arabroot.io/articles/%D8%AC%D9%85%D8%B9-%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logs-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A7-%D9%85%D8%B9-grafana-loki/#lab-ingest): 200 ألف حدث JSON لمتجر فيه ثماني خدمات، من الملف `gen.py` في ملفات المختبر، وحجمها 61 MB. ونرسلها بأداة Vector، والتحويل التالي يضيف الشرطة السفلية لكل حقل:

```yaml
data_dir: /var/lib/vector
sources:
  dataset:
    type: file
    include: ["/data/dataset.jsonl"]
    read_from: beginning
transforms:
  gelf:
    type: remap
    inputs: ["dataset"]
    source: |
      e = object!(parse_json!(.message))
      out = {"host": "lab-shop", "message": e.message, "timestamp": parse_timestamp!(e.ts, "%+")}
      for_each(e) -> |k, v| {
        if k != "message" && k != "ts" {
          out = set!(out, ["_" + k], v)
        }
      }
      . = out
sinks:
  graylog:
    type: socket
    inputs: ["gelf"]
    mode: tcp
    address: obs2-graylog:12201
    encoding: { codec: gelf }
    framing:
      method: character_delimited
      character_delimited: { delimiter: "\0" }
```

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

وبعد أن وصلت الرسائل كلها إلى الفهرس `graylog_0` بحثنا عن رسالة واحدة بمعرف الطلب:

```bash
docker exec obs2-curl curl -s -G -u 'admin:lab-Graylog-2026!' -H Accept:application/json \
  http://obs2-graylog:9000/api/search/universal/absolute --data-urlencode "query=request_id:12287b7a0facd7784694b5eaf3cbe2f6" \
  --data-urlencode from=2026-10-22T00:00:00.000Z --data-urlencode to=2026-10-22T12:30:00.000Z --data-urlencode limit=1
```

```json
{"query":"request_id:12287b7a0facd7784694b5eaf3cbe2f6","built_query":"request_id:12287b7a0facd7784694b5eaf3cbe2f6","used_indices":[],"messages":[{"highlight_ranges":{},"message":{"gl2_accounted_message_size":268,"http_route":"/login","gl2_receive_timestamp":"2026-10-23 01:30:47.801","level":"info","gl2_remote_ip":"192.168.96.6","gl2_remote_port":34974,"streams":["000000000000000000000001"],"gl2_message_id":"01M5H33SR0000RD83FDXADXJZP","source":"lab-shop","message":"request completed in 45.9 ms","gl2_source_input":"6adab8a5146a5dafb633ca5e","gl2_input_message_size":309,"duration_ms":45.9,"gl2_processing_timestamp":"2026-10-23 01:31:41.937","user_id":"u1082","service":"auth","http_status":200,"gl2_source_node":"8daaaf51-d086-4527-b2fd-5814b014de72","_id":"8008f63f-ce81-11f1-8121-5a32b6c4a127","event":"auth.request","request_id":"12287b7a0facd7784694b5eaf3cbe2f6","tenant":"t03","gl2_processing_duration_ms":54136,"timestamp":"2026-10-22T06:00:00.000Z"},"index":"graylog_0","decoration_stats":null}],"fields":[],"time":524,"total_results":1,…}
```

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

- استقبل Graylog هذه الرسالة عند 01:30:47 وعالجها عند 01:31:41، والحقل `gl2_processing_duration_ms` يقول إنها انتظرت 54 ثانية، أي أنها بقيت في ال Journal لأن المعالجة والكتابة إلى OpenSearch كانتا أبطأ من الاستقبال وقت الدفعة الكبيرة.
- أضاف Graylog إلى كل رسالة حقول `gl2_*` عن المدخل والعقدة والوقت، وهي مفيدة في التحقيق ولكنها تكبر الفهرس.
- الحقل `timestamp` هو وقت الحدث من السطر نفسه، وليس وقت وصوله، لذلك يظهر في البحث في يومه الصحيح.

وحجم الفهرس بعد الإدخال:

```bash
docker exec obs2-curl curl -s "http://obs2-os:9200/_cat/indices/graylog_*?v&h=index,docs.count,store.size,pri.store.size"
```

```text
index     docs.count store.size pri.store.size
graylog_0     200000     62.9mb         62.9mb
```

أي أن الفهرس أكبر قليلاً من الملف الأصلي نفسه، بينما أخذت البيانات نفسها في Loki أقل من 10 MB، وهذا هو الثمن الذي تدفعه مقابل أن يكون كل حقل وكل كلمة في `message` مفهرسة وجاهزة للبحث الفوري.

### البحث، والنص العربي

البحث في Graylog بصيغة Lucene، مثل `level:error AND message:timeout`، والصورة التالية من المختبر:

![صفحة البحث في Graylog 7.2 مع الاستعلام level:error AND message:timeout على يوم 22 أكتوبر 2026 من منتصف الليل إلى 12:30، والنتيجة 1527 رسالة، ورسم عدد الرسائل لكل فترة، وتحته الرسائل payment provider timeout after 2000 ms بالوقت والمصدر lab-shop](https://arabroot.io/content/images/2026/10/graylog-04-search.webp)

1527 رسالة، وهو العدد الصحيح الذي حسبناه من الملف الأصلي

أما النص العربي ففي البيانات حقل منفصل اسمه `message_ar` في 5% من الرسائل، فيه أربع جمل مثل «فشل الدفع بسبب انتهاء المهلة»، وجربنا البحث فيه بأربع طرق:

```bash
docker exec obs2-curl sh -c "G='curl -s -u admin:lab-Graylog-2026! -H Accept:application/json'; \
for q in 'message_ar:\"انتهاء المهلة\"' 'message_ar:المهلة' 'message_ar:مهلة' 'message_ar:\"فشل الدفع بسبب انتهاء المهلة\"'; do \
printf '%s -> ' \"\$q\"; \$G -G http://obs2-graylog:9000/api/search/universal/absolute --data-urlencode \"query=\$q\" \
--data-urlencode from=2026-10-22T00:00:00.000Z --data-urlencode to=2026-10-22T12:30:00.000Z --data-urlencode limit=1 \
| grep -o '\"total_results\":[0-9]*'; done"
docker exec obs2-curl curl -s "http://obs2-os:9200/graylog_0/_mapping/field/message_ar,message"
```

```text
message_ar:"انتهاء المهلة" -> "total_results":0
message_ar:المهلة -> "total_results":0
message_ar:مهلة -> "total_results":0
message_ar:"فشل الدفع بسبب انتهاء المهلة" -> "total_results":2554
{"graylog_0":{"mappings":{"message_ar":{"full_name":"message_ar","mapping":{"message_ar":{"type":"keyword"}}},"message":{"full_name":"message","mapping":{"message":{"type":"text","analyzer":"standard"}}}}}}
```

صفر في الطرق الثلاث الأولى، والسبب في السطر الأخير: Graylog يحفظ كل حقل نصي إضافي بالنوع `keyword`، أي قيمة كاملة لا تقسم إلى كلمات، أما الحقل `message` فهو نص `text` يقسمه المحلل Analyzer إلى كلمات. لذلك فالحقل الإضافي لا يجده إلا البحث بقيمته الكاملة، وقد وجد 2554 رسالة، وهو العدد الصحيح من الملف الأصلي. ثم أرسلنا ثلاث رسائل عربية في الحقل `message` نفسه عبر مدخل GELF HTTP بالخدمة `arabic-test`، وبحثنا فيها بالاستعلام نفسه مسبوقاً بالشرط `service:arabic-test AND`:

```bash
docker exec obs2-curl sh -c 'for m in "فشل الدفع بسبب انتهاء المهلة" "تم إنشاء الطلب بنجاح" "مهلة الاتصال بمزود الدفع انتهت"; do
  curl -s -o /dev/null -w "%{http_code} " -H "Content-Type: application/json" http://obs2-graylog:12202/gelf \
    -d "{\"version\":\"1.1\",\"host\":\"lab-shop\",\"short_message\":\"$m\",\"_service\":\"arabic-test\",\"_level\":\"info\"}"
done; echo'
```

```text
202 202 202
message:المهلة -> "total_results":1
message:مهلة -> "total_results":1
message:"انتهاء المهلة" -> "total_results":1
message:الدفع -> "total_results":2
```

البحث بالكلمة يعمل الآن، ولكن لاحظ النتيجتين الأوليين: `المهلة` وجدت الرسالة الأولى فقط، و`مهلة` وجدت الثالثة فقط، رغم أن الكلمة واحدة، والسبب أن المحلل الافتراضي Standard Analyzer يقسم النص بالمسافات ولا يعرف أن «ال» ليست جزءاً من الكلمة. والحل أن تعرف للفهرس محللاً عربياً بقالب فهرس Index Template خاص، كما شرحنا في [المحلل العربي في ELK](https://arabroot.io/articles/%D9%85%D8%AC%D9%85%D9%88%D8%B9%D8%A9-elk-elasticsearch-%D9%88-logstash-%D9%88-kibana/#analyzers)، أو أن يبحث المستخدم بالصيغتين، أو أن يكتب التطبيق رسائل السجل بالإنجليزية ويبقي العربية لما يعرض للمستخدم، وهذا الأخير هو الأسهل في التشغيل.

### ال Streams: توجيه الرسائل بقواعد

ال Stream مجموعة رسائل تطابق قواعد Rules على الحقول، وتستخدمه للصلاحيات والتنبيهات ومدة الاحتفاظ، فكل Stream يمكن أن يكتب في مجموعة فهارس Index Set مختلفة. ننشئ Stream لأخطاء الدفع، ولاحظ أن Graylog 7 يطلب الكائن الجديد داخل الحقل `entity`، فالطلب بالصيغة القديمة يرد بالخطأ التالي:

```text
{"type":"RequestError","message":"Error at \"\" [1, 52]: entity cannot be null","line":1,"column":52,"reference_path":"org.graylog.security.shares.CreateEntityRequest"} HTTP 400
```

والملف `stream.sh` يقرأ معرف مجموعة الفهارس الافتراضية، ثم ينشئ ال Stream ويشغله:

```bash
docker exec obs2-curl sh -c '
G="curl -s -u admin:lab-Graylog-2026! -H X-Requested-By:lab -H Content-Type:application/json -H Accept:application/json"
IDX=$($G http://obs2-graylog:9000/api/system/indices/index_sets | grep -o "\"id\":\"[^\"]*\",\"title\":\"Default index set\"" | cut -d\" -f4)
$G http://obs2-graylog:9000/api/streams -d "{\"entity\":{\"title\":\"Payment errors\",\"description\":\"service=payments and level=error\",\"index_set_id\":\"$IDX\",\"matching_type\":\"AND\",\"remove_matches_from_default_stream\":false,\"rules\":[{\"field\":\"service\",\"type\":1,\"value\":\"payments\",\"inverted\":false},{\"field\":\"level\",\"type\":1,\"value\":\"error\",\"inverted\":false}]}}" | tee /tmp/stream.json; echo
SID=$(grep -o "\"stream_id\":\"[^\"]*\"" /tmp/stream.json | cut -d\" -f4); echo $SID > /tmp/sid
$G -X POST http://obs2-graylog:9000/api/streams/$SID/resume -w "resume HTTP %{http_code}\n"
'
```

```text
{"stream_id":"6adab998146a5dafb633cb05"}
resume HTTP 204
```

والقاعدة `type: 1` تعني تطابقاً كاملاً للقيمة، و`matching_type: AND` يعني أن القاعدتين معاً شرط، وال Stream ينشأ متوقفاً حتى تشغله بالطلب `resume`. وقد يتساءل البعض: هل يضم ال Stream أخطاء الدفع التي أدخلناها قبله، وعددها 727؟ والإجابة لا، فالبحث في هذا ال Stream على يوم 22 أكتوبر أعاد `"total_results":0`، لأن التوجيه يحدث وقت وصول الرسالة فقط، لذلك أنشئ ال Streams قبل أن تبدأ السجلات بالوصول.

### ال Pipelines بدل ال Extractors: إخفاء معرف المستخدم

في الإصدارات القديمة كانت ال Extractors على كل مدخل هي طريقة تحليل الرسائل، ولكن [توثيق Graylog](https://go2docs.graylog.org/current/making%5Fsense%5Fof%5Fyour%5Flog%5Fdata/extractors.htm?ref=arabroot.io) يصفها اليوم بأنها «ميزة قديمة Legacy»، ويوصي بال Pipelines بدلاً منها لأنها تدعم الشروط وتعمل على رسائل عدة مدخلات معاً. وال Pipeline سلسلة مراحل Stages من القواعد Rules تعمل على رسائل ال Stream الذي تربطها به، والقاعدة التالية تستبدل `user_id` بالهاش SHA-256 له:

```text
rule "hash user id"
when
  has_field("user_id")
then
  set_field("user_id", sha256(to_string($message.user_id)));
end
```

ثم ننشئ Pipeline اسمه `privacy` بمرحلة واحدة فيها القاعدة، ونربطه بال Stream الافتراضي، والأوامر كاملة في الملف `pipeline.sh` في ملفات المختبر، وآخر سطر في مخرجها هو الربط:

```text
pipeline "privacy"
stage 0 match either
  rule "hash user id";
end
```

```text
{"stream_id":"000000000000000000000001","pipeline_ids":["6adab9a3146a5dafb633cb17"],"id":"6adab9a3146a5dafb633cb1a"}
```

![صفحة Pipeline privacy في Graylog وفيها أن ال Pipeline يعالج رسائل Default Stream، ومرحلة واحدة Stage 0 فيها القاعدة hash user id بدون أخطاء](https://arabroot.io/content/images/2026/10/graylog-03-pipeline.webp)

ال Pipeline مربوط بال Stream الافتراضي، وفيه مرحلة واحدة بقاعدة واحدة

وأرسلنا بعدها عبر GELF HTTP بالملف `send-test.sh` ثماني رسائل خطأ لخدمة payments والمستخدم `u1082`، ورسالة نجاح للخدمة نفسها، ورسالة خطأ لخدمة shipping، ثم بحثنا في ال Stream وحسبنا الهاش بأنفسنا:

```bash
docker exec obs2-curl sh -c 'SID=$(cat /tmp/sid); curl -s -G -u admin:lab-Graylog-2026! -H Accept:application/json \
  http://obs2-graylog:9000/api/search/universal/relative --data-urlencode "query=streams:$SID" \
  --data-urlencode range=600 --data-urlencode limit=1 --data-urlencode fields=service,level,user_id' \
  | grep -o '"user_id":"[^"]*"\|"total_results":[0-9]*'
printf 'u1082' | sha256sum
```

```text
"user_id":"969fd298d98211ed0f024d08caa5a36c54e6d1fea53536b6d19c723f585449fb"
"total_results":8
969fd298d98211ed0f024d08caa5a36c54e6d1fea53536b6d19c723f585449fb *-
```

في ال Stream الرسائل الثماني فقط، فرسالة النجاح ورسالة shipping لم تطابقا القاعدتين، وقيمة `user_id` في المخزن هي الهاش وليست `u1082`. ولكن لاحظ ما يكشفه السطر الأخير: الهاش هو نفسه الذي يحسبه أي أحد، ومن يعرف أن القيمة SHA-256 لمعرف قصير يستطيع أن يحسب الهاش لكل المعرفات المحتملة، وهي هنا خمسة آلاف فقط، ويجد صاحبه في ثوان، لذلك فهذا إخفاء جزئي Pseudonymisation وليس إخفاء كاملاً للهوية Anonymisation، والأفضل أن تضيف إلى القيمة سراً قبل الهاش، أو ألا يرسل التطبيق المعرف أصلاً كما شرحنا في [إخفاء البيانات في الطريق](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/#redaction).

### التنبيه Alerting: تعريف حدث Event Definition

التنبيهات في Graylog تعريفات أحداث تبحث في Stream كل فترة، وإذا تحقق الشرط تنشئ حدثاً، وإذا كان الحدث تنبيهاً ترسله الإشعارات Notifications مثل البريد أو Webhook. والملف `alert.sh` ينشئ تعريفاً يبحث في Stream أخطاء الدفع كل دقيقة، ويطلق تنبيهاً إذا زادت الرسائل على خمس، وهو أيضاً داخل الحقل `entity`، وأهم حقوله:

```json
{"type":"aggregation-v1","streams":["6adab998146a5dafb633cb05"],"series":[{"id":"count-","type":"count"}],
 "conditions":{"expression":{"expr":">","left":{"expr":"number-ref","ref":"count-"},"right":{"expr":"number","value":5}}},
 "search_within_ms":60000,"execute_every_ms":60000}
```

وبعد أقل من دقيقة من إرسال الرسائل الثماني ظهر الحدث في بحث الأحداث `/api/events/search`:

```json
{"events":[{"event":{"id":"01M5K6CZDTHYSVEZFRSX6DNYND","event_definition_type":"aggregation-v1","event_definition_id":"6adab9be146a5dafb633cb2c","origin_context":null,"timestamp":"2026-10-23T01:35:55.021Z","timestamp_processing":"2026-10-23T01:35:55.322Z","timerange_start":"2026-10-23T01:34:55.021Z","timerange_end":"2026-10-23T01:35:55.021Z","streams":["000000000000000000000002"],"source_streams":["6adab998146a5dafb633cb05"],"message":"Payment errors burst: count()=8.0","source":"ea66c644da3a","key_tuple":[],"key":null,"priority":2,"scores":{},"associated_assets":[],"tags":[],"alert":true,"fields":{},"group_by_fields":{},"aggregation_conditions":{"count":8.0},…},"index_name":"gl-events_0","index_type":"message"}],…}
```

![صفحة Alerts and Events في Graylog وفيها رسم الأحداث والتنبيهات على أسبوع، وسطر واحد: Payment errors burst count يساوي 8 بأولوية Medium ونوع Alert وتعريف Payment errors burst من نوع Filter and Aggregation](https://arabroot.io/content/images/2026/10/graylog-05-alerts.webp)

التنبيه في صفحة Alerts، ولم نربطه بإشعار فبقي في الواجهة فقط

ولاحظ أن الحدث نفسه يحفظ في فهرس خاص `gl-events_0`، فتستطيع أن تبحث في التنبيهات القديمة كما تبحث في الرسائل.

### نجرب العطل: ماذا يحدث إذا توقف OpenSearch؟

قلنا إن ال Journal يحمي الرسائل إذا توقف المخزن، والآن نتأكد. نوقف OpenSearch، ونرسل 500 رسالة، ثم نسأل Graylog عن ال Journal:

```bash
docker stop obs2-os
docker exec obs2-curl sh -c 'i=0; while [ $i -lt 500 ]; do i=$((i+1)); curl -s -o /dev/null -H Content-Type:application/json \
  http://obs2-graylog:12202/gelf -d "{\"version\":\"1.1\",\"host\":\"lab-shop\",\"short_message\":\"outage test $i\",\"_service\":\"outage-test\",\"_level\":\"info\"}"; done; echo sent 500'
docker exec obs2-curl curl -s -u admin:lab-Graylog-2026! -H Accept:application/json http://obs2-graylog:9000/api/system/journal \n  | grep -o '"uncommitted_journal_entries":[0-9]*'
docker logs obs2-graylog --since 2m 2>&1 | grep -iE "WARN|ERROR" | tail -3
```

```text
sent 500
"uncommitted_journal_entries":500
2026-10-23 01:40:35,236 WARN : org.graylog2.indexer.messages.Messages - Caught exception during bulk indexing: ElasticsearchException{message=java.io.IOException: obs2-os, errorDetails=[]}, retrying (attempt #12).
2026-10-23 01:40:36,128 ERROR: org.graylog.storage.opensearch3.ClusterAdapterOS - obs2-os (obs2-os)
2026-10-23 01:40:37,128 ERROR: org.graylog.storage.opensearch3.ClusterAdapterOS - obs2-os (obs2-os)
```

استقبل المدخل الرسائل كلها رغم أن المخزن متوقف، وبقيت 500 رسالة غير مكتوبة Uncommitted في ال Journal، وGraylog يعيد محاولة الكتابة ويسجل ذلك. ثم شغلنا OpenSearch بالأمر `docker start obs2-os`، وبعد قرابة دقيقتين، وهي مدة إقلاع OpenSearch، صار العداد صفراً، وأعاد البحث `service:outage-test` النتيجة `"total_results":500`، أي أن رسالة واحدة لم تضع.

ولكن ال Journal له حدود، ورد النقطة نفسها `/api/system/journal` بدون `grep` يحمل إعداده الافتراضي:

```json
"journal_config":{"directory":"file:///usr/share/graylog/data/journal/","segment_size":104857600,"segment_age":3600000,"max_size":5368709120,"max_age":43200000,"flush_interval":1000000,"flush_age":60000}
```

أي 5 GB كحد أقصى للحجم، و12 ساعة كحد أقصى للعمر، لذلك احسب كم رسالة تصلك في أطول توقف تتوقعه للمخزن، وارفع `message_journal_max_size` و`message_journal_max_age` إذا لزم، وتأكد أن القرص الذي عليه ال Journal يتسع لها، وإلا بدأت الرسائل الأقدم فيه تحذف قبل أن تكتب.

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

وفيما يلي ما تحتاج أن تعرفه بعد التثبيت:

- **الذاكرة Memory:** بعد الإدخال كان Graylog قرابة 1.7 GiB، وOpenSearch قرابة 2.0 GiB، وMongoDB قرابة 187 MiB، بحد ذاكرة Java يساوي 1 GB لكل من الأولين، لذلك احسب للنظام 6 GB على الأقل لسيرفر صغير، أي قرابة أربعة أضعاف ما احتاجه Loki وAlloy وGrafana معاً في الدليل السابق.
- **ال Journal:** راقب حجمه وعدد الرسائل غير المكتوبة فيه من النقطة `/api/system/journal` كما فعلنا، فارتفاعه المستمر يعني أن المعالجة أو OpenSearch أبطأ من الاستقبال.
- **الاحتفاظ Retention:** يحدد لكل Index Set، والمجموعة الافتراضية في الإصدار 7.2 تحتفظ بالفهارس بين 30 و40 يوماً ثم تحذفها، وتستطيع أن تجعل Stream الأخطاء في مجموعة بمدة أطول، وStream السجلات العادية في مجموعة بمدة أقصر.
- **النسخ الاحتياطي Backup:** MongoDB هو ما لا يمكن إعادة بنائه، لأنه يحمل الإعدادات كلها، فانسخه ومعه السر `GRAYLOG_PASSWORD_SECRET`، أما الرسائل فبلقطات OpenSearch Snapshots.
- **الصلاحيات Permissions:** أنشئ حسابات شخصية بأدوار Roles، وأعط كل فريق صلاحية Streams خدمته فقط، ولا تستخدم حساب admin يومياً.
- **التحديث Upgrade:** اقرأ جدول التوافق قبل تحديث Graylog أو OpenSearch أو MongoDB، وحدث واحداً في كل مرة.

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

- **البحث بكلمة داخل حقل إضافي:** فلا يجد شيئاً لأن الحقول الإضافية `keyword`، لذلك ضع النص الذي تبحث فيه بالكلمات في `message`.
- **توقع أن يجد البحث العربي الكلمة بكل صيغها:** والمحلل الافتراضي لا يعرف «ال» ولا الهمزات.
- **إنشاء Stream وانتظار رسائل قديمة فيه:** وهو يطبق على الرسائل الجديدة فقط.
- **تحديث OpenSearch إلى آخر إصدار:** قبل أن يظهر في جدول التوافق.
- **الهاش بدون سر لمعرفات قليلة:** فيمكن عكسه بتجربة كل المعرفات.
- **فقد `GRAYLOG_PASSWORD_SECRET`:** فلا يستطيع Graylog قراءة القيم المشفرة بعد الاستعادة.
- **فتح منافذ المدخلات للإنترنت:** فيرسل أي أحد سجلات مزورة، لذلك اجعلها داخل الشبكة أو بشهادات TLS.

## تدريب عملي

- أضف مدخل Syslog UDP وأرسل إليه سجلات السيرفر، ثم أنشئ Stream لها في Index Set بمدة احتفاظ مختلفة.
- اكتب قاعدة Pipeline تسقط رسائل `/healthz` الناجحة بالدالة `drop_message()` قبل أن تخزن، ثم قس الفرق في حجم الفهرس.
- عدل قاعدة الهاش لتضيف سراً قبل الهاش، وتأكد أن الناتج لم يعد يطابق `sha256sum` للمعرف وحده.

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

```bash
docker rm -f obs2-graylog obs2-mongo obs2-os obs2-vector obs2-curl
docker volume rm obs2-graylog-data obs2-mongo-data obs2-os-data
docker network rm obs2-net
docker rmi graylog/graylog:7.2.1 mongo:8.0.34 opensearchproject/opensearch:3.7.0
```

## الخلاصة

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

- Graylog خادم يجمع المدخلات وال Journal وال Streams وال Pipelines والتنبيهات فوق OpenSearch، ويحفظ إعداداته في MongoDB، والإصدارات المسموحة يحددها جدول التوافق وليس آخر إصدار منشور.
- رخصته SSPL منذ 2020، والإصدار المفتوح بدون رسوم للمستخدمين أو حد للإدخال، والإصدارات المدفوعة تضيف بحيرة البيانات وقدرات SIEM.
- فهرس كل حقل يجعل البحث فورياً ولكنه يكلف: الفهرس كان بحجم الملف الأصلي، والذاكرة قرابة 4 GB للخادم والمخزن معاً.
- الحقول الإضافية تبحث بقيمتها الكاملة فقط، والبحث العربي في `message` لا يعرف «ال» دون محلل عربي.
- ال Streams توجه الرسائل الجديدة، وال Pipelines تعدل وتخفي بدل ال Extractors، وتعريفات الأحداث تنبه، وال Journal حفظ 500 رسالة كاملة أثناء توقف OpenSearch.

[![](https://arabroot.io/content/images/2026/10/loki-cover-cover.webp)دليلجمع السجلات Logs مركزياً مع Grafana Lokiكيف تشغل Grafana Loki مع Alloy وGrafana، وتختار التسميات Labels التي لا توقف الإدخال، ثم تبحث وتنبه وتحذف بلغة LogQL.](https://arabroot.io/articles/%D8%AC%D9%85%D8%B9-%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logs-%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A7-%D9%85%D8%B9-grafana-loki/)[![](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/elk-stack-cover-cover.webp)دليلمجموعة ELK: Elasticsearch وLogstash وKibanaكيف تجمع سجلات سيرفراتك في Elasticsearch وتبحث فيها من Kibana بأمان، وما الذي تعنيه رخصته لك اليوم.](https://arabroot.io/articles/%D9%85%D8%AC%D9%85%D9%88%D8%B9%D8%A9-elk-elasticsearch-%D9%88-logstash-%D9%88-kibana/)[![](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/)

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

- أكتوبر 2026: كتابة الدليل وتنفيذ المختبر بإصدارات Graylog 7.2.1 وOpenSearch 3.7.0 وMongoDB 8.0.34 وVector 0.60.0.