> ## 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.

# SigNoz: بديل Datadog مفتوح المصدر Open Source على سيرفرك
- URL: https://arabroot.io/articles/signoz-بديل-datadog-مفتوح-المصدر/
- Published: 2026-10-22T16:00:00.000Z
- Updated: 2026-10-23T08:01:07.000Z
- Description: كيف تشغل SigNoz وترسل إليه التتبع والسجلات والمقاييس عبر OpenTelemetry، وما الذي يقدمه فعلاً مقابل Datadog وبكم من الذاكرة.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps والتشغيل, DevOps وCI/CD, الاستضافة الذاتية, SigNoz

في [مفاهيم المراقبة Observability](https://arabroot.io/articles/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%A7%D9%84%D9%85%D8%B1%D8%A7%D9%82%D8%A8%D8%A9-observability-%D9%85%D9%86-%D8%A7%D9%84%D8%A7%D8%B4%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AA%D9%86%D8%A8%D9%8A%D9%87/) قسمنا ما يخرج من التطبيق إلى ثلاث إشارات Signals: السجلات Logs والمقاييس Metrics والتتبع Traces، ثم شغلنا في المقالات التالية أداة لكل جزء: Loki وGraylog وSeq للسجلات، وGlitchTip لتتبع الأخطاء Error Tracking. وهذا يعمل، ولكن عندما يبطئ طلب في الإنتاج سوف تجد نفسك تنتقل بين ثلاث واجهات وتنسخ رقم الطلب من واحدة إلى أخرى، وهذا بالضبط ما تبيعه Datadog: منصة واحدة ترى فيها الطلب وسجلاته ومقاييسه في شاشة واحدة، ولكن بفاتورة تكبر مع كل سيرفر وكل GB. و[SigNoz](https://signoz.io/?ref=arabroot.io) هو أشهر محاولة مفتوحة المصدر لتقديم التجربة نفسها على سيرفرك، فهو مبني من البداية على OpenTelemetry ويخزن الإشارات الثلاث في ClickHouse ويعرضها في واجهة واحدة.

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

- ما هو SigNoz ومكوناته، وما الذي يقدمه مقارنة بـ Datadog.
- الترخيص بالتفصيل: ما هو تحت MIT وما هو تحت ترخيص Enterprise، ولماذا يهمك أي Docker Image تشغل.
- التثبيت الرسمي الجديد عبر Foundry مع Docker Compose، وإعدادات يجب أن تغيرها من اليوم الأول.
- تطبيق Python صغير من خدمتين يرسل التتبع والسجلات والمقاييس عبر OpenTelemetry دون تعديل في الكود تقريباً.
- خريطة الخدمات Service Map، وكيف وجدنا نقطة بطيئة من التتبع وأصلحناها، وكيف وصلنا من طلب فاشل إلى سجله.
- البحث بالنص العربي، واللوحات Dashboards، وتنبيه ينطلق فعلاً إلى Webhook ثم يعود Resolved بعد الإصلاح.
- الاحتفاظ Retention، وما قسناه من الذاكرة والقرص، وماذا يحدث إذا توقف ClickHouse، والنسخ الاحتياطي والاستعادة.
- حدود SigNoz أمام Datadog بصراحة.

## ما هو SigNoz وكيف يعمل؟

SigNoz منصة مراقبة APM واحدة تستقبل الإشارات الثلاث بصيغة OTLP، أي البروتوكول الذي يعرفه OpenTelemetry، وبالتالي لا يوجد Agent خاص بالمنصة تثبته على السيرفرات كما في Datadog، وإنما تستخدم مكتبات OpenTelemetry الرسمية في كل لغة، وإذا قررت لاحقاً أن تنتقل إلى أداة أخرى فإن الكود نفسه يبقى كما هو وتغير عنوان الإرسال فقط. والشكل التالي يبين المكونات التي تعمل على سيرفرك ومسار البيانات بينها:

![على اليمين تطبيقان orders-api وpayments-api مع OpenTelemetry SDK وسيرفرات مع OTel Collector، ترسل Traces وLogs وMetrics بصيغة OTLP على المنفذين 4317 و4318 إلى المستقبل Ingester وهو signoz-otel-collector، ثم إلى ClickHouse الذي يخزن قواعد signoz_traces وsignoz_logs وsignoz_metrics مع ClickHouse Keeper للتنسيق، ثم على اليسار SigNoz نفسه بالواجهة والـ API والتنبيهات على المنفذ 8080 مع PostgreSQL للمستخدمين واللوحات والتنبيهات، والإشعارات إلى Webhook وSlack وEmail](https://arabroot.io/content/images/2026/10/signoz-01-architecture.webp)

خمس Containers تعمل دائماً، وأثقلها ClickHouse

والجدول التالي يلخص دور كل مكون، والأرقام في العمود الأخير من مختبرنا تحت حمل خفيف كما سيأتي في [قسم الموارد](#resources):

| المكون                           | الدور                                                                     | الذاكرة RAM تحت الحمل |
| -------------------------------- | ------------------------------------------------------------------------- | --------------------- |
| signoz/signoz                    | الواجهة UI والـ API، ومحرك التنبيهات Ruler وAlertmanager                  | 53 إلى 70 MiB         |
| signoz-otel-collector (Ingester) | يستقبل OTLP ويكتب الدفعات Batches في ClickHouse                           | 92 إلى 105 MiB        |
| clickhouse-server                | قاعدة بيانات عمودية Columnar تخزن كل الإشارات                             | 1734 إلى 1924 MiB     |
| clickhouse-keeper                | التنسيق Coordination الذي تحتاجه جداول ClickHouse الموزعة                 | 118 إلى 126 MiB       |
| postgres                         | قاعدة البيانات الوصفية Metastore: المستخدمون واللوحات والتنبيهات والقنوات | 39 MiB                |

ويضاف إليها Container يعمل مرة واحدة ثم يخرج، وهو `telemetrystore-migrator` الذي ينشئ جداول ClickHouse ويحدثها عند كل ترقية.

### نقاط القوة ونقاط الضعف

نقاط القوة:

- **الإشارات الثلاث في مكان واحد:** من التتبع Trace تنتقل إلى سجلات الطلب نفسه بضغطة، لأن الربط يتم بـ `trace_id` الذي يضعه الـ SDK في كل سجل، وهذا ما رأيناه في [الطلب الفاشل](#correlated-logs).
- **OpenTelemetry من الأصل:** لا يوجد Agent خاص ولا مكتبة خاصة، وبالتالي لا تقيد كود تطبيقك بمنصة واحدة.
- **ClickHouse في الخلف:** ضغط Compression مرتفع واستعلامات تجميع Aggregation سريعة على حجم كبير، ورأينا في المختبر أن 86 ألف Span شغلت على القرص أقل من 6 MiB.
- **التنبيهات واللوحات مدمجة:** تنبيه على عدد سجلات أو على زمن الاستجابة Latency من التتبع، ويصل إلى Webhook أو Slack أو Email دون أداة إضافية.

نقاط الضعف:

- **ClickHouse ثقيل على السيرفرات الصغيرة:** قرابة 2 GB من الذاكرة تحت حمل خفيف، والمنصة كلها تحتاج 4 GB على الأقل لـ Docker بحسب [دليل التثبيت](https://signoz.io/docs/install/docker/?ref=arabroot.io).
- **الترخيص ليس MIT بالكامل:** الـ Docker Image الافتراضي يحتوي كوداً تحت ترخيص Enterprise كما سيأتي في [قسم الترخيص](#licence).
- **لا توجد مصادقة Authentication على الاستقبال Ingestion في النسخة المجتمعية:** تطبيقنا أرسل دون أي مفتاح، ومفاتيح الاستقبال Ingestion Keys من ميزات السحابة في [صفحة الأسعار](https://signoz.io/pricing/?ref=arabroot.io)، لذلك لا تعرض المنفذين 4317 و4318 للإنترنت.
- **البحث العربي حرفي:** يطابق الحروف كما هي دون تطبيع Normalization للهمزات والتاء المربوطة.

## الترخيص License: ما هو مفتوح المصدر Open Source وما هو تجاري؟

يقول ملف [LICENSE](https://github.com/SigNoz/signoz/blob/v0.147.0/LICENSE?ref=arabroot.io) في المستودع إن كل ما هو خارج المجلدين `ee/` و`cmd/enterprise/` تحت ترخيص MIT، أما ما بداخلهما فتحت [ترخيص SigNoz Enterprise](https://github.com/SigNoz/signoz/blob/v0.147.0/ee/LICENSE?ref=arabroot.io)، وهذا الترخيص يسمح بنسخ الكود وتعديله للتطوير والاختبار، ولكنه يشترط اتفاقية واشتراكاً صالحاً للاستخدام في الإنتاج Production. وقد يتساءل البعض: وما علاقة هذا بي إذا كنت لن أستخدم أي ميزة مدفوعة؟ والإجابة أن SigNoz يبني من المستودع نفسه Docker Image مختلفين:

| الـ Docker Image        | يبنى من                          | ما رأيناه في المختبر                                                                |
| ----------------------- | -------------------------------- | ----------------------------------------------------------------------------------- |
| signoz/signoz           | cmd/enterprise، أي يتضمن كود ee/ | الافتراضي في Foundry، وسجل التشغيل يقول "variant":"enterprise"، ويعمل مع PostgreSQL |
| signoz/signoz-community | cmd/community، أي كود MIT فقط    | سجل التشغيل يقول "variant":"community"، ويعمل مع SQLite فقط                         |

ولاحظ النقطة الأخيرة لأنها ظهرت لنا بالتجربة: عندما شغلنا `signoz-community` فوق التثبيت الافتراضي الذي يستخدم PostgreSQL توقف فوراً بالخطأ التالي:

```text
"exception.message":"factory \"postgres\" not found or not registered"
```

والسبب أن دعم PostgreSQL قاعدةً وصفية Metastore موجود في المجلد `ee/sqlstore/postgressqlstore`، أي في الجزء غير المفتوح. وعندما غيرنا القاعدة الوصفية إلى SQLite عمل `signoz-community` وقرأ بيانات ClickHouse نفسها وعرض الخدمات كما هي. لذلك إذا كانت سياسة مؤسستك تشترط ترخيصاً مفتوحاً بالكامل فاستخدم `signoz-community` مع `metastore.kind: sqlite`، وإذا أردت PostgreSQL أو ميزات Enterprise فاقرأ ترخيص `ee/` وتواصل مع SigNoz قبل الإنتاج، لأن تفسير الترخيص قرار قانوني وليس تقنياً.

أما الميزات نفسها فصفحة [الأسعار](https://signoz.io/pricing/?ref=arabroot.io) (راجعناها في 23 أكتوبر 2026) تقسمها كما يلي:

| الخطة                                 | السعر المعلن List Price                                                                                                                             | ما يهمك منها                                                                                                                   |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Community (استضافة ذاتية Self-Hosted) | مجاني                                                                                                                                               | التتبع والسجلات والمقاييس واللوحات والتنبيهات، ودعم عبر Slack المجتمعي                                                         |
| Teams (سحابة SigNoz Cloud)            | يبدأ من 49 دولاراً شهرياً تشمل استخداماً بقيمة 49 دولاراً، ثم 0.30 دولار لكل GB من السجلات أو التتبع و0.10 دولار لكل مليون عينة Samples من المقاييس | SAML، وتنبيهات اكتشاف الشذوذ Anomaly Detection، ومفاتيح استقبال متعددة، واحتفاظ افتراضي 15 يوماً للسجلات والتتبع وشهر للمقاييس |
| Enterprise                            | يبدأ من 4000 دولار شهرياً (عقد مخصص)                                                                                                                | سحابة مخصصة Dedicated أو داخل حسابك السحابي BYOC أو استضافة ذاتية مع دعم، وصلاحيات دقيقة Fine-grained RBAC (تجريبية Beta)      |

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

## المتطلبات

- **سيرفر Linux** مع Docker Engine 20.10 أو أحدث وإضافة Docker Compose v2\. والتوثيق يطلب على Windows تشغيل Docker Engine داخل WSL 2 بدل Docker Desktop، لأن ClickHouse Keeper قد يتوقف بالرمز 139 تحت Docker Desktop.
- **الذاكرة RAM:** 4 GB على الأقل مخصصة لـ Docker بحسب التوثيق، و8 GB أريح لأن ClickHouse وحده تجاوز 1.8 GB في مختبرنا.
- **القرص:** قرابة 3.5 GB للـ Docker Images، ثم حجم البيانات بحسب ما ترسل ومدة الاحتفاظ.
- **المنافذ:** 8080 للواجهة، و4317 (gRPC) و4318 (HTTP) لاستقبال OTLP. اجعلها كلها على شبكة خاصة، واعرض الواجهة فقط عبر Reverse Proxy.
- **اتصال بالإنترنت عند أول تشغيل:** Container اسمه `telemetrystore-clickhouse-user-scripts` ينزل ملفاً تنفيذياً صغيراً من GitHub لدالة `histogramQuantile`، فإذا كان السيرفر معزولاً فجهز هذا الملف مسبقاً.

## التثبيت Installation مع Foundry

إذا قرأت دليلاً قديماً عن SigNoz فسوف تجده يطلب استنساخ المستودع وتشغيل `install.sh` أو ملفات Compose من المجلد `deploy/`، وهذه الطريقة متوقفة رسمياً منذ الإصدار v0.130.0 كما يقول [دليل الترحيل Migration](https://github.com/SigNoz/signoz/blob/v0.147.0/deploy/MIGRATION.md?ref=arabroot.io)، والطريقة المدعومة اليوم هي أداة [Foundry](https://github.com/SigNoz/foundry?ref=arabroot.io): تكتب ملفاً واحداً اسمه `casting.yaml` يصف ما تريد، والأداة `foundryctl` تولد منه ملفات Compose وإعدادات ClickHouse والمستقبل في المجلد `pours/`. والفكرة جيدة لأن الترقية بعدها تصبح تغيير رقم الإصدار وإعادة التوليد، بدل دمج ملف Compose طويل يدوياً.

أولاً نثبت `foundryctl`. والتوثيق يستخدم سكربتاً من الموقع، والأوضح أن تنزل الإصدار المحدد من صفحة [الإصدارات](https://github.com/SigNoz/foundry/releases/tag/v0.3.0?ref=arabroot.io) وتتحقق من الـ Checksum:

```bash
mkdir -p ~/signoz && cd ~/signoz
curl -fsSLO https://github.com/SigNoz/foundry/releases/download/v0.3.0/foundry_linux_amd64.tar.gz
curl -fsSL https://github.com/SigNoz/foundry/releases/download/v0.3.0/foundry_0.3.0_checksums.txt | grep linux_amd64 | sha256sum -c
tar xzf foundry_linux_amd64.tar.gz
sudo install foundry_linux_amd64/bin/foundryctl /usr/local/bin/
```

ثم ملف `casting.yaml`. والملف الأدنى في التوثيق أربعة أسطر، ولكن Foundry يولد عندها `latest` لكل Docker Image، ويفتح المنافذ على كل الواجهات، ويترك إرسال الإحصاءات مفعلاً، لذلك نكتب الملف التالي:

```yaml
apiVersion: v1alpha1
kind: Installation
metadata:
  name: signoz
spec:
  deployment:
    flavor: compose
    mode: docker
  signoz:
    spec:
      image: signoz/signoz:v0.147.0
      env:
        SIGNOZ_STATSREPORTER_ENABLED: "false"
        SIGNOZ_GLOBAL_EXTERNAL__URL: "https://signoz.example.com"
        SIGNOZ_ALERTMANAGER_SIGNOZ_EXTERNAL__URL: "https://signoz.example.com"
  ingester:
    spec:
      image: signoz/signoz-otel-collector:v0.144.14
  metastore:
    kind: postgres
    spec:
      image: postgres:16.15
  patches:
    - target: "deployment/compose.yaml"
      operations:
        - op: replace
          path: /services/signoz-signoz-0/ports
          value: ["127.0.0.1:8080:8080"]
        - op: replace
          path: /services/ingester/ports
          value: ["10.0.0.5:4317:4317", "10.0.0.5:4318:4318"]
```

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

- **الإصدارات مثبتة Pinned:** `signoz/signoz:v0.147.0` هو آخر إصدار عند كتابة الدليل (صدر في 22 أكتوبر 2026)، و`signoz-otel-collector:v0.144.14` هو آخر إصدار للمستقبل. أما ClickHouse فيثبته Foundry نفسه على `25.12.5`.
- **`metadata.name`:** منه تبنى أسماء الـ Containers والـ Volumes، فالاسم `signoz` يعطي `signoz-signoz-0` و`signoz-telemetrystore-0-0-data` وهكذا. ولا تختر اسماً ينتهي بـ `-signoz` مثل `prod-signoz`، فقد رأينا في foundryctl v0.3.0 أنه يولد عندها عنوان OpAMP في ملف `ingester/opamp.yaml` يشير إلى Container قاعدة PostgreSQL بدل SigNoz، فيبقى المستقبل يكرر `connection refused`.
- **`SIGNOZ_STATSREPORTER_ENABLED`:** مفعل افتراضياً في [ملف الإعدادات](https://github.com/SigNoz/signoz/blob/v0.147.0/conf/example.yaml?ref=arabroot.io) ويرسل إحصاءات استخدام إلى SigNoz كل 6 ساعات، فأطفئه إذا كان هذا يخالف سياستك.
- **عنوانان خارجيان External URL:** الأول للواجهة كما في [التوثيق](https://signoz.io/docs/manage/administrator-guide/configuration/serving-on-external-url/?ref=arabroot.io)، والثاني تستخدمه التنبيهات في الروابط التي ترسلها. وبدونه وصلتنا كل إشعارات الـ Webhook بروابط `http://localhost:8080/alerts/...` لا تفتح عند أحد، وعندما أضفناه أصبحت الروابط بالعنوان الصحيح.
- **الـ `patches`:** عمليات JSON Patch على ملف Compose الناتج، وبها نربط الواجهة بـ `127.0.0.1` فقط خلف الـ Reverse Proxy، والاستقبال بعنوان الشبكة الخاصة (هنا `10.0.0.5` مثال). وهذه هي الطريقة الرسمية لتغيير ما لا يغطيه `casting.yaml`، ولا تعدل ملفات `pours/` بيدك لأن التوليد التالي يستبدلها.

الآن نولد الملفات ونراجعها ثم نشغل. والأمر `foundryctl cast` يفعل الخطوتين معاً، ولكننا نفصلهما لنرى ما سيعمل قبل تشغيله:

```bash
foundryctl gauge -f casting.yaml
foundryctl forge -f casting.yaml
grep -n "image:" pours/deployment/compose.yaml
cd pours/deployment && docker compose pull && docker compose up -d
```

والمخرج سوف يكون كما يلي (أسطر `image`):

```text
19:    image: signoz/signoz-otel-collector:v0.144.14
45:    image: postgres:16.15
72:    image: signoz/signoz:v0.147.0
97:    image: clickhouse/clickhouse-keeper:25.12.5
125:    image: clickhouse/clickhouse-server:25.12.5
148:    image: clickhouse/clickhouse-server:25.12.5
168:    image: signoz/signoz-otel-collector:v0.144.14
```

واستغرق تنزيل الـ Images قرابة دقيقة، ثم احتاج التشغيل الأول قرابة أربع دقائق حتى أصبح كل شيء جاهزاً، لأن الـ `migrator` ينشئ الجداول ثم ينتظر المستقبل حتى تكتمل كل الترحيلات Migrations قبل أن يبدأ الاستقبال. لذلك لا تقلق إذا رأيت في سجل المستقبل لدقيقتين رسائل `Error occurred while checking for sync migrations to complete, retrying`، وانتظر السطر التالي:

```bash
docker compose logs ingester | grep "Everything is ready"
curl -s http://127.0.0.1:8080/api/v1/health
curl -s http://127.0.0.1:8080/api/v1/version
```

```text
ingester-1  | {"level":"info","ts":"2026-10-23T06:45:38.354Z","caller":"service@v0.144.0/service.go:262","msg":"Everything is ready. Begin running and processing data." …}
{"status":"ok"}
{"version":"v0.147.0","ee":"Y","setupCompleted":false}
```

ثم افتح الواجهة وأنشئ حساب المدير الأول، فأول من يسجل يصبح المدير Admin، لذلك لا تترك الواجهة مفتوحة للإنترنت قبل هذه الخطوة.

## تطبيق صغير يرسل الإشارات Signals الثلاث

لكي نرى ما يقدمه SigNoz فعلاً نحتاج تطبيقاً فيه مشكلات حقيقية، لذلك كتبنا خدمتين صغيرتين بـ Flask: `orders-api` تستقبل الطلبات وتستدعي `payments-api` عبر HTTP. وفي الكود مشكلتان مقصودتان: بوابة الدفع تتأخر 2.5 ثانية في 30% من طلبات بطاقات `amex` بينما مهلة الطلب Timeout ثانيتان، وتقرير المبيعات اليومي يستدعي خدمة الدفع مرة لكل طلب، أي مشكلة N+1 المعروفة. والملف كاملاً كما يلي:

```python
# Two small services for the SigNoz lab: orders-api (calls payments-api) and payments-api.
# Telemetry comes from opentelemetry-instrument (zero-code) plus one custom counter.
import logging, os, random, sqlite3, time
import requests
from flask import Flask, jsonify, request
from opentelemetry import metrics

SERVICE = os.environ.get("ROLE", "orders")
PAYMENTS = os.environ.get("PAYMENTS_URL", "http://payments:8000")
log = logging.getLogger(SERVICE)
log.setLevel(logging.INFO)
app = Flask(__name__)
meter = metrics.get_meter("shop")
checkouts = meter.create_counter("shop.checkouts", description="checkout attempts by result")

DB = "/tmp/shop.db"
if SERVICE == "orders":
    con = sqlite3.connect(DB)
    con.execute("create table if not exists orders(id integer primary key, customer text, total real)")
    if not con.execute("select count(*) from orders").fetchone()[0]:
        con.executemany("insert into orders values(?,?,?)",
                        [(i, f"customer-{i % 40}", round(random.uniform(20, 900), 2)) for i in range(1, 501)])
    con.commit(); con.close()

def db():
    return sqlite3.connect(DB)

@app.get("/api/orders/<int:oid>")
def get_order(oid):
    with db() as c:
        row = c.execute("select id, customer, total from orders where id = ?", (oid,)).fetchone()
    if not row:
        return jsonify(error="not found"), 404
    return jsonify(id=row[0], customer=row[1], total=row[2])

@app.post("/api/checkout")
def checkout():
    body = request.get_json(force=True)
    oid, brand = body["order_id"], body["card_brand"]
    with db() as c:
        c.execute("select total from orders where id = ?", (oid,)).fetchone()
    try:
        r = requests.post(f"{PAYMENTS}/charge", json={"order_id": oid, "card_brand": brand}, timeout=2)
        r.raise_for_status()
    except requests.RequestException as e:
        checkouts.add(1, {"result": "failed", "card_brand": brand})
        log.error("فشل الدفع للطلب %s: انتهت مهلة بوابة الدفع", oid,
                  extra={"order_id": oid, "card_brand": brand, "error_code": "PSP_TIMEOUT"})
        return jsonify(error="payment failed"), 502
    checkouts.add(1, {"result": "ok", "card_brand": brand})
    log.info("تم الدفع للطلب %s", oid, extra={"order_id": oid, "card_brand": brand})
    return jsonify(status="paid", order_id=oid)

@app.get("/api/reports/daily")
def daily_report():
    # Deliberately slow: one HTTP call per order (N+1) instead of one batch call.
    with db() as c:
        ids = [r[0] for r in c.execute("select id from orders order by id limit 150")]
    if os.environ.get("REPORT_MODE") == "batch":
        # The fix: one call for all orders.
        paid = requests.post(f"{PAYMENTS}/status/batch", json={"ids": ids}, timeout=5).json()["paid"]
    else:
        paid = 0
        for oid in ids:
            paid += requests.get(f"{PAYMENTS}/status/{oid}", timeout=5).json()["paid"]
    log.info("تقرير المبيعات اليومي جاهز", extra={"orders": len(ids), "paid": paid})
    return jsonify(orders=len(ids), paid=paid)

@app.post("/charge")
def charge():
    body = request.get_json(force=True)
    if body["card_brand"] == "amex" and random.random() < 0.3:
        log.warning("تأخر رد مزود البطاقات", extra={"order_id": body["order_id"], "card_brand": "amex"})
        time.sleep(2.5)
    else:
        time.sleep(random.uniform(0.02, 0.06))
    return jsonify(paid=True)

@app.get("/status/<int:oid>")
def status(oid):
    time.sleep(0.004)
    return jsonify(order_id=oid, paid=oid % 3 != 0)

@app.post("/status/batch")
def status_batch():
    ids = request.get_json(force=True)["ids"]
    time.sleep(0.004)
    return jsonify(paid=sum(1 for oid in ids if oid % 3 != 0))

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, threaded=True)
```

لاحظ أن الكود لا يستورد أي مكتبة تتبع سوى `metrics` للعداد `shop.checkouts`، أما التتبع وربط السجلات فتضيفه أداة `opentelemetry-instrument` عند التشغيل، وهذا ما يسميه OpenTelemetry [الأتمتة بدون كود Zero-code Instrumentation](https://opentelemetry.io/docs/zero-code/python/?ref=arabroot.io). والـ Dockerfile يثبت المكتبات بإصدارات محددة:

```dockerfile
FROM python:3.13.16-slim
WORKDIR /app
RUN pip install --no-cache-dir \
      flask==3.1.3 requests==2.34.2 \
      opentelemetry-distro==0.66b1 \
      opentelemetry-exporter-otlp==1.45.1 \
      opentelemetry-instrumentation-flask==0.66b1 \
      opentelemetry-instrumentation-requests==0.66b1 \
      opentelemetry-instrumentation-sqlite3==0.66b1 \
      opentelemetry-instrumentation-logging==0.66b1
COPY app.py loadgen.py ./
CMD ["opentelemetry-instrument", "python", "app.py"]
```

والإعداد كله في متغيرات البيئة في ملف Compose:

```yaml
name: sg1-app
x-otel: &otel
  OTEL_EXPORTER_OTLP_ENDPOINT: http://sg1-ingester:4317
  OTEL_EXPORTER_OTLP_PROTOCOL: grpc
  OTEL_TRACES_EXPORTER: otlp
  OTEL_METRICS_EXPORTER: otlp
  OTEL_LOGS_EXPORTER: otlp
  OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED: "true"
  OTEL_METRIC_EXPORT_INTERVAL: "15000"
services:
  orders:
    build: .
    image: sg1-shop:1
    container_name: sg1-orders
    environment:
      <<: *otel
      ROLE: orders
      OTEL_SERVICE_NAME: orders-api
      OTEL_RESOURCE_ATTRIBUTES: deployment.environment=lab,service.version=1.0.0
      PAYMENTS_URL: http://payments:8000
      REPORT_MODE: ${REPORT_MODE:-loop}
  payments:
    image: sg1-shop:1
    container_name: sg1-payments
    depends_on: [orders]
    environment:
      <<: *otel
      ROLE: payments
      OTEL_SERVICE_NAME: payments-api
      OTEL_RESOURCE_ATTRIBUTES: deployment.environment=lab,service.version=1.0.0
  loadgen:
    image: sg1-shop:1
    container_name: sg1-loadgen
    profiles: [load]
    command: ["python", "loadgen.py"]
    environment:
      ORDERS_URL: http://orders:8000
      MINUTES: "20"
networks:
  default:
    name: sg1-network
    external: true
```

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

- `OTEL_SERVICE_NAME` هو الاسم الذي سوف تراه في قائمة الخدمات وخريطتها، فاختره ثابتاً وواضحاً.
- `OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED` يجعل سجلات وحدة `logging` العادية تخرج عبر OTLP ومعها `trace_id` و`span_id` للطلب الجاري، وبدونه تصل التتبعات وتبقى السجلات في stdout.
- الحقول في `extra` مثل `error_code` و`order_id` تصل خصائص Attributes مستقلة تبحث بها، وقد شرحنا لماذا يكتب السجل منظماً في [سجلات تساعدك فعلاً](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-%D8%AA%D8%B3%D8%A7%D8%B9%D8%AF%D9%83-%D9%81%D8%B9%D9%84%D8%A7-%D9%85%D9%86-%D8%AD%D8%AF%D8%AB-%D8%AE%D8%B7%D8%A7-%D8%A7%D9%84%D9%89-%D8%B3%D8%A8%D8%A8-%D8%A7%D9%84%D9%85%D8%B4%D9%83%D9%84%D8%A9/).
- العنوان `sg1-ingester:4317` والشبكة `sg1-network` هما اسم المستقبل وشبكة Docker عندما يكون `metadata.name` هو `sg1`، فإذا كان التطبيق على سيرفر آخر فالعنوان هو العنوان الخاص لسيرفر SigNoz على المنفذ 4317.
- الـ `loadgen` يرسل حملاً ثابتاً: قراءة طلب، ثم دفع ببطاقة عشوائية من ثلاث، ثم التقرير اليومي كل 15 ثانية.

## الخدمات Services وخريطة الخدمات Service Map

بعد دقيقة من بدء الحمل تظهر الخدمتان في صفحة Services مع زمن الاستجابة p99 ونسبة الأخطاء وعدد العمليات في الثانية، وكلها محسوبة من التتبع دون أن نكتب أي مقياس:

![صفحة Services في SigNoz وفيها payments-api بزمن p99 قدره 2501.94 ms ونسبة أخطاء 0 و15.24 عملية في الثانية، وorders-api بزمن p99 قدره 2045.55 ms ونسبة أخطاء 4.80 و5.57 عملية في الثانية](https://arabroot.io/content/images/2026/10/signoz-03-services.webp)

p99 لخدمة الدفع 2.5 ثانية، وهو التأخير المقصود في بطاقات amex

وخريطة الخدمات Service Map تبنى من العلاقة بين الـ Spans: كل استدعاء من خدمة إلى أخرى يصبح خطاً بينهما، وحجم الدائرة يمثل عدد الطلبات. وهي مفيدة عندما تكون لديك عشرون خدمة ولا يعرف أحد من يستدعي من:

![خريطة الخدمات في SigNoz وفيها دائرة orders-api صغيرة متصلة بخط بدائرة payments-api الأكبر](https://arabroot.io/content/images/2026/10/signoz-04-service-map.webp)

payments-api أكبر لأن التقرير اليومي يستدعيها 150 مرة في كل تشغيل

وعند فتح الخدمة `orders-api` ترى زمن الاستجابة p50 وp90 وp99 ومعدل الطلبات ومؤشر Apdex، وفي الأسفل جدول العمليات الرئيسية Key Operations مرتباً، ومنه تبدأ عادة:

![صفحة الخدمة orders-api وفيها رسم Latency بخطوط p50 وp90 وp99، ورسم Rate، ورسم Apdex، وجدول Key Operations فيه GET /api/reports/daily بزمن P50 قدره 2139.56 ms وP95 قدره 3633.56 ms، وPOST /api/checkout بنسبة أخطاء 9.71%](https://arabroot.io/content/images/2026/10/signoz-05-service-overview.webp)

التقرير اليومي في رأس القائمة: p50 أكثر من ثانيتين تحت الحمل

## نقطة Endpoint بطيئة: من التتبع Trace إلى السبب

في جدول العمليات كان `GET /api/reports/daily` هو الأبطأ، فقد كان الوسيط p50 عند 1627 ms قبل أن نضاعف الحمل، ثم وصل p95 إلى 4.76 ثانية عندما شغلنا أربعة مولدات حمل معاً. والمقياس هنا يخبرك أن التقرير بطيء ولكنه لا يخبرك لماذا، لذلك نفتح أحد طلباته في عارض التتبع:

![عارض التتبع في SigNoz لطلب GET /api/reports/daily مدته 1.71 ثانية وفيه 301 Span، والـ Flame Graph يظهر شريطين من عشرات المستطيلات الصغيرة المتتالية، والـ Waterfall يظهر GET من orders-api يليه GET /status/<int:oid> من payments-api مكرراً](https://arabroot.io/content/images/2026/10/signoz-06-trace-slow.webp)

301 Span في طلب واحد: استدعاء لكل طلب بالتسلسل

والتتبع يكفي للتشخيص: 150 استدعاء HTTP متتالياً، كل واحد قرابة 11 ms، ومجموعها 1.71 ثانية، والوقت لا يذهب في المعالجة وإنما في الذهاب والعودة. وهذا ما لا تراه في السجلات مهما كانت مفصلة، لأن كل استدعاء وحده سريع وناجح. والحل الأنسب هو استدعاء واحد لكل الطلبات، فأضفنا في خدمة الدفع `POST /status/batch` وغيرنا التقرير كما يلي:

```python
if os.environ.get("REPORT_MODE") == "batch":
    # The fix: one call for all orders.
    paid = requests.post(f"{PAYMENTS}/status/batch", json={"ids": ids}, timeout=5).json()["paid"]
```

وبعد إعادة التشغيل أصبح التتبع نفسه ثلاثة Spans فقط:

![عارض التتبع لطلب GET /api/reports/daily مدته 13 ms وفيه 3 Spans: GET /api/reports/daily ثم POST من orders-api ثم POST /status/batch من payments-api بمدة 5.94 ms](https://arabroot.io/content/images/2026/10/signoz-08-trace-fixed.webp)

من 1.71 ثانية إلى 13 ms بعد الإصلاح

وفي ClickHouse انخفض متوسط التقرير في الدقيقة التالية من 1752 ms إلى 14 ms، ثم بقي بين 12 و13 ms.

## من الطلب Request الفاشل إلى سجله Log

المشكلة الثانية أصعب: قرابة 10% من طلبات الدفع تفشل بـ 502\. ولو كانت السجلات في Loki والتتبع في أداة أخرى لاحتجت أن تبحث بالوقت وتخمن أي سطر يخص أي طلب. والشكل التالي يبين كيف يربط SigNoz بين الإشارات الثلاث لطلب واحد:

![في الأعلى تتبع trace_id يبدأ بـ 4b6a84ec فيه POST /api/checkout من orders-api بحالة 502 ومدة 2.01 ثانية، وتحته POST بمهلة ثانيتين وخطأ، وتحته POST /charge من payments-api بحالة 200 ومدة 2.50 ثانية مع ملاحظة أن الخدمة استمرت بعد أن تخلى عنها الطلب، ثم سهم trace_id إلى سجل بمستوى ERROR نصه فشل الدفع للطلب 429: انتهت مهلة بوابة الدفع وفيه trace_id وspan_id وerror_code = PSP_TIMEOUT وcard_brand = amex، وعلى اليسار مقياس shop.checkouts للحالات الفاشلة يتجاوز حد التنبيه](https://arabroot.io/content/images/2026/10/signoz-02-correlation.webp)

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

وفي الواجهة نفتح أحد الطلبات الفاشلة، ونضغط على الـ Span ثم التبويب Logs، فتظهر سجلات الطلب نفسه من الخدمتين معاً، دون أن نكتب أي استعلام:

![عارض التتبع لطلب POST /api/checkout بحالة 502، وعلى اليمين لوحة Span details فيها الخدمة orders-api والحالة Error، وفي التبويب Logs سطران: الأول بمستوى خطأ نصه فشل الدفع للطلب 429: انتهت مهلة بوابة الدفع، والثاني تحذير نصه تأخر رد مزود البطاقات](https://arabroot.io/content/images/2026/10/signoz-07-trace-logs.webp)

سجل الخطأ من orders-api وسجل التحذير من payments-api في المكان نفسه

ولاحظ في التتبع ما لا يظهر في أي سجل: `orders-api` تخلت عن الطلب بعد ثانيتين وردت على العميل بـ 502، ولكن `payments-api` استمرت 2.5 ثانية وأنهت العملية بنجاح 200\. أي أن العميل رأى خطأ بينما قد تكون بوابة الدفع خصمت المبلغ فعلاً. وهذا النوع من الأخطاء هو سبب وجود مفتاح عدم التكرار Idempotency Key في أنظمة الدفع، والتتبع هو ما جعله ظاهراً.

💡

الربط بين السجل والتتبع يعتمد على أن يكتب السجل أثناء الطلب، أي داخل الـ Span. فالسجلات التي تكتبها مهمة خلفية Background Job خارج أي طلب تصل بدون `trace_id`، وتراها في Logs Explorer ولكن لا تظهر في تبويب Logs لأي تتبع. وتفاصيل الربط في [توثيق SigNoz](https://signoz.io/docs/traces-management/guides/correlate-traces-and-logs/?ref=arabroot.io).

## النص العربي في السجلات Logs والبحث Search

كتبنا رسائل السجلات بالعربية عمداً، والسؤال: هل تصل كما هي، وهل نبحث بها؟ والإجابة نعم في الحالتين، مع حدود. فالنص يخزن ويعرض بحروف متصلة صحيحة، ولكن عمود Body يعرض السطر باتجاه من اليسار إلى اليمين LTR، لذلك يظهر الرقم وعلامة النقطتين في سطر مثل «فشل الدفع للطلب 261: انتهت مهلة بوابة الدفع» في غير مكانهما:

![Logs Explorer في SigNoz وفي خانة البحث body CONTAINS 'مهلة'، والنتائج سجلات بمستوى خطأ نصها فشل الدفع للطلب ورقم ثم انتهت مهلة بوابة الدفع، والرقم والنقطتان يظهران في وسط السطر بسبب اتجاه العرض من اليسار إلى اليمين](https://arabroot.io/content/images/2026/10/signoz-09-logs-arabic.webp)

البحث العربي يعمل، والعرض بالاتجاه LTR يغير موضع الأرقام

ولقياس البحث كتبنا سكربتاً يرسل عدة تعابير بحث إلى الـ API نفسها التي تستخدمها الواجهة ويعد النتائج:

```python
# Counts log records in SigNoz for several Arabic filter expressions (last 2 hours), through the v5 query API.
#   python search.py <token>
import json, os, sys, time, urllib.request
sys.stdout.reconfigure(encoding="utf-8")
TOKEN = sys.argv[1]
URL = os.environ.get("SIGNOZ_URL", "http://127.0.0.1:8080")
EXPRS = [
    "body CONTAINS 'مهلة'",
    "body CONTAINS 'انتهت مهلة'",
    "body CONTAINS 'انتهاء المهلة'",
    "body CONTAINS 'بوابه'",
    "body CONTAINS 'الدفع'",
    "body REGEXP 'بواب[ةه]'",
    "'مهلة'",
    "error_code = 'PSP_TIMEOUT'",
    "severity_text = 'ERROR'",
]
now = int(time.time() * 1000)
for expr in EXPRS:
    req = {"schemaVersion": "v1", "start": now - 2 * 3600 * 1000, "end": now, "requestType": "scalar",
           "compositeQuery": {"queries": [{"type": "builder_query", "spec": {
               "name": "A", "signal": "logs", "filter": {"expression": expr},
               "aggregations": [{"expression": "count()", "alias": "n"}]}}]}}
    r = urllib.request.Request(f"{URL}/api/v5/query_range", json.dumps(req).encode(),
                               {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}"})
    try:
        body = json.load(urllib.request.urlopen(r))
        data = body["data"]["data"]["results"][0]["data"]
        print(f"{data[0][0] if data else 0:>6}  {expr}")
    except urllib.error.HTTPError as e:
        print(f" error  {expr}  ->  {e.read().decode()[:200]}")
```

والمخرج سوف يكون كما يلي:

```text
   112  body CONTAINS 'مهلة'
   112  body CONTAINS 'انتهت مهلة'
     0  body CONTAINS 'انتهاء المهلة'
     0  body CONTAINS 'بوابه'
  1042  body CONTAINS 'الدفع'
   112  body REGEXP 'بواب[ةه]'
   112  'مهلة'
   112  error_code = 'PSP_TIMEOUT'
   112  severity_text = 'ERROR'
```

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

- البحث بجزء من النص يعمل، وكذلك البحث الحر بكتابة الكلمة وحدها `'مهلة'`.
- «انتهاء المهلة» لم تجد شيئاً رغم أن المعنى نفسه، و«بوابه» بالهاء لم تجد «بوابة» بالتاء المربوطة، لأن المطابقة حرفية دون تحليل لغوي Analyzer كما في OpenSearch.
- «الدفع» وجدت 1042 سجلاً لأنها تطابق سجلات النجاح «تم الدفع» مع سجلات الفشل، فالبحث بكلمة عامة يعطيك أكثر مما تريد.
- التعبير النمطي Regular Expression يحل مشكلة الهاء والتاء المربوطة، ولكن الحل الأنسب هو الخاصية الثابتة `error_code = 'PSP_TIMEOUT'`: نفس النتيجة، ولا يتأثر بصياغة الرسالة أو لغتها.

وقد قارنا البحث العربي في أدوات السجلات الأخرى على البيانات نفسها في [مقارنة أدوات السجلات بالأرقام](https://arabroot.io/articles/%D9%85%D9%82%D8%A7%D8%B1%D9%86%D8%A9-%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%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-%D8%A8%D8%A7%D9%84%D8%A7%D8%B1%D9%82%D8%A7%D9%85/).

## لوحة Dashboard من الكود

تستطيع بناء اللوحة من الواجهة بالضغط على New Panel، ولكن اللوحة التي تبنى بالضغط تضيع مع أول إعادة تثبيت، لذلك نكتبها ملفاً ونحفظه في Git. وصيغة اللوحات في هذا الإصدار مبنية على مواصفات Perses، وهي الصيغة نفسها لقوالب SigNoz الرسمية في [مستودع اللوحات](https://github.com/SigNoz/dashboards?ref=arabroot.io)، فأخذنا شكل لوحة `apm/http-api-monitoring.json` وبنينا منها لوحة بثلاثة رسوم:

```python
# Builds a small dashboard (Perses-style v2 JSON, the same shape as the official templates in SigNoz/dashboards)
# and creates it through the API:  python dashboard.py <token>
import json, os, sys, urllib.request
URL = os.environ.get("SIGNOZ_URL", "http://127.0.0.1:8080")

def panel(title, unit, signal, aggregation, filt, group, legend):
    return {"kind": "Panel", "spec": {
        "display": {"name": title, "description": ""},
        "plugin": {"kind": "signoz/TimeSeriesPanel", "spec": {
            "visualization": {"timePreference": "global_time", "fillSpans": False},
            "formatting": {"unit": unit, "decimalPrecision": "2"},
            "legend": {"position": "bottom", "mode": "list", "customColors": None}, "thresholds": None}},
        "queries": [{"kind": "time_series", "spec": {"name": "A", "plugin": {"kind": "signoz/BuilderQuery", "spec": {
            "name": "A", "stepInterval": 60, "signal": signal, "source": "", "aggregations": [aggregation],
            "disabled": False, "filter": {"expression": filt},
            "groupBy": [{"name": group, "signal": "", "fieldContext": "", "fieldDataType": "string"}],
            "order": [], "having": {"expression": ""}, "functions": [], "legend": legend}}}}],
        "links": []}}

panels = {
    "p95": panel("p95 latency per route (orders-api)", "ns", "traces", {"expression": "p95(duration_nano)"},
                 "service.name = 'orders-api' AND kind_string = 'Server'", "http.route", "{{http.route}}"),
    "checkouts": panel("Checkouts per minute by result", "none", "metrics",
                       {"metricName": "shop.checkouts", "timeAggregation": "increase", "spaceAggregation": "sum"},
                       "", "result", "{{result}}"),
    "logs": panel("Log records by severity", "none", "logs", {"expression": "count()"}, "", "severity_text", "{{severity_text}}"),
}
items = [{"x": 0, "y": 0, "width": 12, "height": 6, "content": {"$ref": "#/spec/panels/p95"}},
         {"x": 0, "y": 6, "width": 6, "height": 6, "content": {"$ref": "#/spec/panels/checkouts"}},
         {"x": 6, "y": 6, "width": 6, "height": 6, "content": {"$ref": "#/spec/panels/logs"}}]
dash = {"schemaVersion": "v6", "generateName": True, "tags": [], "spec": {
    "display": {"name": "Shop: checkout health", "description": "orders-api and payments-api"},
    "variables": [], "panels": panels, "layouts": [{"kind": "Grid", "spec": {"items": items}}],
    "duration": "30m", "refreshInterval": "0s", "links": []}}
req = urllib.request.Request(f"{URL}/api/v2/dashboards", json.dumps(dash).encode(),
                             {"Content-Type": "application/json", "Authorization": f"Bearer {sys.argv[1]}"})
print(urllib.request.urlopen(req).read().decode()[:300])
```

والسكربتات تقرأ العنوان من المتغير `SIGNOZ_URL`، وتحتاج توكن Token جلسة نحصل عليه بالسكربت التالي:

```bash
#!/usr/bin/env bash
# Prints a fresh SigNoz access token for the admin (sessions expire after 30 minutes).
#   SIGNOZ_URL defaults to http://127.0.0.1:8080
URL=${SIGNOZ_URL:-http://127.0.0.1:8080}
ORG=$(curl -s $URL/api/v2/sessions/context?email=admin@example.com | python -c "import sys,json;print(json.load(sys.stdin)['data']['orgs'][0]['id'])")
curl -s -X POST $URL/api/v2/sessions/email_password -H 'Content-Type: application/json' \
  -d "{\"email\":\"admin@example.com\",\"password\":\"Lab-SigNoz-2026!\",\"orgId\":\"$ORG\"}" \
  | python -c "import sys,json;print(json.load(sys.stdin)['data']['accessToken'])"
```

وبعد تشغيل `python dashboard.py "$(bash token.sh)"` ظهرت اللوحة كما يلي:

![لوحة Shop: checkout health في SigNoz فيها رسم p95 latency per route لثلاثة مسارات /api/reports/daily و/api/checkout و/api/orders، ورسم Checkouts per minute by result بخطين ok وfailed، ورسم Log records by severity بخطوط INFO وERROR وWARN](https://arabroot.io/content/images/2026/10/signoz-10-dashboard.webp)

زمن الاستجابة من التتبع، وعدد الدفعات من العداد، والسجلات حسب المستوى في لوحة واحدة

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

## تنبيه Alert ينطلق فعلاً إلى Webhook

التنبيه الذي لم تره ينطلق لا تثق به، لذلك شغلنا سيرفراً صغيراً يطبع كل ما يصله، ثم أنشأنا قناة إشعارات Notification Channel من نوع Webhook بحسب [التوثيق](https://signoz.io/docs/alerts-management/notification-channel/webhook/?ref=arabroot.io):

```bash
curl -s -X POST http://127.0.0.1:8080/api/v1/channels -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"lab-webhook","webhook_configs":[{"send_resolved":true,"url":"http://hook:8080/signoz"}]}'
```

ثم تنبيهين: الأول على السجلات [Log-based Alert](https://signoz.io/docs/alerts-management/log-based-alerts/?ref=arabroot.io) عندما تتجاوز أخطاء `PSP_TIMEOUT` ثلاثة في خمس دقائق، ولاحظ النص العربي في الملخص:

```json
{
  "alert": "Payment gateway timeouts",
  "alertType": "LOGS_BASED_ALERT",
  "description": "orders-api cannot reach the payment gateway in time",
  "annotations": {
    "summary": "مهلة بوابة الدفع: {{$value}} حالة خلال 5 دقائق",
    "description": "orders-api logged {{$value}} PSP_TIMEOUT errors in the last 5 minutes."
  },
  "labels": { "severity": "critical", "team": "payments" },
  "condition": {
    "compositeQuery": {
      "panelType": "graph",
      "queryType": "builder",
      "queries": [{
        "type": "builder_query",
        "spec": {
          "name": "A", "signal": "logs", "stepInterval": 60,
          "aggregations": [{ "expression": "count()" }],
          "filter": { "expression": "service.name = 'orders-api' AND error_code = 'PSP_TIMEOUT'" }
        }
      }]
    },
    "selectedQueryName": "A",
    "thresholds": {
      "kind": "basic",
      "spec": [{ "name": "critical", "op": "above", "target": 3, "matchType": "at_least_once", "channels": ["lab-webhook"] }]
    }
  },
  "evaluation": { "kind": "rolling", "spec": { "evalWindow": "5m", "frequency": "1m" } },
  "notificationSettings": { "renotify": { "enabled": false } },
  "ruleType": "threshold_rule",
  "schemaVersion": "v2alpha1",
  "version": "v5"
}
```

والثاني على التتبع [Trace-based Alert](https://signoz.io/docs/alerts-management/trace-based-alerts/?ref=arabroot.io) عندما يتجاوز p95 للتقرير ثانية واحدة. والمقياس هنا بالنانو ثانية، لذلك نحدد `"unit": "ns"` للاستعلام و`"targetUnit": "s"` للحد، ويحول SigNoz بينهما:

```json
{
  "alert": "Daily report is slow",
  "alertType": "TRACES_BASED_ALERT",
  "description": "p95 of GET /api/reports/daily above 1 second",
  "annotations": { "summary": "p95 of the daily report is {{$value}}" },
  "labels": { "severity": "warning" },
  "condition": {
    "compositeQuery": {
      "panelType": "graph",
      "queryType": "builder",
      "unit": "ns",
      "queries": [{
        "type": "builder_query",
        "spec": {
          "name": "A", "signal": "traces", "stepInterval": 60,
          "aggregations": [{ "expression": "p95(duration_nano)" }],
          "filter": { "expression": "service.name = 'orders-api' AND name = 'GET /api/reports/daily'" }
        }
      }]
    },
    "selectedQueryName": "A",
    "thresholds": {
      "kind": "basic",
      "spec": [{ "name": "warning", "op": "above", "target": 1, "targetUnit": "s", "matchType": "at_least_once", "channels": ["lab-webhook"] }]
    }
  },
  "evaluation": { "kind": "rolling", "spec": { "evalWindow": "5m", "frequency": "1m" } },
  "notificationSettings": { "renotify": { "enabled": false } },
  "ruleType": "threshold_rule",
  "schemaVersion": "v2alpha1",
  "version": "v5"
}
```

ونرسل الملفين إلى `POST /api/v2/rules`. وبعد دقيقة واحدة وصل إلى الـ Webhook ما يلي (مختصراً):

```json
{"receiver":"lab-webhook","status":"firing","alerts":[{"status":"firing",
  "labels":{"alertname":"Payment gateway timeouts","severity":"critical","team":"payments", …},
  "annotations":{"description":"orders-api logged 12 PSP_TIMEOUT errors in the last 5 minutes.",
    "related_logs":"http://localhost:8080/logs/logs-explorer?compositeQuery=…",
    "summary":"مهلة بوابة الدفع: 12 حالة خلال 5 دقائق"},
  "startsAt":"2026-10-23T07:00:35.476741641Z", …}], …}
```

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

- أنشأنا القاعدة في 09:59:38 وانطلقت في 10:00:35، أي عند أول تقييم بعد دقيقة، ووصل الإشعار بعدها بنصف دقيقة.
- النص العربي في `summary` وصل سليماً بترميز UTF-8، فتستطيع كتابة رسائل التنبيه بالعربية لفريق المناوبة.
- الإشعار يحمل رابط `related_logs` جاهزاً بالفلتر نفسه، ولكنه على `localhost:8080` لأن هذا الإشعار وصل قبل أن نضبط `SIGNOZ_ALERTMANAGER_SIGNOZ_EXTERNAL__URL` كما في [قسم التثبيت](#install).

ثم جاء دور الإصلاح: أعدنا تشغيل التقرير بالاستدعاء الواحد في 10:05:52، فانتهى التنبيه الثاني في 10:13:42 ووصل إشعار `"status":"resolved"`. أي قرابة ثماني دقائق بين الإصلاح وإشعار العودة، وهذا منطقي لأن نافذة التقييم Evaluation Window خمس دقائق، فيبقى الحد متجاوزاً حتى تخرج القيم البطيئة منها. وتذكر أن ملخص إشعار العودة يحمل آخر قيمة وقت الانطلاق (`p95 of the daily report is 1.49 s`) وليس القيمة الحالية.

![صفحة Alert Rules في SigNoz وفيها قاعدتان بحالة FIRING: Payment gateway timeouts بمستوى critical وملصق team: payments، وDaily report is slow بمستوى warning](https://arabroot.io/content/images/2026/10/signoz-11-alerts.webp)

القاعدتان في الواجهة بعد أن أنشأناهما من الـ API

## الاحتفاظ Retention

الإعدادات الافتراضية في التثبيت الذاتي هي 15 يوماً للتتبع و15 يوماً للسجلات وشهر للمقاييس، وقرأناها من الـ API ومن جداول ClickHouse نفسها. وتغيرها من Settings ثم Workspace، أو من الـ API كما يلي:

```bash
curl -s -X POST "http://127.0.0.1:8080/api/v1/settings/ttl?duration=168h&type=traces" -H "Authorization: Bearer $TOKEN"
curl -s -X POST http://127.0.0.1:8080/api/v2/settings/ttl -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"type":"logs","defaultTTLDays":7}'
```

```text
{"message":"move ttl has been successfully set up"}
{"message":"custom retention TTL has been successfully set up"}
```

![صفحة Settings ثم Workspace في SigNoz وفيها Retention Controls: Metrics شهر واحد، وTraces سبعة أيام مع رسالة نجاح تغيير الاحتفاظ إلى 7 أيام، وLogs سبعة أيام](https://arabroot.io/content/images/2026/10/signoz-12-retention.webp)

التتبع والسجلات أصبحت 7 أيام

والتغيير ينفذ في ClickHouse مباشرة: فالجدول `signoz_index_v3` أصبح `TTL … + toIntervalSecond(604800)`، أي 7 أيام، والقيمة الافتراضية لعمود `_retention_days` في جدول السجلات أصبحت 7\. وتذكر أن ClickHouse يحذف البيانات القديمة عند دمج الأجزاء Merges وليس في لحظة التغيير، فلا تتوقع أن ينخفض حجم القرص فوراً. أما الاحتفاظ المختلف لكل مصدر سجلات فصفحة الأسعار تذكره ميزة قادمة في السحابة وEnterprise، وليس في النسخة المجتمعية.

## كم يستهلك SigNoz؟ الذاكرة RAM والقرص Disk بالأرقام

ClickHouse هو ما يحدد حجم السيرفر، لذلك قسنا ثلاث حالات: المنصة بدون بيانات، ثم تحت حمل خفيف من مولد واحد، ثم عشر دقائق بأربعة مولدات معاً على الإعداد البطيء للتقرير، أي قرابة 96 Span و64 سجلاً في الثانية. والقياس بالأمر `docker stats` كل 25 ثانية على سيرفر بثمانية معالجات مشتركة:

| المكون            | بدون بيانات | تحت الحمل (أدنى إلى أعلى) | المعالج CPU تحت الحمل (متوسط / أعلى) |
| ----------------- | ----------- | ------------------------- | ------------------------------------ |
| ClickHouse        | 714 MiB     | 1734 إلى 1924 MiB         | 66% / 258%                           |
| ClickHouse Keeper | 91 MiB      | 118 إلى 126 MiB           | 5% / 17%                             |
| المستقبل Ingester | 32 MiB      | 92 إلى 105 MiB            | 5% / 18%                             |
| SigNoz            | 38 MiB      | 53 إلى 70 MiB             | 5% / 88%                             |
| PostgreSQL        | 37 MiB      | 39 MiB                    | 1% / 15%                             |

أي أن المنصة كلها تحتاج قرابة 2.3 GB من الذاكرة لحمل خفيف، والنسبة 100% في عمود المعالج تعني معالجاً واحداً كاملاً. أما القرص فبعد قرابة 40 دقيقة من البيانات كان كما يلي:

| القاعدة في ClickHouse          | السجلات     | على القرص | قبل الضغط |
| ------------------------------ | ----------- | --------- | --------- |
| signoz\_traces                 | 86,447 Span | 5.95 MiB  | 98.42 MiB |
| signoz\_logs                   | 58,220 سجل  | 2.56 MiB  | 37.64 MiB |
| signoz\_metrics                | 43,010 عينة | 0.37 MiB  | 5.28 MiB  |
| system (سجلات ClickHouse نفسه) |             | 62.10 MiB |           |

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

- الضغط Compression قرابة 16 ضعفاً، أي حوالي 72 بايت للـ Span و46 بايت للسجل. ولكن تذكر أن بيانات المختبر متكررة جداً (خمسة مسارات ورسائل متشابهة)، والبيانات الحقيقية بخصائص أكثر تنضغط أقل، فاستخدم هذه الأرقام حداً أدنى وقس على بياناتك.
- سجلات ClickHouse الداخلية في قاعدة `system` كانت عشرة أضعاف بياناتنا. وجداولها في هذا التثبيت تحذف بعد يوم واحد (TTL يوم)، لذلك يتوقف نموها، ولكنها تفسر لماذا كان الـ Volume بحجم 233.8 MB رغم أن البيانات نفسها أقل من 10 MiB، ويضاف إليها الأجزاء القديمة التي تنتظر الحذف بعد الدمج.
- زمن التقرير نفسه تأثر بالحمل: p95 قفز من قرابة 1.8 ثانية إلى 4.76 ثانية، ورأينا ذلك في التنبيه قبل أن نراه في الكود.

### ماذا يحدث إذا توقف ClickHouse؟

أوقفنا ClickHouse 90 ثانية والحمل يعمل، ثم شغلناه مرة أخرى. والتطبيق لم يتأثر إطلاقاً لأن الإرسال يتم في الخلفية، والمستقبل بدأ يكرر `Exporting failed. Will retry the request after interval.`. وبعد العودة عددنا طلبات الدفع المسجلة: كانت قرابة 60 في كل 30 ثانية قبل التوقف، ووصل للدقيقة والنصف التي توقف فيها 177 طلباً مقابل قرابة 180 متوقعة، أي أن البيانات وصلت متأخرة ولم يضع منها إلا القليل. ولكن لا تعتمد على هذا في توقف أطول، والسبب أن الإعداد الافتراضي للمستقبل يعطل الطابور `sending_queue: enabled: false`، فالمحاولة تتم والدفعة في الذاكرة، وحدها الأقصى في [مكتبة الـ Exporters](https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/exporterhelper/README.md?ref=arabroot.io) هو `max_elapsed_time` بقيمة 300 ثانية افتراضياً، ثم تسقط الدفعة. فإذا كانت الصيانة ستطول فضع أمام SigNoz مستقبلاً Collector بطابور على القرص، كما شرحنا الفكرة في [مسار السجل](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/).

## الوصول عبر نطاق Domain مع TLS

الواجهة مربوطة بـ `127.0.0.1:8080`، لذلك نضعها خلف Reverse Proxy. والمثال التالي بـ [Caddy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/) الذي يصدر الشهادة تلقائياً بحسب [توثيق reverse\_proxy](https://caddyserver.com/docs/caddyfile/directives/reverse%5Fproxy?ref=arabroot.io):

```text
signoz.example.com {
    reverse_proxy 127.0.0.1:8080
}
```

أما الاستقبال على 4317 و4318 فاتركه على الشبكة الخاصة أو داخل VPN، لأن النسخة المجتمعية لا تتحقق من هوية المرسل، وأي جهة تصل إلى المنفذ تستطيع أن تملأ قرصك أو أن تدس سجلات مزيفة. ولم نختبر إصدار شهادة حقيقية هنا لأنه يحتاج نطاقاً عاماً، والإعداد أعلاه هو نفسه في بقية أدلة الموقع.

## التحقق من النجاح

1. `curl -s http://127.0.0.1:8080/api/v1/health` يرجع `{"status":"ok"}`.
2. سجل المستقبل فيه `Everything is ready. Begin running and processing data.`، ولا يتكرر فيه `connection refused` لعنوان OpAMP.
3. خدماتك تظهر في Services خلال دقيقة من بدء الإرسال، وفي Logs Explorer تجد سجلات بقيمة في `trace_id`.
4. تبويب Logs في أي تتبع يعرض سجلات الطلب نفسه.
5. تنبيه تجريبي يصل إلى قناتك، والرابط فيه يفتح على عنوانك الخارجي وليس `localhost`.
6. صفحة Retention تعرض المدد التي اخترتها.

## النسخ الاحتياطي Backup والاستعادة Restore

لا يوجد في توثيق SigNoz دليل رسمي للنسخ الاحتياطي في Docker، لذلك نعتمد على ما نعرفه عن مكوناته: الإعدادات (المستخدمون واللوحات والتنبيهات والقنوات) في PostgreSQL، والبيانات في ClickHouse، وحالة التنسيق في Keeper. والطريقة التي اختبرناها كاملة هي النسخة الباردة Cold Backup: نوقف الـ Containers، ونضغط الـ Volumes الثلاثة، ثم نشغل مرة أخرى، مع نسخة منطقية من PostgreSQL قبل الإيقاف:

```bash
#!/usr/bin/env bash
# Cold backup of a SigNoz Docker install: stop the containers, archive the three data volumes, start again.
# Run from the folder that holds pours/deployment.  Usage: NAME=signoz bash backup.sh /path/to/backups
set -euo pipefail
NAME=${NAME:-signoz}   # metadata.name in casting.yaml
DEST=${1:?backup folder}; STAMP=$(date +%Y%m%d-%H%M); mkdir -p "$DEST"
docker exec "$NAME-metastore-postgres-0" pg_dump -U signoz -Fc signoz > "$DEST/metastore-$STAMP.dump"   # logical copy, taken while running
(cd pours/deployment && docker compose stop)
for v in "$NAME-telemetrystore-0-0-data" "$NAME-telemetrykeeper-0-data" "$NAME-metastore-postgres-0-data"; do
  docker run --rm -v "$v:/src:ro" -v "$DEST:/backup" alpine:3.22 tar czf "/backup/$v-$STAMP.tar.gz" -C /src .
done
(cd pours/deployment && docker compose start)
ls -l "$DEST"
```

والاستعادة تنشئ الـ Volumes من جديد وتفك فيها النسخة:

```bash
#!/usr/bin/env bash
# Restores the volumes archived by backup.sh into empty volumes, then starts SigNoz.
# Usage: NAME=signoz bash restore.sh /path/to/backups <stamp>
set -euo pipefail
NAME=${NAME:-signoz}
SRC=${1:?backup folder}; STAMP=${2:?stamp, e.g. 20261023-1050}
(cd pours/deployment && docker compose down)
for v in "$NAME-telemetrystore-0-0-data" "$NAME-telemetrykeeper-0-data" "$NAME-metastore-postgres-0-data"; do
  docker volume rm "$v" 2>/dev/null || true
  docker volume create "$v" >/dev/null
  docker run --rm -v "$v:/dst" -v "$SRC:/backup:ro" alpine:3.22 tar xzf "/backup/$v-$STAMP.tar.gz" -C /dst
done
(cd pours/deployment && docker compose up -d)
```

والمتغير `NAME` هو `metadata.name` في `casting.yaml`، لأن أسماء الـ Volumes مبنية عليه. والنتيجة في المختبر كانت كما يلي:

| الملف                                 | الحجم   |
| ------------------------------------- | ------- |
| نسخة PostgreSQL المنطقية pg\_dump -Fc | 118 KiB |
| Volume قاعدة PostgreSQL               | 7.5 MiB |
| Volume الـ Keeper                     | 2.7 MiB |
| Volume الـ ClickHouse                 | 172 MiB |

- توقفت المنصة 52 ثانية أثناء النسخ، والتطبيقات استمرت تعمل والإرسال أعاد المحاولة كما رأينا في [توقف ClickHouse](#outage).
- بعد الاستعادة في Volumes جديدة رجع العدد نفسه تماماً: 90,229 Span و60,704 سجلات قبل وقت النسخ، ومعها اللوحة والتنبيهان ومدة الاحتفاظ 168 ساعة.
- نسخة ClickHouse لم تنضغط تقريباً (172 MiB من Volume بحجم قرابة 234 MB) لأن بياناته مضغوطة أصلاً، ومعظمها سجلات ClickHouse الداخلية كما في [قسم الموارد](#resources).

⚠️

النسخة الباردة مناسبة لسيرفر واحد وحجم صغير. وعندما تكبر البيانات إلى مئات GB تصبح دقائق التوقف أطول من المقبول، والحل الأنسب عندها أمر `BACKUP` في ClickHouse نفسه إلى قرص أو S3 كما في [توثيق ClickHouse](https://clickhouse.com/docs/operations/backup?ref=arabroot.io)، مع `pg_dump` للإعدادات. ولم نختبر هذا الطريق هنا، فاختبر الاستعادة قبل أن تعتمد عليه.

وقد يتساءل البعض: هل تحتاج نسخة احتياطية لبيانات المراقبة أصلاً؟ والإجابة أن التتبع والسجلات عمرها أيام وقيمتها في الحاضر، فكثير من الفرق تكتفي بنسخ PostgreSQL وملف `casting.yaml` وملفات اللوحات والتنبيهات في Git، لأن إعادة بناء هذه الإعدادات هي الجزء المؤلم، أما البيانات فتعود خلال دقائق من الإرسال.

## التحديث Upgrade إلى إصدار أحدث

SigNoz يصدر إصداراً جديداً كل أسبوع أو أسبوعين تقريباً (v0.143.0 في 23 سبتمبر وv0.147.0 في 22 أكتوبر 2026)، وبعض الإصدارات فيها ترحيلات Migrations يجب أن تتوقف عندها. لذلك يطلب [دليل الترقية](https://signoz.io/docs/operate/upgrade/?ref=arabroot.io) أن تستخدم أداة مسار الترقية Upgrade Path Tool لتعرف المحطات بين إصدارك والإصدار الهدف، وأن ترقي إلى كل محطة بإصدار مثبت ثم تنتظر اكتمال الترحيلات قبل التالية. والخطوات مع Foundry كما يلي:

1. خذ نسخة احتياطية كما في القسم السابق.
2. غير `image` في `casting.yaml` إلى الإصدار التالي في المسار، للخدمة `signoz` وللمستقبل `ingester` بحسب ملاحظات الإصدار.
3. شغل `foundryctl forge -f casting.yaml` وراجع الفرق في `pours/deployment/compose.yaml`.
4. شغل `docker compose up -d` وانتظر خروج `telemetrystore-migrator` بالرمز 0 وظهور `Everything is ready` في سجل المستقبل.

ولم نختبر الترقية بين إصدارين هنا لأن الدليل كتب على آخر إصدار، ولكننا غيرنا `casting.yaml` وأعدنا التوليد والتشغيل عدة مرات (تغيير الـ Image ومتغيرات البيئة)، وفي كل مرة أعاد Compose إنشاء الـ Container الذي تغير وأعاد تشغيل الـ Containers التي تعمل مرة واحدة (`migrator` و`user-scripts`)، وبقيت البيانات كما هي.

## حدود SigNoz أمام Datadog بصراحة

SigNoz يغطي جيداً القلب المشترك: التتبع والسجلات والمقاييس واللوحات والتنبيهات في واجهة واحدة. ولكن Datadog منصة أوسع بكثير، وقبل أن تقرر الانتقال قارن ما تستخدمه أنت فعلاً:

| الجانب                                | Datadog                                                                                                                                                      | SigNoz المستضاف ذاتياً                                                                                                                                                                                       |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| جمع البيانات                          | Agent واحد مع [تكاملات Integrations](https://docs.datadoghq.com/integrations/?ref=arabroot.io) جاهزة لمئات الأنظمة                                           | OpenTelemetry SDK وCollector، وتجمع إعداد كل مصدر بنفسك                                                                                                                                                      |
| مراقبة الواجهة الأمامية Frontend      | RUM مع [إعادة تشغيل الجلسات Session Replay](https://docs.datadoghq.com/real%5Fuser%5Fmonitoring/session%5Freplay/?ref=arabroot.io)                           | [Web Vitals والتتبع عبر OpenTelemetry](https://signoz.io/docs/frontend-monitoring/?ref=arabroot.io)، ولم نجد في التوثيق ما يقابل Session Replay                                                              |
| الاختبارات الاصطناعية Synthetics      | [مدمجة](https://docs.datadoghq.com/synthetics/?ref=arabroot.io)                                                                                              | لم نجد ما يقابلها في التوثيق، فتحتاج أداة منفصلة                                                                                                                                                             |
| التحليل المستمر Profiling والأمن SIEM | [Continuous Profiler](https://docs.datadoghq.com/profiler/?ref=arabroot.io) و[Cloud SIEM](https://docs.datadoghq.com/security/cloud%5Fsiem/?ref=arabroot.io) | خارج نطاق المنتج                                                                                                                                                                                             |
| اكتشاف الشذوذ Anomaly Detection       | مدمج                                                                                                                                                         | [متاح في السحابة وEnterprise](https://signoz.io/docs/alerts-management/anomaly-based-alerts/?ref=arabroot.io)، وليس في Community                                                                             |
| الصلاحيات SAML وRBAC الدقيقة          | مدمجة                                                                                                                                                        | في السحابة وEnterprise، والأدوار المخصصة تحتاج ترخيصاً بحسب [التوثيق](https://signoz.io/docs/manage/administrator-guide/iam/roles/?ref=arabroot.io)، ورأينا في الواجهة الأدوار الجاهزة Admin وEditor وViewer |
| التشغيل                               | لا شيء عليك                                                                                                                                                  | ClickHouse وترقيات أسبوعية ونسخ احتياطي ومساحة قرص، وكلها عليك                                                                                                                                               |
| التكلفة                               | تزيد مع كل سيرفر وGB                                                                                                                                         | سيرفر بذاكرة 8 GB أو أكثر، ووقت مهندس يديره                                                                                                                                                                  |

والحكم العملي: إذا كانت فاتورة Datadog عندك أغلبها APM والسجلات، ولديك من يدير ClickHouse، فإن SigNoz يعطيك معظم ما تستخدمه يومياً. أما إذا كنت تعتمد على Session Replay أو Synthetics أو SIEM فسوف تحتاج أدوات إضافية، وكل أداة إضافية تعيدك جزئياً إلى مشكلة تعدد الواجهات التي بدأنا بها.

## مشكلات شائعة وحلولها

- **المستقبل يكرر `Error occurred while checking for sync migrations to complete, retrying`:** طبيعي في أول دقيقتين أو ثلاث، فالمستقبل ينتظر الـ `migrator`. وإذا استمر فافحص سجل `telemetrystore-migrator`.
- **المستقبل يكرر `dial tcp …:4320: connect: connection refused`:** افتح `pours/deployment/ingester/opamp.yaml` وتأكد أن العنوان يشير إلى Container SigNoz. ورأينا هذا مع `metadata.name` ينتهي بـ `-signoz` في foundryctl v0.3.0، والحل اسم آخر.
- **SigNoz يتوقف بـ `factory "postgres" not found or not registered`:** أنت تشغل `signoz-community` مع PostgreSQL، فإما `metastore.kind: sqlite` أو الـ Image الافتراضي.
- **روابط التنبيهات على `localhost:8080`:** أضف `SIGNOZ_ALERTMANAGER_SIGNOZ_EXTERNAL__URL` إلى `env` وأعد التوليد والتشغيل.
- **الـ `patches` لا تطابق والخطأ `did not match any generated material`:** ظهر لنا مع `foundryctl.exe` على Windows، لأنه يتوقع المسار `deployment\compose.yaml` بالشرطة المائلة العكسية، بينما يعمل `deployment/compose.yaml` مع نسخة Linux. والتوثيق يدعم Linux وmacOS وWSL 2 أصلاً.
- **الخدمة تظهر والسجلات لا تصل:** تحقق من `OTEL_LOGS_EXPORTER=otlp` و`OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true` في Python، أو ما يقابلها في لغتك.
- **البحث العربي لا يجد ما تتوقعه:** اختلاف الهمزة أو التاء المربوطة يكفي، فابحث بخاصية ثابتة مثل `error_code`.
- **الـ Containers تعيد التشغيل باستمرار:** غالباً الذاكرة، فخصص لـ Docker 4 GB على الأقل، وراقب ClickHouse أولاً.

## الخلاصة

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

- SigNoz يجمع التتبع والسجلات والمقاييس في واجهة واحدة فوق OpenTelemetry وClickHouse، والربط بينها بـ `trace_id` يعمل تلقائياً، وهو أقرب ما يكون مفتوح المصدر لتجربة Datadog في APM.
- الـ Docker Image الافتراضي `signoz/signoz` مبني من كود Enterprise، والبديل MIT الخالص `signoz/signoz-community` يعمل مع SQLite فقط، فاختر عن قصد.
- التثبيت اليوم عبر Foundry، فثبت الإصدارات وأطفئ الإحصاءات واضبط العنوانين الخارجيين ولا تعرض منافذ الاستقبال.
- التتبع كشف في دقيقة مشكلة N+1 لا تظهر في أي سجل، ونقل التقرير من 1.71 ثانية إلى 13 ms، وكشف أن الدفع قد ينجح بعد أن يرى العميل الخطأ.
- البحث العربي يعمل بمطابقة حرفية، والتنبيهات تحمل النص العربي سليماً إلى الـ Webhook.
- احسب 2.3 GB ذاكرة على الأقل، ومعظمها ClickHouse، والبيانات تنضغط جيداً بينما سجلات ClickHouse الداخلية تأخذ مساحة أكبر في البداية.
- النسخة الباردة للـ Volumes الثلاثة استعادت كل شيء كما هو بتوقف 52 ثانية.

## تدريب عملي

- غير مهلة الطلب في `orders-api` إلى ثلاث ثوان، ولاحظ في خريطة الخدمات وفي التنبيه كيف تختفي أخطاء 502 بينما يرتفع p99 لخدمة الطلبات.
- أضف تنبيهاً على العداد `shop.checkouts` بحيث ينطلق عندما تتجاوز نسبة `result = failed` عشرة في المئة، ثم قارن وقت انطلاقه بالتنبيه المبني على السجلات.

وفي المقال القادم بإذن الله نحسب تكلفة منصات المراقبة التجارية مقارنة بالاستضافة الذاتية بالأرقام.

[![](https://arabroot.io/content/images/2026/10/observability-intro-cover-cover.webp)دليلمفاهيم المراقبة Observability: من الإشارة إلى التنبيهما الذي تجيب عنه المقاييس والسجلات والتتبع، وكيف تبني تنبيهاً على ما يشعر به المستخدم وعلى ميزانية الأخطاء بدل كل رقم.](https://arabroot.io/articles/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%A7%D9%84%D9%85%D8%B1%D8%A7%D9%82%D8%A8%D8%A9-observability-%D9%85%D9%86-%D8%A7%D9%84%D8%A7%D8%B4%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%89-%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/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/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/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/seq-cover-cover.webp)دليلسجلات منظمة Structured Logs تبحث فيها بالخصائص مع Seqكيف تشغل Seq وترسل إليه السجلات من .NET وأي لغة أخرى، وتبحث فيها بالخصائص، وتضبط الاحتفاظ والتنبيهات، مع ما يسمح به الترخيص المجاني.](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-%D9%85%D9%86%D8%B8%D9%85%D8%A9-structured-logs-%D9%85%D8%B9-seq/)[![](https://arabroot.io/content/images/2026/10/log-tools-compare-cover-cover.webp)دليلأي أداة لسجلاتك المركزية؟ مقارنة بالأرقام على البيانات نفسهاأدخلنا 500 ألف حدث في Loki وGraylog وSeq وOpenSearch، وقارنا التخزين والذاكرة والبحث بالعربية والتكلفة ومن يناسبه كل منها.](https://arabroot.io/articles/%D9%85%D9%82%D8%A7%D8%B1%D9%86%D8%A9-%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%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-%D8%A8%D8%A7%D9%84%D8%A7%D8%B1%D9%82%D8%A7%D9%85/)[![](https://arabroot.io/content/images/2026/10/error-tracking-cover-cover.webp)دليلتتبع الأخطاء Error Tracking للمطورين مع GlitchTipماذا يضيف تتبع الأخطاء فوق السجلات، وكيف تشغل GlitchTip بديلاً خفيفاً لـ Sentry يستقبل أخطاء تطبيقك بالـ SDK نفسه.](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-error-tracking-%D9%85%D8%B9-glitchtip/)

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

- أكتوبر 2026: كتابة الدليل واختباره على SigNoz v0.147.0 وsignoz-otel-collector v0.144.14 وClickHouse 25.12.5 مع foundryctl v0.3.0، وتطبيق Python 3.13 بمكتبات OpenTelemetry 1.45.1 / 0.66b1، ومراجعة صفحة الأسعار والترخيص في 23 أكتوبر 2026.