في مفاهيم المراقبة Observability قسمنا ما يخرج من التطبيق إلى ثلاث إشارات Signals: السجلات Logs والمقاييس Metrics والتتبع Traces، ثم شغلنا في المقالات التالية أداة لكل جزء: Loki وGraylog وSeq للسجلات، وGlitchTip لتتبع الأخطاء Error Tracking. وهذا يعمل، ولكن عندما يبطئ طلب في الإنتاج سوف تجد نفسك تنتقل بين ثلاث واجهات وتنسخ رقم الطلب من واحدة إلى أخرى، وهذا بالضبط ما تبيعه Datadog: منصة واحدة ترى فيها الطلب وسجلاته ومقاييسه في شاشة واحدة، ولكن بفاتورة تكبر مع كل سيرفر وكل GB. وSigNoz هو أشهر محاولة مفتوحة المصدر لتقديم التجربة نفسها على سيرفرك، فهو مبني من البداية على 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 الرسمية في كل لغة، وإذا قررت لاحقاً أن تنتقل إلى أداة أخرى فإن الكود نفسه يبقى كما هو وتغير عنوان الإرسال فقط. والشكل التالي يبين المكونات التي تعمل على سيرفرك ومسار البيانات بينها:

والجدول التالي يلخص دور كل مكون، والأرقام في العمود الأخير من مختبرنا تحت حمل خفيف كما سيأتي في قسم الموارد:
| المكون | الدور | الذاكرة 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 في كل سجل، وهذا ما رأيناه في الطلب الفاشل. - OpenTelemetry من الأصل: لا يوجد Agent خاص ولا مكتبة خاصة، وبالتالي لا تقيد كود تطبيقك بمنصة واحدة.
- ClickHouse في الخلف: ضغط Compression مرتفع واستعلامات تجميع Aggregation سريعة على حجم كبير، ورأينا في المختبر أن 86 ألف Span شغلت على القرص أقل من 6 MiB.
- التنبيهات واللوحات مدمجة: تنبيه على عدد سجلات أو على زمن الاستجابة Latency من التتبع، ويصل إلى Webhook أو Slack أو Email دون أداة إضافية.
نقاط الضعف:
- ClickHouse ثقيل على السيرفرات الصغيرة: قرابة 2 GB من الذاكرة تحت حمل خفيف، والمنصة كلها تحتاج 4 GB على الأقل لـ Docker بحسب دليل التثبيت.
- الترخيص ليس MIT بالكامل: الـ Docker Image الافتراضي يحتوي كوداً تحت ترخيص Enterprise كما سيأتي في قسم الترخيص.
- لا توجد مصادقة Authentication على الاستقبال Ingestion في النسخة المجتمعية: تطبيقنا أرسل دون أي مفتاح، ومفاتيح الاستقبال Ingestion Keys من ميزات السحابة في صفحة الأسعار، لذلك لا تعرض المنفذين 4317 و4318 للإنترنت.
- البحث العربي حرفي: يطابق الحروف كما هي دون تطبيع Normalization للهمزات والتاء المربوطة.
الترخيص License: ما هو مفتوح المصدر Open Source وما هو تجاري؟
يقول ملف LICENSE في المستودع إن كل ما هو خارج المجلدين ee/ وcmd/enterprise/ تحت ترخيص MIT، أما ما بداخلهما فتحت ترخيص SigNoz Enterprise، وهذا الترخيص يسمح بنسخ الكود وتعديله للتطوير والاختبار، ولكنه يشترط اتفاقية واشتراكاً صالحاً للاستخدام في الإنتاج 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 توقف فوراً بالخطأ التالي:
"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 قبل الإنتاج، لأن تفسير الترخيص قرار قانوني وليس تقنياً.
أما الميزات نفسها فصفحة الأسعار (راجعناها في 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، والطريقة المدعومة اليوم هي أداة Foundry: تكتب ملفاً واحداً اسمه casting.yaml يصف ما تريد، والأداة foundryctl تولد منه ملفات Compose وإعدادات ClickHouse والمستقبل في المجلد pours/. والفكرة جيدة لأن الترقية بعدها تصبح تغيير رقم الإصدار وإعادة التوليد، بدل دمج ملف Compose طويل يدوياً.
أولاً نثبت foundryctl. والتوثيق يستخدم سكربتاً من الموقع، والأوضح أن تنزل الإصدار المحدد من صفحة الإصدارات وتتحقق من الـ Checksum:
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، ويفتح المنافذ على كل الواجهات، ويترك إرسال الإحصاءات مفعلاً، لذلك نكتب الملف التالي:
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: مفعل افتراضياً في ملف الإعدادات ويرسل إحصاءات استخدام إلى SigNoz كل 6 ساعات، فأطفئه إذا كان هذا يخالف سياستك.- عنوانان خارجيان External URL: الأول للواجهة كما في التوثيق، والثاني تستخدمه التنبيهات في الروابط التي ترسلها. وبدونه وصلتنا كل إشعارات الـ Webhook بروابط
http://localhost:8080/alerts/...لا تفتح عند أحد، وعندما أضفناه أصبحت الروابط بالعنوان الصحيح. - الـ
patches: عمليات JSON Patch على ملف Compose الناتج، وبها نربط الواجهة بـ127.0.0.1فقط خلف الـ Reverse Proxy، والاستقبال بعنوان الشبكة الخاصة (هنا10.0.0.5مثال). وهذه هي الطريقة الرسمية لتغيير ما لا يغطيهcasting.yaml، ولا تعدل ملفاتpours/بيدك لأن التوليد التالي يستبدلها.
الآن نولد الملفات ونراجعها ثم نشغل. والأمر foundryctl cast يفعل الخطوتين معاً، ولكننا نفصلهما لنرى ما سيعمل قبل تشغيله:
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):
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، وانتظر السطر التالي:
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/versioningester-1 | {"level":"info","ts":"2026-10-23T06:45:38.354Z","caller":"[email protected]/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 المعروفة. والملف كاملاً كما يلي:
# 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. والـ 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:
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 مستقلة تبحث بها، وقد شرحنا لماذا يكتب السجل منظماً في سجلات تساعدك فعلاً. - العنوان
sg1-ingester:4317والشبكةsg1-networkهما اسم المستقبل وشبكة Docker عندما يكونmetadata.nameهوsg1، فإذا كان التطبيق على سيرفر آخر فالعنوان هو العنوان الخاص لسيرفر SigNoz على المنفذ 4317. - الـ
loadgenيرسل حملاً ثابتاً: قراءة طلب، ثم دفع ببطاقة عشوائية من ثلاث، ثم التقرير اليومي كل 15 ثانية.
الخدمات Services وخريطة الخدمات Service Map
بعد دقيقة من بدء الحمل تظهر الخدمتان في صفحة Services مع زمن الاستجابة p99 ونسبة الأخطاء وعدد العمليات في الثانية، وكلها محسوبة من التتبع دون أن نكتب أي مقياس:

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

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

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

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

وفي ClickHouse انخفض متوسط التقرير في الدقيقة التالية من 1752 ms إلى 14 ms، ثم بقي بين 12 و13 ms.
من الطلب Request الفاشل إلى سجله Log
المشكلة الثانية أصعب: قرابة 10% من طلبات الدفع تفشل بـ 502. ولو كانت السجلات في Loki والتتبع في أداة أخرى لاحتجت أن تبحث بالوقت وتخمن أي سطر يخص أي طلب. والشكل التالي يبين كيف يربط SigNoz بين الإشارات الثلاث لطلب واحد:

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

ولاحظ في التتبع ما لا يظهر في أي سجل: orders-api تخلت عن الطلب بعد ثانيتين وردت على العميل بـ 502، ولكن payments-api استمرت 2.5 ثانية وأنهت العملية بنجاح 200. أي أن العميل رأى خطأ بينما قد تكون بوابة الدفع خصمت المبلغ فعلاً. وهذا النوع من الأخطاء هو سبب وجود مفتاح عدم التكرار Idempotency Key في أنظمة الدفع، والتتبع هو ما جعله ظاهراً.
trace_id، وتراها في Logs Explorer ولكن لا تظهر في تبويب Logs لأي تتبع. وتفاصيل الربط في توثيق SigNoz.النص العربي في السجلات Logs والبحث Search
كتبنا رسائل السجلات بالعربية عمداً، والسؤال: هل تصل كما هي، وهل نبحث بها؟ والإجابة نعم في الحالتين، مع حدود. فالنص يخزن ويعرض بحروف متصلة صحيحة، ولكن عمود Body يعرض السطر باتجاه من اليسار إلى اليمين LTR، لذلك يظهر الرقم وعلامة النقطتين في سطر مثل «فشل الدفع للطلب 261: انتهت مهلة بوابة الدفع» في غير مكانهما:

ولقياس البحث كتبنا سكربتاً يرسل عدة تعابير بحث إلى الـ API نفسها التي تستخدمها الواجهة ويعد النتائج:
# 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]}")والمخرج سوف يكون كما يلي:
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': نفس النتيجة، ولا يتأثر بصياغة الرسالة أو لغتها.
وقد قارنا البحث العربي في أدوات السجلات الأخرى على البيانات نفسها في مقارنة أدوات السجلات بالأرقام.
لوحة Dashboard من الكود
تستطيع بناء اللوحة من الواجهة بالضغط على New Panel، ولكن اللوحة التي تبنى بالضغط تضيع مع أول إعادة تثبيت، لذلك نكتبها ملفاً ونحفظه في Git. وصيغة اللوحات في هذا الإصدار مبنية على مواصفات Perses، وهي الصيغة نفسها لقوالب SigNoz الرسمية في مستودع اللوحات، فأخذنا شكل لوحة apm/http-api-monitoring.json وبنينا منها لوحة بثلاثة رسوم:
# 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 جلسة نحصل عليه بالسكربت التالي:
#!/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/[email protected] | 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\":\"[email protected]\",\"password\":\"Lab-SigNoz-2026!\",\"orgId\":\"$ORG\"}" \
| python -c "import sys,json;print(json.load(sys.stdin)['data']['accessToken'])"وبعد تشغيل python dashboard.py "$(bash token.sh)" ظهرت اللوحة كما يلي:

ولاحظ أن الرسوم الثلاثة من ثلاث إشارات مختلفة: الأول من التتبع، والثاني من المقياس الذي كتبناه في الكود، والثالث من السجلات، وهذا ما يصعب فعله عندما تكون كل إشارة في أداة.
تنبيه Alert ينطلق فعلاً إلى Webhook
التنبيه الذي لم تره ينطلق لا تثق به، لذلك شغلنا سيرفراً صغيراً يطبع كل ما يصله، ثم أنشأنا قناة إشعارات Notification Channel من نوع Webhook بحسب التوثيق:
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 عندما تتجاوز أخطاء PSP_TIMEOUT ثلاثة في خمس دقائق، ولاحظ النص العربي في الملخص:
{
"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 عندما يتجاوز p95 للتقرير ثانية واحدة. والمقياس هنا بالنانو ثانية، لذلك نحدد "unit": "ns" للاستعلام و"targetUnit": "s" للحد، ويحول SigNoz بينهما:
{
"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 ما يلي (مختصراً):
{"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كما في قسم التثبيت.
ثم جاء دور الإصلاح: أعدنا تشغيل التقرير بالاستدعاء الواحد في 10:05:52، فانتهى التنبيه الثاني في 10:13:42 ووصل إشعار "status":"resolved". أي قرابة ثماني دقائق بين الإصلاح وإشعار العودة، وهذا منطقي لأن نافذة التقييم Evaluation Window خمس دقائق، فيبقى الحد متجاوزاً حتى تخرج القيم البطيئة منها. وتذكر أن ملخص إشعار العودة يحمل آخر قيمة وقت الانطلاق (p95 of the daily report is 1.49 s) وليس القيمة الحالية.

الاحتفاظ Retention
الإعدادات الافتراضية في التثبيت الذاتي هي 15 يوماً للتتبع و15 يوماً للسجلات وشهر للمقاييس، وقرأناها من الـ API ومن جداول ClickHouse نفسها. وتغيرها من Settings ثم Workspace، أو من الـ API كما يلي:
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}'{"message":"move ttl has been successfully set up"}
{"message":"custom retention TTL has been successfully set up"}
والتغيير ينفذ في 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 هو max_elapsed_time بقيمة 300 ثانية افتراضياً، ثم تسقط الدفعة. فإذا كانت الصيانة ستطول فضع أمام SigNoz مستقبلاً Collector بطابور على القرص، كما شرحنا الفكرة في مسار السجل.
الوصول عبر نطاق Domain مع TLS
الواجهة مربوطة بـ 127.0.0.1:8080، لذلك نضعها خلف Reverse Proxy. والمثال التالي بـ Caddy الذي يصدر الشهادة تلقائياً بحسب توثيق reverse_proxy:
signoz.example.com {
reverse_proxy 127.0.0.1:8080
}أما الاستقبال على 4317 و4318 فاتركه على الشبكة الخاصة أو داخل VPN، لأن النسخة المجتمعية لا تتحقق من هوية المرسل، وأي جهة تصل إلى المنفذ تستطيع أن تملأ قرصك أو أن تدس سجلات مزيفة. ولم نختبر إصدار شهادة حقيقية هنا لأنه يحتاج نطاقاً عاماً، والإعداد أعلاه هو نفسه في بقية أدلة الموقع.
التحقق من النجاح
curl -s http://127.0.0.1:8080/api/v1/healthيرجع{"status":"ok"}.- سجل المستقبل فيه
Everything is ready. Begin running and processing data.، ولا يتكرر فيهconnection refusedلعنوان OpAMP. - خدماتك تظهر في Services خلال دقيقة من بدء الإرسال، وفي Logs Explorer تجد سجلات بقيمة في
trace_id. - تبويب Logs في أي تتبع يعرض سجلات الطلب نفسه.
- تنبيه تجريبي يصل إلى قناتك، والرابط فيه يفتح على عنوانك الخارجي وليس
localhost. - صفحة Retention تعرض المدد التي اخترتها.
النسخ الاحتياطي Backup والاستعادة Restore
لا يوجد في توثيق SigNoz دليل رسمي للنسخ الاحتياطي في Docker، لذلك نعتمد على ما نعرفه عن مكوناته: الإعدادات (المستخدمون واللوحات والتنبيهات والقنوات) في PostgreSQL، والبيانات في ClickHouse، وحالة التنسيق في Keeper. والطريقة التي اختبرناها كاملة هي النسخة الباردة Cold Backup: نوقف الـ Containers، ونضغط الـ Volumes الثلاثة، ثم نشغل مرة أخرى، مع نسخة منطقية من PostgreSQL قبل الإيقاف:
#!/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 من جديد وتفك فيها النسخة:
#!/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.
- بعد الاستعادة في Volumes جديدة رجع العدد نفسه تماماً: 90,229 Span و60,704 سجلات قبل وقت النسخ، ومعها اللوحة والتنبيهان ومدة الاحتفاظ 168 ساعة.
- نسخة ClickHouse لم تنضغط تقريباً (172 MiB من Volume بحجم قرابة 234 MB) لأن بياناته مضغوطة أصلاً، ومعظمها سجلات ClickHouse الداخلية كما في قسم الموارد.
BACKUP في ClickHouse نفسه إلى قرص أو S3 كما في توثيق ClickHouse، مع pg_dump للإعدادات. ولم نختبر هذا الطريق هنا، فاختبر الاستعادة قبل أن تعتمد عليه.وقد يتساءل البعض: هل تحتاج نسخة احتياطية لبيانات المراقبة أصلاً؟ والإجابة أن التتبع والسجلات عمرها أيام وقيمتها في الحاضر، فكثير من الفرق تكتفي بنسخ PostgreSQL وملف casting.yaml وملفات اللوحات والتنبيهات في Git، لأن إعادة بناء هذه الإعدادات هي الجزء المؤلم، أما البيانات فتعود خلال دقائق من الإرسال.
التحديث Upgrade إلى إصدار أحدث
SigNoz يصدر إصداراً جديداً كل أسبوع أو أسبوعين تقريباً (v0.143.0 في 23 سبتمبر وv0.147.0 في 22 أكتوبر 2026)، وبعض الإصدارات فيها ترحيلات Migrations يجب أن تتوقف عندها. لذلك يطلب دليل الترقية أن تستخدم أداة مسار الترقية Upgrade Path Tool لتعرف المحطات بين إصدارك والإصدار الهدف، وأن ترقي إلى كل محطة بإصدار مثبت ثم تنتظر اكتمال الترحيلات قبل التالية. والخطوات مع Foundry كما يلي:
- خذ نسخة احتياطية كما في القسم السابق.
- غير
imageفيcasting.yamlإلى الإصدار التالي في المسار، للخدمةsignozوللمستقبلingesterبحسب ملاحظات الإصدار. - شغل
foundryctl forge -f casting.yamlوراجع الفرق فيpours/deployment/compose.yaml. - شغل
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 جاهزة لمئات الأنظمة | OpenTelemetry SDK وCollector، وتجمع إعداد كل مصدر بنفسك |
| مراقبة الواجهة الأمامية Frontend | RUM مع إعادة تشغيل الجلسات Session Replay | Web Vitals والتتبع عبر OpenTelemetry، ولم نجد في التوثيق ما يقابل Session Replay |
| الاختبارات الاصطناعية Synthetics | مدمجة | لم نجد ما يقابلها في التوثيق، فتحتاج أداة منفصلة |
| التحليل المستمر Profiling والأمن SIEM | Continuous Profiler وCloud SIEM | خارج نطاق المنتج |
| اكتشاف الشذوذ Anomaly Detection | مدمج | متاح في السحابة وEnterprise، وليس في Community |
| الصلاحيات SAML وRBAC الدقيقة | مدمجة | في السحابة وEnterprise، والأدوار المخصصة تحتاج ترخيصاً بحسب التوثيق، ورأينا في الواجهة الأدوار الجاهزة 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عشرة في المئة، ثم قارن وقت انطلاقه بالتنبيه المبني على السجلات.
وفي المقال القادم بإذن الله نحسب تكلفة منصات المراقبة التجارية مقارنة بالاستضافة الذاتية بالأرقام.
دليلمفاهيم المراقبة Observability: من الإشارة إلى التنبيهما الذي تجيب عنه المقاييس والسجلات والتتبع، وكيف تبني تنبيهاً على ما يشعر به المستخدم وعلى ميزانية الأخطاء بدل كل رقم.
دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.
دليلمسار السجل Log Pipeline من التطبيق إلى البحث والتنبيهكيف ينتقل السجل من التطبيق إلى المخزن عبر الناقل Shipper، وكيف تخفي البيانات الشخصية وتوجه الأخطاء وتحمي السجلات إذا توقفت الوجهة.
دليلجمع السجلات Logs مركزياً مع Grafana Lokiكيف تشغل Grafana Loki مع Alloy وGrafana، وتختار التسميات Labels التي لا توقف الإدخال، ثم تبحث وتنبه وتحذف بلغة LogQL.
دليلالسجلات المركزية Logs مع Graylog وOpenSearchكيف تثبت Graylog فوق OpenSearch، وتوجه الرسائل بال Streams وتخفي بياناتها بال Pipelines وتنبه منها، وما الذي لا يجده بحثه العربي.
دليلسجلات منظمة Structured Logs تبحث فيها بالخصائص مع Seqكيف تشغل Seq وترسل إليه السجلات من .NET وأي لغة أخرى، وتبحث فيها بالخصائص، وتضبط الاحتفاظ والتنبيهات، مع ما يسمح به الترخيص المجاني.
دليلأي أداة لسجلاتك المركزية؟ مقارنة بالأرقام على البيانات نفسهاأدخلنا 500 ألف حدث في Loki وGraylog وSeq وOpenSearch، وقارنا التخزين والذاكرة والبحث بالعربية والتكلفة ومن يناسبه كل منها.سجل التحديثات
- أكتوبر 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.