في دليل مجموعة ELK جمعنا سجلات nginx في Elasticsearch وبحثنا فيها من Kibana، وذكرنا أن رخصة Elasticsearch تغيرت في 2021 فخرجت من تعريف المصدر المفتوح، وأن AWS فرعت آخر إصدار برخصة Apache 2.0 فولد OpenSearch. وفي هذا الدليل نأخذ نفس السجلات بالضبط ونضعها في OpenSearch، ونرى ما الذي بقي كما هو، وما الذي اختلف بعد خمس سنوات من التفرع، ولماذا قد يكون هو الخيار الأنسب لمؤسستك أو لا يكون.
وسوف نناقش في هذا المقال ما يلي:
- قصة التفرع Fork من AWS إلى OpenSearch Software Foundation في Linux Foundation، ورخصة Apache 2.0.
- مكونات OpenSearch: السيرفر وOpenSearch Dashboards والإضافات Plugins مثل Security وAlerting وk-NN.
- التوافق مع Elasticsearch اليوم، وما الذي لا ينتقل بينهما.
- تجربة على نفس سجلات nginx: Fluent Bit يرسل، ومسار إدخال Ingest Pipeline يحلل، والإضافة Security تعطي كل حساب ما يحتاجه فقط، وتنبيه حقيقي يصل إلى Webhook.
- الأخطاء الشائعة، وأهمها كلمات المرور الافتراضية في إعداد التجربة Demo Configuration.
قصة التفرع: من AWS إلى Linux Foundation
في 21 يناير 2021، بعد أسبوع من إعلان Elastic تغيير الرخصة، كتبت AWS أنها سوف تفرع Elasticsearch وKibana من آخر إصدار برخصة Apache وهو 7.10، ثم في 12 أبريل 2021 أعلنت مشروع OpenSearch: سيرفر OpenSearch مشتق من Elasticsearch 7.10.2، وواجهة OpenSearch Dashboards مشتقة من Kibana 7.10.2، ومعهما الإضافات التي كانت في توزيعة Open Distro for Elasticsearch مثل الصلاحيات والتنبيهات وإدارة دورة حياة ال Indices، وكل ذلك برخصة Apache 2.0. وصدر الإصدار 1.0 في 12 يوليو 2021.
وكان السؤال الذي يطرحه الجميع: هل هو مشروع مجتمعي أم مشروع AWS باسم آخر؟ والإجابة جاءت في 16 سبتمبر 2024 حين أعلنت Linux Foundation إنشاء OpenSearch Software Foundation ونقلت AWS المشروع إليها، فأصبحت العلامة والقرار في مؤسسة محايدة لها مجلس إدارة من عدة شركات، وهو نفس الطريق الذي سلكه Valkey وOpenTofu كما في مقال مشاريع Linux Foundation. والصورة التالية تبين الفرعين:

