DevOps والتشغيل الأدلة التقنية

السجلات المركزية Logs مع Graylog وOpenSearch

كيف تثبت Graylog فوق OpenSearch، وتوجه الرسائل بال Streams وتخفي بياناتها بال Pipelines وتنبه منها، وما الذي لا يجده بحثه العربي.

السجلات المركزية Logs مع Graylog وOpenSearch

عندما يطلب فريق التشغيل بحثاً في السجلات وتنبيهات وصلاحيات لكل فريق من واجهة واحدة، دون أن يبني ذلك بنفسه من Kibana أو Grafana، فغالباً سوف يصل إلى Graylog. فهو يستقبل السجلات بمدخلات Inputs جاهزة مثل GELF وSyslog، ويوجهها إلى Streams، ويعدلها بال Pipelines، ويطلق التنبيهات، ويخزن الرسائل في OpenSearch. وقد شرحنا في دليل 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، وهو OpenSearch تغلفه Graylog وتتولى إعداده وشهاداته وانضمام عقده Nodes من واجهتها.

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

مخطط من اليمين: تطبيقات وأجهزة ترسل إلى Graylog Server، وفيه خمس محطات مرقمة: المدخلات GELF وSyslog وBeats وRaw، ثم ال Journal على القرص يحفظ الرسالة قبل المعالجة، ثم ال Streams توجه بقواعد على الحقول، ثم ال Pipelines مربوطة بال Streams، ثم OpenSearch يحفظ الرسائل في Index Sets، ومعه MongoDB للإعدادات فقط، وتحت المحطات تعريفات الأحداث Event Definitions تبحث في ال Streams على فترات وتطلق تنبيهاً ترسله الإشعارات
الرسالة لا تظهر في البحث حتى تمر بالمحطات كلها

وأهم ما في الصورة ال Journal، فهو ملف على قرص الخادم تكتب فيه كل رسالة فور وصولها قبل أي معالجة، فإذا أبطأ OpenSearch أو توقف تتراكم الرسائل في ال Journal ولا تضيع، وهذا يشبه الذاكرة المؤقتة على القرص في الناقل، ولكنه داخل Graylog نفسه، وسوف نجربه في نهاية المختبر.

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

ملف Docker Compose الرسمي في مستودع Graylog يبدأ مع ال Data Node، وأول تشغيل له يمر بواجهة إعداد أولى Preflight في المتصفح تصدر منها الشهادات وتضيف العقد، وهذا مناسب إذا لم يكن عندك OpenSearch وتريد أن تتولى Graylog إدارته. أما في هذا الدليل فاخترنا OpenSearch تديره أنت، لأن كثيراً من الفرق عندها OpenSearch أصلاً كما في دليل OpenSearch، ولأن الإعداد كله يكون في متغيرات البيئة فتعيده بأمر واحد.

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

المكونأقل إصدارأعلى إصدار
MongoDB7.x8.2.x
OpenSearch1.1.x3.7.x

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

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