والرخصة اليوم كما هي منذ البداية، فملف LICENSE.txt في المستودع هو Apache License 2.0، والإضافات الرسمية كذلك، ولا توجد طبقة مدفوعة داخل البرنامج نفسه، والسبب في أهمية ذلك أن الرخصة المتساهلة Permissive تسمح لك بتقديمه خدمة لعملائك أو تضمينه في منتجك دون شروط، وقد شرحنا أنواع الرخص في مقال الرخص.
مكونات OpenSearch
- OpenSearch: السيرفر نفسه، مبني على Apache Lucene مثل Elasticsearch، بنفس مفاهيم ال Index وال Mapping وال Shards التي شرحناها في دليل ELK، ونفس شكل ال REST API في أغلب العمليات اليومية.
- OpenSearch Dashboards: واجهة الويب، ومنها Discover واللوحات، وصفحات الإضافات مثل التنبيهات وإدارة الصلاحيات.
- Security: إضافة الصلاحيات، وفيها المستخدمون والأدوار Roles، وصلاحيات على مستوى ال Index والمستند Document-Level Security والحقل Field-Level Security، وتسجيل التدقيق Audit Logs، والربط مع LDAP وSAML وOpenID Connect.
- Alerting وNotifications: Monitors تشغل استعلاماً على جدول زمني وتطلق تنبيهاً عند شرط، وقنوات إشعار مثل Webhook والبريد وSlack.
- Index State Management: واختصاراً ISM، وهو مقابل ILM في Elasticsearch: حالات وانتقالات بين Hot وDelete وغيرها.
- k-NN والبحث بالمتجهات: Vector Search بفهارس HNSW عبر Faiss أو Lucene، ومعها البحث الهجين Hybrid Search، وهي التي نستخدمها في مقال البحث الدلالي.
- Data Prepper: بديل Logstash من نفس المشروع، وهو خيار إذا احتجت معالجة كبيرة قبل الفهرسة، أما في التجربة فاكتفينا بـ Fluent Bit ومسار إدخال داخل السيرفر.
وعند تشغيل الأمر _cat/plugins على Docker Image الرسمية بالإصدار 3.9.0 سوف تجد أنها تأتي بكل الإضافات جاهزة:
opensearch-alerting opensearch-knn opensearch-observability
opensearch-anomaly-detection opensearch-ltr opensearch-performance-analyzer
opensearch-asynchronous-search opensearch-ml opensearch-reports-scheduler
opensearch-cross-cluster-replication opensearch-neural-search opensearch-search-relevance
opensearch-custom-codecs opensearch-notifications opensearch-security
opensearch-flow-framework opensearch-notifications-core opensearch-security-analytics
opensearch-geospatial opensearch-index-management opensearch-skills
opensearch-job-scheduler opensearch-sql opensearch-system-templates
opensearch-ubi query-insightsالتوافق مع Elasticsearch اليوم
قد يتساءل البعض: هل أستطيع نقل تطبيقي من Elasticsearch إلى OpenSearch بتغيير العنوان فقط؟ والإجابة أن ذلك كان صحيحاً تقريباً في 2021، ثم ابتعد المشروعان مع كل إصدار، والتفصيل كما يلي:
- ال API الأساسية: البحث والفهرسة وال Mapping وأغلب استعلامات Query DSL والتجميعات بنفس الشكل، وقد استخدمنا في التجربة نفس استعلامات دليل ELK ونفس المحلل
arabicدون تغيير. - مكتبات العملاء Clients: مكتبات Elasticsearch الرسمية منذ الإصدار 7.14 تتحقق من أن السيرفر Elasticsearch فعلاً، فمكتبة Python مثلاً ترفع الخطأ UnsupportedProductError إذا لم يكن كذلك، لذلك استخدم مكتبات OpenSearch لكل لغة.
- أدوات الجمع: Logstash يحتاج مخرج OpenSearch الخاص، وFluent Bit فيه مخرج opensearch مدمج، وهذا ما استخدمناه.
- ما لا ينتقل: لا تعتمد على استعادة لقطات Snapshots من Elasticsearch 8 أو 9 في OpenSearch، ولا اللوحات المحفوظة من Kibana الحديثة، ولا الميزات الخاصة بكل طرف مثل ES|QL في Elasticsearch أو PPL في OpenSearch، والطريق الآمن للانتقال هو إعادة الفهرسة من المصدر.
متى تختار OpenSearch ومتى تختار Elasticsearch؟
نقاط القوة: رخصة Apache 2.0 للكل دون طبقة مدفوعة، فالصلاحيات على مستوى الحقل والتنبيهات والدخول الموحد SAML وOpenID Connect كلها موجودة في نفس ال Docker Image، ومؤسسة محايدة تملك العلامة، ومزودو سحابة متعددون يقدمونه خدمة مدارة إذا احتجت ذلك لاحقاً.
نقاط الضعف: أغلب الأدلة والإجابات على الإنترنت مكتوبة لـ Elasticsearch، وبعضها لا ينطبق، وتكاملات Elastic Agent وFleet الجاهزة لعشرات الأنظمة لا توجد بنفس الشكل، وواجهة Dashboards أبطأ تطوراً من Kibana في بعض المجالات مثل لوحات المراقبة الجاهزة.
والقاعدة العملية: إذا كنت تبني منتجاً تبيعه وفيه محرك البحث، أو كانت سياسة مؤسستك تشترط رخصة معتمدة من OSI في التوزيعة نفسها، أو تحتاج الصلاحيات على مستوى الحقل دون اشتراك، فاختر OpenSearch. وإذا كان فريقك يعرف Elastic جيداً ويعتمد على Elastic Agent وتكاملاته، فـ Elasticsearch بالتوزيعة الافتراضية مناسب للاستخدام الداخلي.
التجربة: نفس سجلات nginx في OpenSearch
نفس nginx ونفس مولد الطلبات من دليل ELK، ولكن بدل Filebeat وLogstash نستخدم Fluent Bit 5.0.11 يرسل السطر كما هو، ويتولى مسار إدخال Ingest Pipeline داخل OpenSearch تحليله. والإصدارات: OpenSearch وOpenSearch Dashboards بالإصدار 3.9.0، وكل المنافذ على 127.0.0.1، وكل ال Containers تبدأ بـ lab-os-. ولا تشغل هذه التجربة وتجربة ELK في نفس الوقت على جهاز صغير، لأن كل واحد منهما يحجز ذاكرة Java كاملة. والصورة التالية تبين ما سوف نبنيه:

ملف Docker Compose
# OpenSearch lab: the same nginx traffic as the ELK lab, shipped by Fluent Bit,
# parsed by an OpenSearch ingest pipeline, protected by the security plugin.
# Every port is bound to 127.0.0.1 only. Run: docker compose -p lab-os up -d
name: lab-os
services:
node:
image: opensearchproject/opensearch:3.9.0
container_name: lab-os-node
environment:
- cluster.name=lab-os
- node.name=lab-os-node
- discovery.type=single-node
- bootstrap.memory_lock=true
- OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m
- OPENSEARCH_INITIAL_ADMIN_PASSWORD=${ADMIN_PASSWORD}
ulimits:
memlock: { soft: -1, hard: -1 }
nofile: { soft: 65536, hard: 65536 }
mem_limit: 1536m
volumes:
- os-data:/usr/share/opensearch/data
ports:
- "127.0.0.1:19201:9200"
dashboards:
image: opensearchproject/opensearch-dashboards:3.9.0
container_name: lab-os-dashboards
depends_on: [node]
environment:
- OPENSEARCH_HOSTS=["https://lab-os-node:9200"]
mem_limit: 1g
ports:
- "127.0.0.1:15602:5601"
nginx:
image: nginx:1.31.6-alpine
container_name: lab-os-nginx
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- nginx-logs:/var/log/lab
fluentbit:
image: fluent/fluent-bit:5.0.11
container_name: lab-os-fluentbit
depends_on: [node, nginx]
environment:
- SHIPPER_PASSWORD=${SHIPPER_PASSWORD}
volumes:
- ./fluent-bit/fluent-bit.yaml:/fluent-bit/etc/fluent-bit.yaml:ro
- nginx-logs:/var/log/lab:ro
command: ["/fluent-bit/bin/fluent-bit", "-c", "/fluent-bit/etc/fluent-bit.yaml"]
# receives the alert webhook, so we can see what OpenSearch sends
hook:
image: python:3.13.16-slim
container_name: lab-os-hook
volumes:
- ./hook/hook.py:/hook.py:ro
command: ["python", "-u", "/hook.py"]
volumes:
os-data:
nginx-logs:في الإعداد أعلاه لاحظ التالي:
- الذاكرة
-Xms512m -Xmx512mهي القيمة التي يستخدمها مثال Docker Compose في توثيق التثبيت بـ Docker، وهي تكفي لتجربة ولا تكفي للإنتاج. - منذ OpenSearch 2.12 لا يبدأ إعداد التجربة إلا بكلمة مرور قوية للمستخدم
adminفي المتغيرOPENSEARCH_INITIAL_ADMIN_PASSWORD، ولا توجد كلمة مرور افتراضية للمدير. - الإضافة Security مفعلة مع شهادات تجريبية ذاتية التوقيع Self-signed، لذلك يعمل المنفذ بـ HTTPS ونستخدم
curl -kفي التجربة فقط، وفي الإنتاج ضع شهاداتك كما في دليل الشهادات. - ال Container باسم
lab-os-hookبرنامج Python من عشرة أسطر يطبع كل طلب POST يصله، حتى نرى التنبيه بأعيننا.
إعداد Fluent Bit:
service:
flush: 2
log_level: info
pipeline:
inputs:
- name: tail
path: /var/log/lab/access.log
tag: nginx.access
read_from_head: true
filters:
- name: modify
match: "*"
add: service.name shop
outputs:
- name: opensearch
match: "*"
host: lab-os-node
port: 9200
tls: on
tls.verify: off # lab only: the demo certificate is self-signed
http_user: log_shipper
http_passwd: ${SHIPPER_PASSWORD}
index: logs-nginx-lab
write_operation: create # data streams accept "create" only
pipeline: nginx-access # parsing happens inside OpenSearch
suppress_type_name: on
replace_dots: off
retry_limit: 5لاحظ write_operation: create، لأن ال Data Stream لا يقبل إلا إضافة مستندات جديدة، وpipeline: nginx-access الذي يطلب من OpenSearch تمرير كل مستند على مسار الإدخال قبل الفهرسة.
مسار الإدخال وISM والقالب والأدوار
والملف setup.sh كاملاً، وهو يقابل setup.sh وnginx.conf في دليل ELK معاً:
#!/usr/bin/env bash
# Prepares OpenSearch: an ingest pipeline that parses nginx lines, an ISM policy, a data stream template,
# and two roles: one that can only write logs and one that reads them without the client IP.
set -euo pipefail
source .env
OS=https://127.0.0.1:19201
os() { curl -sk -u "admin:$ADMIN_PASSWORD" -H 'Content-Type: application/json' "$@"; echo; }
# 1) parse the combined log line inside OpenSearch (instead of Logstash)
os -X PUT "$OS/_ingest/pipeline/nginx-access" -d '{
"description": "nginx combined log to structured fields",
"processors": [
{ "grok": { "field": "log", "patterns": ["%{COMBINEDAPACHELOG}"] } },
{ "date": { "field": "timestamp", "formats": ["dd/MMM/yyyy:HH:mm:ss Z"], "target_field": "@timestamp" } },
{ "rename": { "field": "clientip", "target_field": "source.address" } },
{ "rename": { "field": "verb", "target_field": "http.request.method" } },
{ "rename": { "field": "request", "target_field": "url.original" } },
{ "convert":{ "field": "response", "type": "integer", "target_field": "http.response.status_code" } },
{ "rename": { "field": "log", "target_field": "message" } },
{ "grok": { "if": "ctx.url?.original != null && ctx.url.original.startsWith(\"/search?\")",
"field": "url.original", "patterns": ["^%{URIPATH:url.path}\\?%{GREEDYDATA:url.query}"] } },
{ "kv": { "if": "ctx.url?.query != null", "field": "url.query", "field_split": "&", "value_split": "=",
"target_field": "_tmp", "include_keys": ["q"] } },
{ "gsub": { "if": "ctx._tmp?.q != null", "field": "_tmp.q", "pattern": "\\+", "replacement": " " } },
{ "urldecode": { "if": "ctx._tmp?.q != null", "field": "_tmp.q", "target_field": "search_query" } },
{ "user_agent": { "field": "agent", "target_field": "user_agent", "ignore_missing": true } },
{ "remove": { "field": ["_tmp", "timestamp", "response", "ident", "auth", "httpversion", "agent", "referrer", "bytes"],
"ignore_missing": true } }
],
"on_failure": [ { "set": { "field": "error.message", "value": "{{ _ingest.on_failure_message }}" } } ]
}'
# 2) lifecycle with ISM: roll over daily or at 10 GB, delete after 30 days
os -X PUT "$OS/_plugins/_ism/policies/nginx-logs" -d '{
"policy": {
"description": "nginx logs: 30 days",
"default_state": "hot",
"states": [
{ "name": "hot", "actions": [ { "rollover": { "min_index_age": "1d", "min_primary_shard_size": "10gb" } } ],
"transitions": [ { "state_name": "delete", "conditions": { "min_index_age": "30d" } } ] },
{ "name": "delete", "actions": [ { "delete": {} } ], "transitions": [] }
],
"ism_template": [ { "index_patterns": [".ds-logs-nginx-*"], "priority": 100 } ]
}
}'
# 3) the data stream template, with the Arabic field
os -X PUT "$OS/_index_template/logs-nginx" -d '{
"index_patterns": ["logs-nginx-*"],
"data_stream": {},
"priority": 200,
"template": {
"settings": { "number_of_shards": 1, "number_of_replicas": 0 },
"mappings": {
"dynamic": false,
"properties": {
"@timestamp": { "type": "date" },
"message": { "type": "text" },
"tags": { "type": "keyword" },
"error": { "properties": { "message": { "type": "text" } } },
"service": { "properties": { "name": { "type": "keyword" } } },
"source": { "properties": { "address": { "type": "ip" } } },
"http": { "properties": {
"request": { "properties": { "method": { "type": "keyword" } } },
"response": { "properties": { "status_code": { "type": "short" } } } } },
"url": { "properties": { "original": { "type": "keyword" }, "path": { "type": "keyword" } } },
"user_agent": { "properties": { "name": { "type": "keyword" }, "os": { "properties": { "name": { "type": "keyword" } } } } },
"search_query": { "type": "text", "analyzer": "arabic", "fields": { "raw": { "type": "keyword" } } }
}
}
}
}'
# 4) security: a shipper that can only write, and a developer role that reads without the client IP
os -X PUT "$OS/_plugins/_security/api/roles/log_writer" -d '{
"cluster_permissions": ["cluster_monitor", "indices:data/write/bulk"],
"index_permissions": [{ "index_patterns": ["logs-nginx-*"],
"allowed_actions": ["create_index", "indices:admin/mapping/put", "indices:data/write/index", "indices:data/write/bulk*"] }]
}'
os -X PUT "$OS/_plugins/_security/api/internalusers/log_shipper" -d "{\"password\":\"$SHIPPER_PASSWORD\"}"
os -X PUT "$OS/_plugins/_security/api/rolesmapping/log_writer" -d '{"users":["log_shipper"]}'
os -X PUT "$OS/_plugins/_security/api/roles/shop_developer" -d '{
"cluster_permissions": ["cluster_composite_ops_ro"],
"index_permissions": [{ "index_patterns": ["logs-nginx-*", ".ds-logs-nginx-*"],
"allowed_actions": ["read"],
"fls": ["~source.address", "~message"] }]
}'
os -X PUT "$OS/_plugins/_security/api/internalusers/developer1" -d "{\"password\":\"$READER_PASSWORD\"}"
os -X PUT "$OS/_plugins/_security/api/rolesmapping/shop_developer" -d '{"users":["developer1"]}'والمخرج سوف يكون كما يلي:
{"acknowledged":true}
{"_id":"nginx-logs","_version":1,"_primary_term":1,"_seq_no":0,"policy":{"policy":{"policy_id":"nginx-logs", ...
{"acknowledged":true}
{"status":"CREATED","message":"'log_writer' created."}
{"status":"CREATED","message":"'log_shipper' created."}
{"status":"CREATED","message":"'log_writer' created."}
{"status":"CREATED","message":"'shop_developer' created."}
{"status":"CREATED","message":"'developer1' created."}
{"status":"CREATED","message":"'shop_developer' created."}في الإعداد أعلاه لاحظ التالي:
- مسار الإدخال Ingest Pipeline فيه نفس خطوات Logstash:
grokثمdateثمkvوurldecodeلكلمات البحث، ثمuser_agent، والفرق أن التحليل يأخذ من معالج OpenSearch وليس من سيرفر منفصل، فإذا كانت السجلات كثيرة جداً فضع العقد Nodes الخاصة بالإدخال Ingest Nodes منفصلة. - الكتلة
on_failureتضع رسالة الخطأ في الحقلerror.messageبدل أن يرفض المستند، وقد عرفنا الحقل في القالب حتى نستطيع البحث عنه، وهذا الدرس الذي تعلمناه في دليل ELK. - سياسة ISM فيها
ism_templateفتطبق تلقائياً على كل Backing Index جديد يبدأ بـ.ds-logs-nginx-. - الدور
shop_developerفيه"fls": ["~source.address", "~message"]، أي أن المطور يقرأ كل الحقول إلا عنوان الزائر والسطر الخام الذي يحتوي العنوان أيضاً، والسبب أن العنوان بيانات شخصية لا يحتاجها من يصلح خطأ 503.
كلمات المرور الافتراضية في إعداد التجربة
قبل أن نكمل، لننظر إلى المستخدمين الذين أنشأهم إعداد التجربة Demo Configuration تلقائياً، ثم نجرب الدخول بأسمائهم كلمات مرور:
curl -sk -u "admin:$ADMIN_PASSWORD" "https://127.0.0.1:19201/_plugins/_security/api/internalusers" | python -c "import sys,json; print(sorted(json.load(sys.stdin).keys()))"
curl -sk -o /dev/null -w "%{http_code}\n" -u kibanaserver:kibanaserver https://127.0.0.1:19201/
curl -sk -o /dev/null -w "%{http_code}\n" -u logstash:logstash https://127.0.0.1:19201/['admin', 'anomalyadmin', 'developer1', 'kibanaro', 'kibanaserver', 'log_shipper', 'logstash', 'readall', 'snapshotrestore']
200
200المستخدمان kibanaserver وlogstash دخلا بكلمة مرور تساوي الاسم، والسبب أن إعداد التجربة مصمم للاختبار، وكلمة المرور القوية الإلزامية تخص admin وحده. لذلك لا تستخدم إعداد التجربة في الإنتاج أبداً، وإنما ابدأ بـ DISABLE_INSTALL_DEMO_CONFIG=true وملف internal_users.yml خاص بك وشهاداتك، أو على الأقل غير كلمات مرور هؤلاء المستخدمين واحذف من لا تحتاجه منهم.
تشغيل الطلبات والبحث
docker compose -p lab-os up -d
docker run --rm --name lab-os-traffic --network lab-os_default \
-v "$PWD/traffic:/app:ro" python:3.13.16-slim python /app/traffic.py 240done: 1044 iterations in 240sوالمستند كما خزنه OpenSearch بعد مسار الإدخال:
{
"service.name" : "shop",
"source" : { "address" : "192.0.2.24" },
"message" : "192.0.2.24 - - [11/Oct/2026:11:29:58 +0000] \"GET /search?q=%D9%83%D8%AA%D8%A8+%D8%A7%D9%84%D8%A3%D8%B7%D9%81%D8%A7%D9%84 HTTP/1.1\" 200 8 ...",
"search_query" : "كتب الأطفال",
"url" : { "path" : "/search", "original" : "/search?q=%D9%83...", "query" : "q=%D9%83..." },
"@timestamp" : "2026-10-11T11:29:58.000Z",
"http" : { "request" : { "method" : "GET" }, "response" : { "status_code" : 200 } },
"user_agent" : { "os" : { "name" : "Android", "version" : "16" }, "name" : "Chrome Mobile", "device" : { "name" : "Pixel 9" } }
}ونفس استعلامات دليل ELK تعمل كما هي، فهذا توزيع رموز الحالة، ثم البحث بالمحلل العربي عن «مستشفى»:
{"aggregations":{"s":{"buckets":[{"key":200,"doc_count":909},{"key":503,"doc_count":166},{"key":404,"doc_count":75},{"key":401,"doc_count":56}]}}}{
"hits" : { "total" : { "value" : 57 } },
"aggregations" : { "q" : { "buckets" : [
{ "key" : "المستشفى التخصصي", "doc_count" : 29 },
{ "key" : "مستشفيات الرياض", "doc_count" : 28 }
] } }
}نفس 57 طلباً تقريباً بنفس التوزيع، لأن مولد الطلبات يستخدم بذرة عشوائية Seed ثابتة، والفرق البسيط في الأعداد من اختلاف التوقيت بين التشغيلين. ولم يفشل أي سطر في مسار الإدخال، فعدد المستندات التي فيها error.message صفر. وهذه شاشة Discover في OpenSearch Dashboards على نفس البيانات، ويظهر فيها الارتفاع وقت تعطل /api/orders:

هل يرى كل حساب ما يحتاجه فقط؟
نسأل بحساب المطور developer1 عن طلب دخول فاشل، ثم نحاول التجميع على عنوان IP، ثم الحذف، ثم نسأل بحساب Fluent Bit:
curl -sk -u "developer1:$READER_PASSWORD" -H 'Content-Type: application/json' \
"https://127.0.0.1:19201/logs-nginx-lab/_search?pretty&filter_path=hits.hits._source" \
-d '{"size":1,"query":{"term":{"http.response.status_code":401}}}'"_source" : {
"service.name" : "shop",
"source" : { },
"url" : { "original" : "/login" },
"@timestamp" : "2026-10-11T11:29:58.000Z",
"http" : { "request" : { "method" : "POST" }, "response" : { "status_code" : 401 } },
"user_agent" : { "os" : { "name" : "Linux" }, "name" : "Firefox", "device" : { "name" : "Other" }, "version" : "144.0." }
}# developer1: terms aggregation on source.address
{"aggregations":{"ip":{"doc_count_error_upper_bound":0,"sum_other_doc_count":0,"buckets":[]}}}
# developer1: DELETE _data_stream/logs-nginx-lab
no permissions for [indices:admin/data_stream/delete] and User [name=developer1, backend_roles=[], requestedTenant=null]
# log_shipper: _search
no permissions for [indices:data/read/search] and User [name=log_shipper, backend_roles=[], requestedTenant=null]في المخرج أعلاه لاحظ التالي: المستند وصل للمطور دون source.address ودون message، والتجميع على العنوان رجع فارغاً فلا يستطيع استنتاج العناوين بطريق آخر، والحذف مرفوض، وحساب Fluent Bit لا يقرأ شيئاً. والصورة التالية تضع المستندين بجانب بعضهما:

تنبيه حقيقي: ارتفاع أخطاء 5xx
الآن الشيء الذي لا تعطيك إياه اللوحة وحدها: أن يصلك خبر عندما يتعطل شيء وأنت لا تنظر. الملف alert.sh ينشئ قناة Webhook ثم Monitor يعمل كل دقيقة ويعد أخطاء 5xx في الدقيقة الأخيرة:
#!/usr/bin/env bash
# An alert: if the shop returns more than 20 server errors (5xx) within one minute, call the webhook.
set -euo pipefail
source .env
OS=https://127.0.0.1:19201
os() { curl -sk -u "admin:$ADMIN_PASSWORD" -H 'Content-Type: application/json' "$@"; echo; }
# 1) where the alert goes: a webhook channel in the Notifications plugin
os -X POST "$OS/_plugins/_notifications/configs" -d '{
"config_id": "ops-webhook",
"config": { "name": "ops webhook", "description": "lab receiver", "config_type": "webhook",
"is_enabled": true, "webhook": { "url": "http://lab-os-hook:8080/alert" } }
}'
# 2) the monitor: runs every minute, counts 5xx in the last minute, triggers above 20
os -X POST "$OS/_plugins/_alerting/monitors" -d '{
"type": "monitor",
"monitor_type": "query_level_monitor",
"name": "shop 5xx spike",
"enabled": true,
"schedule": { "period": { "interval": 1, "unit": "MINUTES" } },
"inputs": [ { "search": {
"indices": ["logs-nginx-*"],
"query": { "size": 0, "query": { "bool": { "filter": [
{ "range": { "http.response.status_code": { "gte": 500 } } },
{ "range": { "@timestamp": { "gte": "{{period_end}}||-1m", "lte": "{{period_end}}", "format": "epoch_millis" } } }
] } } }
} } ],
"triggers": [ { "query_level_trigger": {
"name": "more than 20 server errors",
"severity": "1",
"condition": { "script": { "source": "ctx.results[0].hits.total.value > 20", "lang": "painless" } },
"actions": [ { "name": "notify ops",
"destination_id": "ops-webhook",
"message_template": { "source": "{{ctx.monitor.name}}: {{ctx.results.0.hits.total.value}} errors between {{ctx.periodStart}} and {{ctx.periodEnd}}" },
"throttle_enabled": true, "throttle": { "value": 10, "unit": "MINUTES" } } ]
} } ]
}'وأثناء التعطل الذي يصنعه مولد الطلبات في منتصف المدة وصل هذا إلى lab-os-hook:
docker logs lab-os-hookALERT RECEIVED: shop 5xx spike: 103 errors between 2026-10-11T11:27:44.418Z and 2026-10-11T11:28:44.418Z
192.168.48.3 - - [11/Oct/2026 11:28:45] "POST /alert HTTP/1.1" 200 -و_plugins/_alerting/monitors/alerts يبين التنبيه وأن التكرار التالي كتم بسبب throttle، لأننا طلبنا ألا يتكرر الإشعار أكثر من مرة في عشر دقائق:
ACTIVE more than 20 server errors 1791718125471 [{'action_id': '5_y2KqEBv8MSRh1SLFtg', 'last_execution_time': 1791718125363, 'throttled_count': 1}]
والرقم 20 هنا مختار للتجربة، أما في الإنتاج فابدأ من متوسط أخطائك الحقيقي في الأسبوع الماضي، واجعل الحد أعلى منه بوضوح، وإلا تعود الفريق على تجاهل التنبيهات كما شرحنا في قسم إرهاق التنبيهات.
التشغيل اليومي
- الذاكرة: توثيق التثبيت يوصي بأن يكون ال Heap نصف ذاكرة السيرفر، والباقي لذاكرة الملفات، والقيمة الافتراضية دون تحديد هي 1 جيجابايت، وإذا استخدمت k-NN فالفهارس الشعاعية تحجز ذاكرة إضافية خارج ال Heap.
- النسخ الاحتياطي: Snapshots إلى مستودع مشترك أو تخزين S3، مع سياسة لقطات من إضافة Index Management، واختبر الاستعادة كما في قاعدة 3-2-1.
- الترقية: بين الإصدارات الكبيرة اقرأ ملاحظات الإصدار، فـ OpenSearch 3 يقرأ Indices أنشئت في 2.x فقط بحسب
minimum_index_compatibility_versionالذي ظهر في أول طلب، وما أنشئ في 1.x يحتاج إعادة فهرسة قبل الترقية. - الدخول الموحد: اربط Dashboards بـ OpenID Connect من مزود هويتك مثل authentik، واجعل الأدوار تأتي من مجموعات المزود Backend Roles بدل إنشاء مستخدمين داخليين لكل شخص.
أخطاء شائعة
- تشغيل إعداد التجربة في الإنتاج: رأينا أن
kibanaserver:kibanaserverيدخل، وأن الشهادات ذاتية التوقيع، وهذا يكفي ليصل أي شخص في الشبكة إلى السجلات. - تعطيل الإضافة Security لتسهيل البداية: المتغير
DISABLE_SECURITY_PLUGIN=trueيترك المنفذ 9200 مفتوحاً بلا حساب، ولا مكان له خارج تجربة على127.0.0.1. - استخدام مكتبة Elasticsearch مع OpenSearch: تعمل الإصدارات القديمة منها ثم تتوقف عند أول ترقية، فاستخدم مكتبة OpenSearch من البداية.
- نسيان
write_operation: createمع Data Stream: ال Data Stream يرفض عمليةindexالعادية، فتظهر أخطاء في سجل Fluent Bit ولا يصل شيء. - إخفاء الحقل ونسيان نسخته الأخرى: إذا أخفيت
source.addressوتركتmessageفالعنوان ما زال في السطر الخام، لذلك أخفينا الاثنين. - تنبيه بلا
throttle: Monitor كل دقيقة دون كتم يرسل ستين رسالة في ساعة التعطل الواحدة.
تدريب عملي
- أضف إلى الدور
shop_developerقاعدة Document-Level Security تجعله يرى طلبات/api/فقط، ثم تحقق بنفس الاستعلام. - غير شرط التنبيه ليستخدم نسبة الأخطاء من كل الطلبات بدل العدد، ثم شغل مولد الطلبات مرة ثانية وقارن.
حذف المختبر
docker compose -p lab-os down -v
rm .env
# optional
docker rmi opensearchproject/opensearch:3.9.0 opensearchproject/opensearch-dashboards:3.9.0 fluent/fluent-bit:5.0.11الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- OpenSearch تفرع من Elasticsearch 7.10.2 في 2021، ويدار اليوم في OpenSearch Software Foundation التابعة لـ Linux Foundation، وكل شيء فيه برخصة Apache 2.0.
- ال API الأساسية متقاربة، فنفس الاستعلامات والمحلل العربي عملت دون تغيير، ولكن مكتبات العملاء واللقطات واللوحات لا تنتقل بين المشروعين.
- الإضافة Security تعطي كل حساب أقل صلاحية، وتخفي الحقول الحساسة من البحث والتجميع، وكلمات مرور إعداد التجربة الافتراضية لا مكان لها في الإنتاج.
- Alerting مع Notifications يحول البحث إلى تنبيه يصلك، ومعه
throttleحتى لا تغرق في الرسائل. - اختره إذا كانت الرخصة مهمة لك أو تحتاج الصلاحيات المتقدمة دون اشتراك، واختر Elasticsearch إذا كان فريقك يعتمد على Elastic Agent وتكاملاته.
دليلمجموعة ELK: Elasticsearch وLogstash وKibanaكيف تجمع سجلات سيرفراتك في Elasticsearch وتبحث فيها من Kibana بأمان، وما الذي تعنيه رخصته لك اليوم.
دليلالبحث الدلالي Semantic Search بالـ Embeddingsمتى يتفوق البحث بالمعنى على البحث بالكلمات في النص العربي، وكيف تقيس ذلك على أسئلتك قبل أن تختار.
دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.
دليلالبحث في النصوص العربية مع OpenSearch وMeilisearchلماذا يرجع البحث صفحة فارغة لمن يكتب «مستشفي» أو «اجازه»، وكيف تضبط محركك حتى يجد الكلمة بكل أشكالها.سجل التحديثات
- أكتوبر 2026: كتابة الدليل، وتجربة على OpenSearch وOpenSearch Dashboards 3.9.0 مع Fluent Bit 5.0.11 ونفس سجلات nginx في دليل ELK، والتحقق من قصة التفرع من إعلانات AWS وLinux Foundation وملف الرخصة في المستودع.