غيرت Graylog رخصة الخادم من GPL إلى SSPL في ملف الرخصة في 16 نوفمبر 2020 مع الإصدار 4.0، وهي الرخصة نفسها التي استخدمتها MongoDB ثم Elasticsearch، ولا تعدها مبادرة المصدر المفتوح OSI رخصة مفتوحة، وتفاصيل ذلك في قصة رخصة Elasticsearch. وعملياً لا تؤثر عليك إذا شغلت Graylog لسجلات جهتك، أما إذا قدمته خدمة لعملاء خارجيين فاقرأ شروطها. وتقول صفحة Graylog Open (اطلعنا عليها في أكتوبر 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 للتجارب الصغيرة:

#!/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'

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

{"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.
  • أنشأنا المدخلات من الواجهة البرمجية بالترويسة X-Requested-By التي يشترطها Graylog لكل طلب يغير شيئاً، والطريقة نفسها من الواجهة في System ثم Inputs، كما في الصورة التالية:
صفحة Inputs في Graylog 7.2 وفيها مدخلان يعملان: GELF HTTP على المنفذ 12202 وGELF TCP على المنفذ 12201 لكل عقد Graylog، مع أزرار Stop input وMore
المدخلان اللذان أنشأهما الملف up.sh

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

صيغة GELF هي صيغة Graylog الأصلية، وهي JSON فيه حقول ثابتة مثل host وshort_message وtimestamp، وكل حقل إضافي يبدأ بالشرطة السفلية مثل _service. والبيانات هي نفسها التي أدخلناها في دليل Loki: 200 ألف حدث JSON لمتجر فيه ثماني خدمات، من الملف gen.py في ملفات المختبر، وحجمها 61 MB. ونرسلها بأداة Vector، والتحويل التالي يضيف الشرطة السفلية لكل حقل:

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" }
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 بحثنا عن رسالة واحدة بمعرف الطلب:

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
{"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 هو وقت الحدث من السطر نفسه، وليس وقت وصوله، لذلك يظهر في البحث في يومه الصحيح.

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

docker exec obs2-curl curl -s "http://obs2-os:9200/_cat/indices/graylog_*?v&h=index,docs.count,store.size,pri.store.size"
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
1527 رسالة، وهو العدد الصحيح الذي حسبناه من الملف الأصلي

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

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"
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:

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'
202 202 202
message:المهلة -> "total_results":1
message:مهلة -> "total_results":1
message:"انتهاء المهلة" -> "total_results":1
message:الدفع -> "total_results":2

البحث بالكلمة يعمل الآن، ولكن لاحظ النتيجتين الأوليين: المهلة وجدت الرسالة الأولى فقط، ومهلة وجدت الثالثة فقط، رغم أن الكلمة واحدة، والسبب أن المحلل الافتراضي Standard Analyzer يقسم النص بالمسافات ولا يعرف أن «ال» ليست جزءاً من الكلمة. والحل أن تعرف للفهرس محللاً عربياً بقالب فهرس Index Template خاص، كما شرحنا في المحلل العربي في ELK، أو أن يبحث المستخدم بالصيغتين، أو أن يكتب التطبيق رسائل السجل بالإنجليزية ويبقي العربية لما يعرض للمستخدم، وهذا الأخير هو الأسهل في التشغيل.

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

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

{"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 ويشغله:

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"
'
{"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 يصفها اليوم بأنها «ميزة قديمة Legacy»، ويوصي بال Pipelines بدلاً منها لأنها تدعم الشروط وتعمل على رسائل عدة مدخلات معاً. وال Pipeline سلسلة مراحل Stages من القواعد Rules تعمل على رسائل ال Stream الذي تربطها به، والقاعدة التالية تستبدل user_id بالهاش SHA-256 له:

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 في ملفات المختبر، وآخر سطر في مخرجها هو الربط:

pipeline "privacy"
stage 0 match either
  rule "hash user id";
end
{"stream_id":"000000000000000000000001","pipeline_ids":["6adab9a3146a5dafb633cb17"],"id":"6adab9a3146a5dafb633cb1a"}
صفحة Pipeline privacy في Graylog وفيها أن ال Pipeline يعالج رسائل Default Stream، ومرحلة واحدة Stage 0 فيها القاعدة hash user id بدون أخطاء
ال Pipeline مربوط بال Stream الافتراضي، وفيه مرحلة واحدة بقاعدة واحدة

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

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
"user_id":"969fd298d98211ed0f024d08caa5a36c54e6d1fea53536b6d19c723f585449fb"
"total_results":8
969fd298d98211ed0f024d08caa5a36c54e6d1fea53536b6d19c723f585449fb *-

في ال Stream الرسائل الثماني فقط، فرسالة النجاح ورسالة shipping لم تطابقا القاعدتين، وقيمة user_id في المخزن هي الهاش وليست u1082. ولكن لاحظ ما يكشفه السطر الأخير: الهاش هو نفسه الذي يحسبه أي أحد، ومن يعرف أن القيمة SHA-256 لمعرف قصير يستطيع أن يحسب الهاش لكل المعرفات المحتملة، وهي هنا خمسة آلاف فقط، ويجد صاحبه في ثوان، لذلك فهذا إخفاء جزئي Pseudonymisation وليس إخفاء كاملاً للهوية Anonymisation، والأفضل أن تضيف إلى القيمة سراً قبل الهاش، أو ألا يرسل التطبيق المعرف أصلاً كما شرحنا في إخفاء البيانات في الطريق.

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

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

{"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:

{"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
التنبيه في صفحة Alerts، ولم نربطه بإشعار فبقي في الواجهة فقط

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

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

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

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
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 يحمل إعداده الافتراضي:

"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 للمعرف وحده.

حذف المختبر

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.
دليلجمع السجلات Logs مركزياً مع Grafana Lokiكيف تشغل Grafana Loki مع Alloy وGrafana، وتختار التسميات Labels التي لا توقف الإدخال، ثم تبحث وتنبه وتحذف بلغة LogQL.دليلOpenSearch: البديل المفتوح لـ Elasticsearch وقصة الترخيصلماذا ولد OpenSearch من Elasticsearch، وكيف تشغله على سيرفرك بصلاحيات دقيقة وتنبيهات حقيقية على سجلاتك.دليلمجموعة ELK: Elasticsearch وLogstash وKibanaكيف تجمع سجلات سيرفراتك في Elasticsearch وتبحث فيها من Kibana بأمان، وما الذي تعنيه رخصته لك اليوم.دليلمسار السجل Log Pipeline من التطبيق إلى البحث والتنبيهكيف ينتقل السجل من التطبيق إلى المخزن عبر الناقل Shipper، وكيف تخفي البيانات الشخصية وتوجه الأخطاء وتحمي السجلات إذا توقفت الوجهة.

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

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

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.