في دليل السجلات للمطورين كتبنا السجل حدثاً منظماً له حقول، وفي مسار السجل Log Pipeline نقلناه من التطبيق إلى مخزن مركزي. وبقي سؤال يسأله كل فريق صغير، خصوصاً فرق .NET: أين نضع هذه الأحداث بحيث نبحث فيها بالحقول كما نبحث في جدول، ونبني منها تنبيهاً، دون أن نشغل مجموعة كاملة من Elasticsearch وKibana أو نتعلم لغة استعلام جديدة؟ وأحد أشهر الإجابات هو Seq، وهو سيرفر سجلات من شركة Datalust بني من البداية للسجلات المنظمة Structured Logs، ويستقبل الأحداث من .NET عبر Serilog ومن أي لغة أخرى عبر HTTP.
وقبل أن نبدأ، لاحظ أن Seq ليس الخيار الوحيد: فهناك Grafana Loki الذي يخزن النص بأقل تكلفة ويفهرس الملصقات Labels فقط، وGraylog الذي يجمع السجلات من الأجهزة والسيرفرات فوق OpenSearch، ومجموعة ELK وOpenSearch للبحث النصي الكامل على حجم كبير. أما Seq فمكانه الفريق الذي يكتب سجلات منظمة أصلاً ويريد سيرفراً واحداً يعمل خلال دقائق، وهذا الدليل عنه.
وسوف نتناول في هذا المقال ما يلي:
- ما هو Seq ولمن يناسب، والترخيص والأسعار كما هي في أكتوبر 2026.
- التثبيت مع Docker Compose، ومفاتيح API Keys، ولماذا يجب أن تمنع الإرسال بدون مفتاح.
- ثلاثة طرق لإرسال السجلات: Serilog من .NET، وصيغة CLEF عبر HTTP من أي لغة، وGELF من Container قديم يكتب نصاً.
- البحث بالخصائص Properties، والاستعلام بلغة تشبه SQL، والبحث بالنص العربي.
- الإشارات Signals، وسياسات الاحتفاظ Retention، والتنبيهات Alerts إلى Webhook.
- ماذا قسنا عند تحميل 500 ألف حدث، والنسخ الاحتياطي والاستعادة، والمشكلات التي ظهرت لنا في المختبر وحلولها.
ما هو Seq ولمن يناسب؟
Seq سيرفر واحد يستقبل الأحداث Events ويخزنها بخصائصها على القرص، ويعطيك واجهة ويب للبحث والاستعلام ولوحات Dashboards وتنبيهات، دون أي مكون آخر: لا يحتاج قاعدة بيانات خارجية ولا محرك بحث ولا ناقل Shipper إلزامي. والشكل التالي يبين الطرق التي تصل بها الأحداث إليه، وما يفعله بها بعد ذلك:

والفكرة التي يقوم عليها Seq هي قالب الرسالة Message Template: فعندما تكتب في Serilog Log.Error(ex, "Checkout {OrderId} failed with {ErrorCode}", orderId, code) لا يصل إلى السيرفر نص فقط، وإنما يصل القالب نفسه وقيمة كل متغير خاصيةً مستقلة، كما في الشكل التالي:

وبالتالي تستطيع أن تبحث بـ ErrorCode = 'PSP_TIMEOUT' وأن تحسب متوسط ElapsedMs لكل نوع بطاقة، بدل أن تبحث عن كلمة داخل نص. وقد شرحنا لماذا يكتب السجل منظماً من الأصل في سجلات تساعدك فعلاً، فلن نعيده هنا.
نقاط القوة ونقاط الضعف
نقاط القوة:
- سيرفر واحد Single Binary: Container واحد ومجلد بيانات واحد، ويعمل خلال دقيقة، وهذا ما رأيناه في المختبر.
- تكامل .NET: المكتبة Serilog.Sinks.Seq سطر واحد في الإعداد، والقوالب والاستثناءات Exceptions تصل كما هي.
- لغة استعلام قريبة من SQL:
select count(*) from stream where … group by …يعرفها أي مطور، مع تعابير بحث Search Expressions بسيطة للبحث اليومي. - تنبيهات وإشارات مدمجة: لا تحتاج أداة منفصلة للتنبيه، وتصل التنبيهات إلى Email أو Slack أو أي Webhook عبر تطبيقات Seq Apps.
نقاط الضعف:
- ليس مفتوح المصدر: Seq منتج تجاري، والنسخة المجانية لمستخدم واحد فقط، وأي فريق يحتاج ترخيصاً مدفوعاً كما سيأتي.
- التوسع Scaling له حدود: يعمل جيداً على سيرفر واحد قوي، والتجميع Clustering متاح فقط في خطة Datacenter، لذلك لا يناسب من يجمع عشرات التيرابايت يومياً.
- البحث النصي الكامل محدود: البحث عن نص داخل الرسالة يعمل، ولكنه ليس محرك بحث نصي مثل OpenSearch يحلل الكلمات Analyzers ويطبع الحروف العربية Normalization، وهذا ما ظهر لنا في البحث بالعربية.
- يستهلك ما يجده من موارد: توصي متطلبات النظام بتشغيله على سيرفر مخصص، لأنه يحاول استخدام ما يتاح له من الذاكرة والمعالج.
الترخيص والأسعار
Seq يعمل بترخيص تجاري، والقبول به شرط للتشغيل (ACCEPT_EULA=Y). والجدول التالي من صفحة الأسعار كما رأيناها في 22 أكتوبر 2026، والأسعار بالدولار سنوياً دون الضريبة:
| الخطة | السعر السنوي | المستخدمون | التخزين | عدد السيرفرات |
|---|---|---|---|---|
| Individual | 0 دولار | مستخدم واحد | 50 GB | غير محدد |
| Team | 1,090 دولار | حتى 10 | 100 GB | سيرفر واحد |
| Standard | 2,990 دولار | بلا حد | بلا حد | حتى 3 |
| Datacenter | 10,990 دولار | بلا حد | بلا حد | بلا حد، مع التجميع Clustering |
وكل الخطط تشمل مدة احتفاظ ومصادر وتنبيهات بلا حد والدخول الموحد SSO. أما الترخيص المجاني Individual فتنص الصفحة على أنه يسمح لشخص واحد فقط بالدخول إلى واجهة الويب، ويسمح لكل مطور في الشركة أن يشغل نسخته الخاصة ما دام لا يستخدمها غيره. لذلك هو مناسب لجهاز المطور أو لسيرفر يتابعه شخص واحد، أما إذا كان الفريق كله سيفتح الواجهة فالبداية من Team. وقد يتساءل البعض: هل يتوقف Seq إذا تجاوزت 50 GB في الخطة المجانية؟ الصفحة لا توضح ذلك، لذلك اضبط الاحتفاظ حتى لا تقترب من الحد أصلاً.
المتطلبات
- السيرفر: تذكر متطلبات Seq أن الحمل الخفيف (أحداث قليلة في الثانية واحتفاظ 30 يوماً) يكفيه معالجان (2 CPU) و3.5 GB من الذاكرة (RAM) و50 GB على قرص SSD، وأن السيرفر المحمل يبدأ من 16 معالجاً و64 GB. وفي مختبرنا استهلك Seq بين 80 و145 MiB وهو فارغ، ووصل إلى 1.2 GiB بعد تحميل 500 ألف حدث والاستعلام عليها.
- القرص: قرص سريع بعدد عمليات IOPS مرتفع، ولا يدعم Seq مشاركات SMB ولا ضغط مجلدات NTFS.
- البرامج: Docker مع Docker Compose، وGit Bash أو أي Shell لتنفيذ الأوامر.
- المنافذ Ports: المنفذ 80 داخل الـ Container للواجهة والـ API، والمنفذ 5341 للاستقبال فقط، والمنفذ 12201/udp إذا شغلت مدخل GELF.
- نطاق فرعي Subdomain: مثل
seq.example.comإذا أردت الوصول من خارج السيرفر عبر HTTPS.
التثبيت مع Docker Compose
نبدأ بمجلد للمشروع، وفيه ملف .env للأسرار. ولا نضع كلمة مرور المدير Admin كما هي، وإنما نضع الهاش Hash الذي يولده Seq نفسه، ونولد معه مفتاح التشفير Secret Key الذي يحمي الحقول الحساسة والنسخ الاحتياطية:
mkdir -p ~/seq && cd ~/seq
printf 'كلمة-مرور-قوية-هنا' | docker run --rm -i datalust/seq:2026.1.17182 config hash
docker run --rm datalust/seq:2026.1.17182 show-key --generateثم نكتب الناتجين في .env:
SEQ_ADMIN_HASH=QG4EuGYq6Ecp...الهاش-كاملاً
SEQ_SECRET_KEY=hyqBdQRa8nEN...المفتاح-كاملاً=
SEQ_PUBLIC_URL=https://seq.example.com
SEQ_GELF_KEY=SEQ_SECRET_KEY خارج السيرفر في مدير كلمات المرور. فالنسخة الاحتياطية من Seq مشفرة بهذا المفتاح، ولا يمكن استعادتها بدونه، وهذا ما تنص عليه صفحة النسخ الاحتياطي في Docker.والآن ملف docker-compose.yml بإصدارات محددة، وفيه Seq ومدخل GELF الذي سنحتاجه لاحقاً:
services:
seq:
image: datalust/seq:2026.1.17182
container_name: seq
restart: unless-stopped
environment:
ACCEPT_EULA: "Y"
SEQ_FIRSTRUN_ADMINPASSWORDHASH: ${SEQ_ADMIN_HASH}
SEQ_STORAGE_SECRETKEY: ${SEQ_SECRET_KEY}
SEQ_API_CANONICALURI: ${SEQ_PUBLIC_URL}
volumes:
- seq-data:/data
ports:
- "127.0.0.1:8080:80" # الواجهة والـ API، خلف الـ Reverse Proxy
- "127.0.0.1:5341:5341" # الاستقبال فقط
healthcheck:
test: ["CMD", "curl", "-fs", "http://localhost/health"]
interval: 15s
timeout: 5s
retries: 10
seq-gelf:
image: datalust/seq-input-gelf:4.0.02323
container_name: seq-gelf
restart: unless-stopped
depends_on:
seq:
condition: service_healthy
environment:
SEQ_ADDRESS: http://seq:5341
SEQ_API_KEY: ${SEQ_GELF_KEY}
GELF_ADDRESS: udp://0.0.0.0:12201
ports:
- "127.0.0.1:12201:12201/udp"
volumes:
seq-data:في الإعداد أعلاه لاحظ التالي:
SEQ_FIRSTRUN_ADMINPASSWORDHASHيستخدم في أول تشغيل فقط لإنشاء المستخدمadmin، ومع الهاش لا يطلب Seq تغيير كلمة المرور عند أول دخول، بخلافSEQ_FIRSTRUN_ADMINPASSWORDكما في دليل التشغيل مع Docker.SEQ_API_CANONICALURIهو العنوان الذي يضعه Seq في روابط التنبيهات، وبدونه وصلتنا روابط تبدأ بـhttp://localhost:80لا تفتح عند أحد. ولهذا الإعداد أثر آخر سوف تراه في المشكلات الشائعة.- المنفذان مربوطان على
127.0.0.1فقط، لأن الواجهة ستمر عبر الـ Reverse Proxy، وتوصي Datalust بفصل منفذ الاستقبال 5341 عن منفذ الواجهة حتى تفتح للتطبيقات الاستقبال وحده. - الـ Healthcheck يستخدم
/healthالذي يجيب بـ{"status":"healthy"}عندما يكون السيرفر جاهزاً، ولذلك ينتظر مدخل GELF حتى يصبح Seq جاهزاً.
نشغل Seq وحده أولاً، لأن مدخل GELF يحتاج مفتاحاً لم ننشئه بعد:
docker compose up -d --wait seq
curl -s http://127.0.0.1:8080/healthوالمخرج سوف يكون كما يلي:
{"status":"healthy","description":"The Seq node is in service."}مفاتيح API Keys: من يرسل ومن يقرأ
كل تطبيق يرسل إلى Seq يجب أن يكون له مفتاح خاص به، والسبب أن المفتاح يحدد من أين جاء الحدث، ويمكن أن يضيف إليه خصائص ثابتة، وأن يرفض ما دون مستوى معين، وأن يلغى وحده إذا تسرب دون أن تمس بقية التطبيقات. وننشئ المفاتيح بأداة seqcli الرسمية، ونشغلها في Container داخل شبكة Seq:
docker run -d --name seqcli --network seq_default --entrypoint sleep datalust/seqcli:2026.1.02654 infinity
SEQCLI="docker exec -i seqcli /bin/seqcli/seqcli"
# مفتاح لإدارة Seq من السكربتات
echo 'كلمة-مرور-قوية-هنا' | $SEQCLI apikey create -t lab-admin \
--permissions=Read,Write,Project,Organization,System \
-s http://seq --connect-username admin --connect-password-stdin
# مفتاح للاستقبال فقط لتطبيق الطلبات، يضيف Environment=lab لكل حدث
echo 'كلمة-مرور-قوية-هنا' | $SEQCLI apikey create -t orders-api -p Environment=lab \
--minimum-level Information -s http://seq --connect-username admin --connect-password-stdinكل أمر يطبع المفتاح الجديد فقط، فاحفظه مباشرة. ثم نعرض المفاتيح:
$SEQCLI apikey list -s http://seq -a "$ADMIN"apikey-35 lab-admin
apikey-37 orders-api
apikey-default Unauthenticatedلاحظ السطر الأخير: apikey-default Unauthenticated، وهو يمثل كل حدث يصل بدون مفتاح. وهنا أول ما جربناه: أرسلنا ثلاثة أحداث مرة بدون مفتاح، ومرة بمفتاح غير موجود أصلاً هو wrongkey123، فقبلها Seq في المرتين:
201 {"MinimumLevelAccepted":null}
201 {"MinimumLevelAccepted":null}أي أن أي جهاز يصل إلى المنفذ 5341 يستطيع أن يكتب في سجلاتك ما يشاء، حتى بمفتاح مختلق، وهذا السلوك الافتراضي غير مناسب إطلاقاً للإنتاج. والحل إعداد واحد يمنع الكتابة بدون مفتاح صحيح:
$SEQCLI setting set -n RequireApiKeyForWritingEvents -v True -s http://seq -a "$ADMIN"وبعد الإعداد أعدنا الإرسال نفسه، فرفض Seq الطلبين الأولين وقبل الطلب الذي يحمل مفتاح التطبيق:
401 {"Error":"A valid API key is required for writing events."}
401 {"Error":"A valid API key is required for writing events."}
201 {"MinimumLevelAccepted":"Information"}ولاحظ أن MinimumLevelAccepted أصبح Information، وهو الحد الذي وضعناه في المفتاح، فالمكتبات التي تفهم هذا الرد مثل Serilog تتوقف عن إرسال ما دونه ولا تضيع الشبكة فيه.
403/Forbidden. لذلك تنشأ المفاتيح بالدخول بمستخدم كما في الأوامر أعلاه، وهذا منطقي لأن المفتاح المسرب لا يستطيع أن يولد مفاتيح جديدة تبقى بعد إلغائه.إرسال السجلات من .NET مع Serilog
لنأخذ مثالاً خدمة طلبات صغيرة تكتب حدثاً لكل عملية دفع، وبعض البطاقات تتأخر عند مزود الدفع حتى تنتهي المهلة. ملف المشروع:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.1.0" />
</ItemGroup>
</Project>والكود في Program.cs:
using Serilog;
using Serilog.Context;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", "orders-api")
.WriteTo.Console()
.WriteTo.Seq("http://seq:5341", apiKey: Environment.GetEnvironmentVariable("SEQ_API_KEY"))
.CreateLogger();
var rnd = new Random(7);
for (var i = 1; i <= 200; i++)
{
var orderId = $"ord_{i:D4}";
using (LogContext.PushProperty("RequestId", Guid.NewGuid().ToString("N")))
{
var card = rnd.Next(10) < 4 ? "mada" : "visa";
var ms = card == "mada" ? rnd.Next(1200, 3200) : rnd.Next(50, 250);
if (ms > 2000)
{
try { throw new TimeoutException($"payment provider did not answer within 2000 ms"); }
catch (TimeoutException ex)
{
Log.Error(ex, "Checkout {OrderId} failed with {ErrorCode} after {ElapsedMs} ms for card {CardBrand}", orderId, "PSP_TIMEOUT", ms, card);
}
}
else
{
Log.Information("Checkout {OrderId} completed in {ElapsedMs} ms for card {CardBrand}", orderId, ms, card);
}
}
}
Log.CloseAndFlush();في الكود أعلاه لاحظ التالي:
- الرسالة قالب فيه أسماء بين أقواس، والقيم تمرر بعده، وهذه هي قوالب الرسائل Message Templates. لا تكتب
$"Checkout {orderId} failed"لأن القيمة تذوب في النص فلا تصبح خاصية. Enrich.WithPropertyيضيفApplicationلكل حدث، وLogContext.PushPropertyيضيفRequestIdلكل ما يكتب داخل الطلب، وهذا ما يربط أحداث الطلب الواحد.Log.CloseAndFlush()ضروري في البرامج القصيرة، لأن Serilog يرسل على دفعات Batches، فإذا انتهى البرنامج قبل الدفعة الأخيرة ضاعت.- عنوان الإرسال هو المنفذ 5341، وهو منفذ الاستقبال، أما الواجهة فلا يحتاجها التطبيق.
شغلنا البرنامج داخل Container من mcr.microsoft.com/dotnet/sdk:10.0.400 في شبكة Seq، وآخر ما طبعه على الشاشة:
[01:06:39 ERR] Checkout ord_0200 failed with PSP_TIMEOUT after 3025 ms for card mada
System.TimeoutException: payment provider did not answer within 2000 ms
at Program.<Main>$(String[] args) in /tmp/b/Program.cs:line 22والحدث نفسه كما خزنه Seq، وقد طلبناه بـ seqcli search --json:
{"@t":"2026-10-23T01:06:39.5928136+00:00","@mt":"Checkout {OrderId} failed with {ErrorCode} after {ElapsedMs} ms for card {CardBrand}","@l":"Error","@x":"System.TimeoutException: payment provider did not answer within 2000 ms\n at Program.<Main>$(String[] args) in /tmp/b/Program.cs:line 22","Application":"orders-api","CardBrand":"mada","ElapsedMs":3025,"Environment":"lab","ErrorCode":"PSP_TIMEOUT","OrderId":"ord_0200","RequestId":"e2da227dbd8c4b68b371a2185b521071"}لاحظ أن ElapsedMs رقم وليس نصاً، وأن Environment لم يكتبه التطبيق وإنما أضافه مفتاح API، وأن الاستثناء وصل كاملاً في @x.
من أي لغة: صيغة CLEF عبر HTTP
إذا كان تطبيقك بلغة Python أو Go أو Node.js فلا تحتاج مكتبة خاصة بـ Seq، لأن الاستقبال مجرد طلب HTTP. فالمسار /ingest/clef يستقبل أحداثاً بصيغة CLEF، وهي سطر JSON لكل حدث فيه حقول تبدأ بـ @: @t للوقت وهو الحقل الوحيد الإلزامي، و@m للرسالة أو @mt للقالب، و@l للمستوى، و@x للاستثناء، كما في صفحة إرسال الأحداث. والمثال التالي بمكتبة Python القياسية فقط، ورسالتان منه بالعربية:
import json, os, urllib.request
from datetime import datetime, timezone
URL = os.environ.get("SEQ_URL", "http://seq:5341/ingest/clef")
KEY = os.environ.get("SEQ_API_KEY", "")
now = datetime.now(timezone.utc).isoformat(timespec="milliseconds")
events = [
{"@t": now, "@mt": "Shipping quote for {OrderId} took {ElapsedMs} ms", "OrderId": "ord_0042", "ElapsedMs": 180, "Application": "shipping-worker"},
{"@t": now, "@mt": "تعذر الوصول إلى خدمة الشحن للطلب {OrderId}", "@l": "Error", "OrderId": "ord_0043", "Application": "shipping-worker", "ErrorCode": "SHIPPING_DOWN"},
{"@t": now, "@mt": "فشل الدفع بسبب انتهاء المهلة للطلب {OrderId}", "@l": "Warning", "OrderId": "ord_0044", "Application": "shipping-worker"},
]
body = "\n".join(json.dumps(e, ensure_ascii=False) for e in events).encode("utf-8")
req = urllib.request.Request(URL, data=body, method="POST", headers={"Content-Type": "application/vnd.serilog.clef"})
if KEY:
req.add_header("X-Seq-ApiKey", KEY)
try:
with urllib.request.urlopen(req) as r:
print(r.status, r.read().decode() or "(empty body)")
except urllib.error.HTTPError as e:
print(e.code, e.read().decode())والمخرج مع مفتاح صحيح:
201 {"MinimumLevelAccepted":"Information"}في الكود أعلاه لاحظ التالي:
- الأحداث مفصولة بسطر جديد في طلب واحد، ونوع المحتوى
application/vnd.serilog.clef، وهكذا ترسل الدفعة كاملة في طلب HTTP واحد. - المفتاح في الترويسة Header
X-Seq-ApiKey، ويمكن وضعه في الرابط?apiKey=ولكن الترويسة أفضل لأن الروابط تظهر في سجلات الـ Reverse Proxy. @lنص مثلErrorأوWarning، وإذا غاب اعتبر Seq الحدث معلوماتياً Information.
ويستقبل Seq أيضاً السجلات والتتبع Traces بصيغة OpenTelemetry على المسار /ingest/otlp/v1/logs عبر HTTP/protobuf، وعبر gRPC على HTTPS فقط، كما في صفحة OpenTelemetry. فإذا كان تطبيقك يستخدم OpenTelemetry SDK فوجهه إلى Seq مباشرة بالمفتاح نفسه في الترويسة.
من Container قديم يكتب نصاً: GELF
لنفرض أن لديك مهمة ليلية قديمة لا تستطيع تعديل كودها، وكل ما تفعله أنها تطبع أسطراً إلى stdout. هنا يأتي دور مشغل السجلات GELF في Docker: Docker نفسه يرسل كل سطر بصيغة GELF عبر UDP، ويستقبله الـ Container seq-input-gelf ويحوله إلى Seq عبر HTTP. ننشئ له مفتاحاً، ونضعه في SEQ_GELF_KEY داخل .env، ثم نشغله:
docker compose up -d seq-gelfثم نشغل المهمة القديمة مع مشغل GELF:
docker run --rm --log-driver gelf --log-opt gelf-address=udp://127.0.0.1:12201 --log-opt tag=nightly-report \
alpine:3.22 sh -c 'echo "report started"; echo "ERROR: export to sftp failed, retry 3/3"; echo "تم إنشاء التقرير الليلي"'ووصلت الأسطر الثلاثة إلى Seq ومعها خصائص أضافها Docker، وهذا أحدها:
{"@t":"2026-10-23T01:06:56.0760000+00:00","@mt":"ERROR: export to sftp failed, retry 3/3","@l":"info","Environment":"lab","command":"sh -c echo \"report started\"; …","container_name":"obs3-legacy-job","host":"docker-desktop","image_name":"alpine:3.22","tag":"nightly-report"}لاحظ "@l":"info": السطر يقول ERROR ولكن Seq خزنه معلوماتياً، لأن stdout لا يحمل مستوى، فكل ما يعرفه مشغل GELF هو النص. لذلك فإن إشارة Errors لن تجد هذا السطر، والتنبيه المبني على المستوى لن ينطلق. فإذا اضطررت لهذا الطريق فابحث بالنص (@Message like 'ERROR%')، أو حلل الأسطر في ناقل مثل Vector قبل Seq كما شرحنا في مسار السجل.
GELF_ADDRESS بالشكل 0.0.0.0:12201/udp، وعندما استخدمناها بهذا الشكل امتلأ سجل الـ Container بالخطأ GELF input failed وinvalid socket address syntax ولم يصل شيء. والصيغة التي عملت هي udp://0.0.0.0:12201 كما في ملف README للمستودع.البحث والاستعلام
في Seq طريقتان للسؤال: تعبير بحث Search Expression يرجع الأحداث نفسها، واستعلام بلغة تشبه SQL يرجع جدولاً من الأرقام. والتعبير يكتب في مربع البحث كما هو:
Application = 'orders-api' and @Level = 'Error'
أما الاستعلام فيبدأ بـ select ويستخدم stream مصدراً للأحداث، كما في صفحة SQL Queries. والمثال التالي يحسب عدد العمليات ومتوسط زمنها وأعلى زمن لكل نوع بطاقة:
select count(*) as orders, mean(ElapsedMs) as avg_ms, percentile(ElapsedMs, 95) as p95_ms
from stream
where Application = 'orders-api'
group by CardBrandونتيجته بعد تشغيل البرنامج مرة واحدة (200 عملية):
"CardBrand","orders","avg_ms","p95_ms"
"visa","113","144.7433628318584070796460177","237.5153722811128"
"mada","87","2078.2758620689655172413793103","3011.871780688884"وهنا يظهر الفرق بين السجل النصي والسجل المنظم: من 200 سطر عرفنا في ثانية أن متوسط بطاقة mada نحو ثانيتين مقابل 145 ملي ثانية لـ visa، وعرفنا من استعلام آخر أن الـ 45 فشلاً كلها لبطاقة mada. والشاشة نفسها تعرض الجدول أو رسماً بيانياً منه:

البحث بالعربية
Seq يخزن النص بترميز UTF-8 ويبحث فيه كما هو، فالبحث عن عبارة عربية بين علامتي تنصيص يعمل:
$SEQCLI search -s http://seq -a "$ADMIN" -f '"انتهاء المهلة"' -c 3[2026-10-23T01:06:47.1520000+00:00 WRN] فشل الدفع بسبب انتهاء المهلة للطلب ord_0044
ولكن البحث يطابق الحروف كما هي، فعندما بحثنا عن "المهله" بالهاء بدل التاء المربوطة لم يرجع شيئاً، وكذلك الحال مع الهمزات (أ وا) والألف المقصورة (ى وي)، لأن Seq ليس فيه مطبع للحروف العربية Arabic Normalizer كالذي في محركات البحث النصي. والقاعدة هنا: لا تعتمد على البحث في نص الرسالة العربية، وإنما ضع في الحدث خاصية ثابتة بالإنجليزية مثل ErrorCode = 'PSP_TIMEOUT' وابحث بها، واترك النص العربي للقراءة فقط. وعلى 500 ألف حدث في المختبر رجع الاستعلام message_ar like '%انتهاء المهلة%' بـ 6271 حدثاً، وهو العدد الصحيح تماماً الذي حسبناه من الملف الأصلي.
الإشارات Signals
الإشارة Signal فلتر محفوظ باسم، تختاره بضغطة من القائمة على يمين الشاشة بدل أن تكتب التعبير كل مرة. وتجمع Seq الإشارات بطريقة ذكية كما في صفحة الإشارات: إذا اخترت إشارتين من مجموعة واحدة (تطبيقين مثلاً) أخذ ما يطابق أياً منهما، وإذا اخترت إشارتين من مجموعتين مختلفتين (تطبيق وبيئة) أخذ ما يطابقهما معاً، وتذكر الصفحة أن Seq يبني فهارس Indexes للإشارات فتصبح الاستعلامات التي تستخدمها أسرع على البيانات الكبيرة. أنشأنا ثلاث إشارات بـ seqcli:
$SEQCLI signal create -t 'orders-api' -f "Application = 'orders-api'" -c OrderId -c ElapsedMs -s http://seq -a "$ADMIN"
$SEQCLI signal create -t 'Payment timeouts' -f "ErrorCode = 'PSP_TIMEOUT'" -c OrderId -c CardBrand -s http://seq -a "$ADMIN"
$SEQCLI signal create -t 'Debug noise' -f "@Level in ['Debug', 'Verbose']" -s http://seq -a "$ADMIN"signal-38 orders-api
signal-39 Payment timeouts
signal-40 Debug noiseوالخيار -c يحدد الأعمدة التي تظهر بجانب كل حدث عند اختيار الإشارة. والإشارة الثالثة لن نبحث بها، وإنما سنحذف بها في الخطوة التالية.
سياسات الاحتفاظ Retention
بدون سياسة احتفاظ يبقى كل حدث إلى أن يمتلئ القرص. وفي Seq تحذف سياسة الاحتفاظ الأحداث بعد عمر معين، إما كلها أو ما يطابق إشارة معينة، وهذا يسمح بأن تحتفظ بالأخطاء شهراً وبسجلات Debug يومين فقط:
$SEQCLI retention create --data-source stream --after 30d --delete-all -s http://seq -a "$ADMIN"
$SEQCLI retention create --data-source stream --after 2d --delete signal-40 -s http://seq -a "$ADMIN"
وفي هذه الخطوة لاحظ التالي:
- الخيار
--deleteيأخذ رقم الإشارةsignal-40وليس تعبيراً، وعندما كتبنا التعبير مكانه فشل الأمر بخطأSyntax error … unexpected id `in`. وكذلك يطلب الأمر--data-source streamصراحة في هذا الإصدار. - الإشارة المستخدمة في الاحتفاظ يجب أن تكون مشتركة Shared وليست شخصية.
- الحذف يعمل في الخلفية، فلن ترى المساحة تنخفض فوراً، وتنصح الصفحة بثلاث إلى خمس سياسات كحد أعلى، لأن كل سياسة تستهلك وقتاً من المعالج والقرص.
- يتوقف Seq عن استقبال الأحداث إذا نزلت المساحة الحرة عن حد Minimum free storage space في الإعدادات، فالحذف بالعمر هو ما يمنع الوصول إلى هذا الحد.
التنبيهات Alerts إلى Webhook
التنبيه في Seq استعلام يعمل دورياً على نافذة زمنية، فإذا تحقق شرط having أرسل إشعاراً عبر تطبيق من تطبيقات Seq، كما في صفحة التنبيهات. وسوف نرسل الإشعار إلى Webhook بسيط، فنثبت أولاً تطبيق Seq.App.HttpRequest وننشئ منه نسخة Instance تشير إلى عنوان الـ Webhook:
$SEQCLI app install --package-id Seq.App.HttpRequest -s http://seq -a "$ADMIN"
$SEQCLI appinstance create -t 'Ops webhook' --app hostedapp-45 -p Url=http://hooks.example.com/seq-alert \
-p BodyIsTemplate=True -p MediaType=text/plain \
-p Body=$'Seq alert: {@m}\nTitle: {Alert.Title}\nResults: {Source.Results}\nLink: {Alert.Url}' \
-s http://seq -a "$ADMIN"ثم التنبيه نفسه: إذا زادت حالات انتهاء المهلة لنوع بطاقة واحد عن 5 في دقيقة واحدة، مع إيقاف التكرار عشر دقائق:
$SEQCLI alert create -t 'Payment timeouts spike' --where "ErrorCode = 'PSP_TIMEOUT'" \
--select "count(*) as timeouts" --group-by CardBrand --window 1m --having "timeouts > 5" \
--notification-level Error --suppression-time 10m --notification-app appinstance-46 \
-s http://seq -a "$ADMIN"شغلنا برنامج .NET مرة أخرى، فوصل إلى الـ Webhook بعد نحو دقيقة الطلب التالي:
POST /seq-alert Seq alert: Alert condition triggered by Payment timeouts spike
Title: Payment timeouts spike
Results: [["time","CardBrand","timeouts"],["2026-10-23T01:40:49.3279324Z","mada",45]]
Link: http://localhost:80/#/alerts/alert-32
في التجربة أعلاه لاحظ التالي:
- النتيجة تحمل
madaو45، أي أن الإشعار نفسه يخبرك بنوع البطاقة وعدد الحالات، بفضل--group-by CardBrand. - الرابط بدأ بـ
http://localhost:80لأن هذه التجربة كانت قبل أن نضبطSEQ_API_CANONICALURI، وبعد ضبطه يصبح الرابط بعنوانك الحقيقي. - بدون
Bodyأرسل التطبيق طلباً بجسم فارغ تماماً، أي إشعاراً لا يقول شيئاً، لذلك اكتب القالب دائماً.
والخطأ الذي وقعنا فيه هنا يستحق الذكر: كتبنا في البداية MediaType=text/plain; charset=utf-8، فانطلق التنبيه ولم يصل شيء إلى الـ Webhook. والسبب ظهر داخل Seq نفسه حدثاً بمستوى Error:
The event event-20f63d9a… could not be sent to Ops webhook.
System.FormatException: The format of value 'text/plain; charset=utf-8' is invalid.أي أن التطبيق يريد نوع المحتوى بدون أي معامل، وعندما كتبناه text/plain وصل الإشعار. والدرس هنا: أنشئ إشارة على أخطاء Seq نفسه، لأن فشل الإشعار لا يظهر إلا هناك.
ماذا قسنا عند تحميل 500 ألف حدث؟
حتى نعرف كيف يتصرف Seq مع حجم أقرب للواقع، ولدنا ملفاً فيه 500 ألف حدث JSON موزعة على 12 ساعة لثماني خدمات، منها 3% أخطاء و5% فيها نص عربي، وحجمه 152 MB. ثم أرسلناه إلى Seq بأداة Vector 0.60.0 بعد تحويل الحقول إلى صيغة CLEF، مرتين على جهاز اختبار بثمانية معالجات يشاركه أعمال أخرى: الأولى والجهاز تحت حمل ثقيل، والثانية وهو أهدأ. والجدول التالي يبين الاثنين، لذلك خذ الأرقام على أنها مقياس تقريبي:
| القياس | التشغيل الأول (جهاز مشغول) | التشغيل الثاني (جهاز أهدأ) |
|---|---|---|
| زمن الوصول حتى يصبح الـ 500 ألف قابلاً للبحث | 276 ثانية | 25 ثانية، أي نحو 20 ألف حدث في الثانية |
حجم مجلد الأحداث /data/Stream بعد التحميل مباشرة | 142 MB | 135 MB |
| الحجم نفسه بعد نحو نصف ساعة من الفهرسة في الخلفية | 71 MB | 69 MB، أي أقل من نصف حجم ملف JSON |
| ذاكرة Seq وهو فارغ / بعد التحميل والاستعلامات | 145 MiB / 390 MiB | 80 MiB / 1.2 GiB |
البحث عن حدث واحد بـ request_id | بين 0.4 و1.4 ثانية | بين 0.2 و0.3 ثانية |
عدد الأخطاء لكل خدمة group by service | بين 0.2 و0.4 ثانية | 0.7 ثانية أول مرة ثم 0.01 ثانية |
البحث عن timeout داخل الرسالة (3765 نتيجة) | بين 0.2 و0.9 ثانية | 0.2 ثانية أول مرة ثم 0.01 ثانية |
والنتائج كلها صحيحة مقارنة بالملف الأصلي، فعدد الأخطاء لكل خدمة طابق ما حسبناه بسكربت Python، وكذلك عدد أسطر timeout والعبارة العربية. وفي الجدول ثلاث ملاحظات: الأولى أن زمن الإدخال تغير عشرة أضعاف بحسب حمل الجهاز، لذلك قس على سيرفرك قبل أن تعتمد على أي رقم. والثانية أن الحجم على القرص انخفض إلى النصف بعد الفهرسة، فلا تحكم على حاجتك من التخزين في أول ساعة. والثالثة أن الذاكرة ارتفعت إلى 1.2 GiB لأن Seq يستخدم ما يجده من ذاكرة لتسريع الاستعلامات المكررة، وهذا ما تقصده متطلبات النظام عندما تنصح بسيرفر مخصص له.
الوصول عبر نطاق مع TLS
الواجهة يجب ألا تفتح على الإنترنت بدون HTTPS، لأن كلمة المرور والجلسة تمران فيها. والمثال التالي بـ Caddy، وهو يصدر الشهادة تلقائياً، ويفصل الواجهة عن الاستقبال على نطاقين فرعيين:
seq.example.com {
reverse_proxy 127.0.0.1:8080
}
seq-ingest.example.com {
reverse_proxy 127.0.0.1:5341
}بعد ذلك تكتب التطبيقات خارج السيرفر https://seq-ingest.example.com عنواناً للإرسال، ويبقى SEQ_API_CANONICALURI مساوياً لـ https://seq.example.com. وتفاصيل reverse_proxy في توثيق Caddy. ولم نختبر إصدار الشهادة هنا لأنه يحتاج نطاقاً عاماً، أما بقية الأجزاء فقد عملت محلياً كما وصفنا.
التحقق من النجاح
curl -s http://127.0.0.1:8080/healthيرجع"status":"healthy"، وdocker compose psيعرض Seq بحالةhealthy.- إرسال حدث بدون مفتاح يرجع
401، ومع مفتاح التطبيق يرجع201. $SEQCLI query -q "select count(*) as events from stream group by Application"يعرض كل تطبيق ترسل منه.- تنبيه تجريبي بشرط سهل التحقق يصل إلى الـ Webhook أو البريد، ولا يظهر في Seq حدث
could not be sent to. - شاشة Storage تعرض سياسات الاحتفاظ التي أنشأتها.
النسخ الاحتياطي والاستعادة
هنا نقطة يغفل عنها كثيرون: النسخة الاحتياطية في Seq تحفظ الإعدادات فقط، أي المستخدمين ومفاتيح API والإشارات والتنبيهات والتطبيقات، ولا تحفظ الأحداث، كما تنص صفحة النسخ الاحتياطي. ويكتب Seq نسخة كل ليلة في /data/Backups ويحتفظ بآخر سبع. وفي المختبر كان حجم النسخة 15 KB:
docker exec seq ls -la /data/Backups-rw-r--r-- 1 root root 15024 Oct 23 02:07 seq_20261023.seqbacوقد يتساءل البعض: لماذا لا أشغل الأمر seqsvr backup متى شئت؟ جربناه والسيرفر يعمل، فرفض لأن مخزن الإعدادات مقفل:
Could not create backup: Flare native storage failed (IOError), error attempting to lock the store.لذلك اعتمد على النسخة الليلية، ووقتها يضبط بالإعداد BackupUtcTimeOfDay بتوقيت UTC. وانسخ الملف خارج السيرفر ضمن نسخك الاحتياطية المعتادة. أما الاستعادة على Volume جديد فقد اختبرناها كما يلي، ويجب أن يكون الملف داخل Backups/ في الـ Volume الجديد:
docker cp seq:/data/Backups/seq_20261023.seqbac .
docker volume create seq-restore
docker run --rm -v seq-restore:/data -v "$PWD:/in:ro" --entrypoint sh datalust/seq:2026.1.17182 \
-c 'mkdir -p /data/Backups && cp /in/seq_20261023.seqbac /data/Backups/'
docker run --rm -v seq-restore:/data datalust/seq:2026.1.17182 \
restore -y -s /data --secret-key="$SEQ_SECRET_KEY" --backup=/data/Backups/seq_20261023.seqbacRestoration is complete. Any Seq app instances must be explicitly re-enabled.ثم شغلنا Seq على الـ Volume المستعاد، فرجعت الإشارات الثلاث والتنبيه، وعمل مفتاح API القديم نفسه، وكان عدد الأحداث صفراً كما هو متوقع. ولاحظ أن تطبيقات الإشعار تعود معطلة حتى لا تنطلق التنبيهات من سيرفر الاختبار، فأعد تفعيلها بنفسك. وإذا أردت حفظ الأحداث نفسها فاستخدم تطبيق أرشفة مثل JSON Archive الذي تذكره الصفحة، أو انسخ الـ Volume كاملاً والسيرفر متوقف.
SEQ_SECRET_KEY الصحيح لا تفيدك النسخة الاحتياطية شيئاً، وأمر الاستعادة يذكر أن الملف يجب أن يكون من الإصدار نفسه من Seq الذي تستعيد إليه، لذلك احفظ مع كل نسخة رقم الإصدار.التحديث إلى إصدار أحدث
- تأكد أن لديك نسخة ليلية حديثة من
/data/Backupsومعها المفتاح، لأن صفحة التحديث تطلب ذلك وتنصح بتجنب الرجوع لإصدار أقدم Downgrade. - اقرأ ملاحظات الإصدار، ثم غير الـ Tag في
docker-compose.ymlإلى الإصدار الجديد بالرقم الكامل، ولا تستخدمlatest. - نفذ
docker compose pull seq && docker compose up -d --wait seq، والـ Volume نفسه يبقى. - حدث تطبيقات Seq من Settings ثم Apps، لأن الصفحة تطلب تحديثها بعد تحديث السيرفر.
مشكلات شائعة وحلولها
401/Unauthorized. Please log inمن seqcli رغم صحة كلمة المرور: إذا كانSEQ_API_CANONICALURIيبدأ بـhttps://فإن Seq يضع على كوكي الجلسة Session Cookie العلامةsecure، فلا يرسلها seqcli عندما يتصل بـhttp://seqداخل الشبكة. ورأينا ذلك في ترويسةSet-Cookieنفسها. الحل أن تتصل بالعنوان الحقيقي عبر HTTPS، أو أن تستخدم مفتاح API بدل الدخول بالمستخدم.- الأحداث تصل من أي مكان: فعل
RequireApiKeyForWritingEventsكما في مفاتيح API، وإلا قبل Seq حتى المفاتيح المختلقة. - Vector يرسل والسيرفر يرد 401: في Vector 0.60.0 أرسل الناقل الترويسة
X-Seq-ApiKeyبالنص الحرفي${SEQ_API_KEY}بدل قيمة المتغير، ورأينا ذلك عندما وجهناه إلى سيرفر يطبع الترويسات. فإذا ظهر الخطأDefault retry strategy: Unauthorizedفتحقق مما يصل فعلاً، أو اكتب المفتاح في ملف الإعداد عند التشغيل. - مدخل GELF يكرر
invalid socket address syntax: اكتبGELF_ADDRESS=udp://0.0.0.0:12201بالبروتوكول في البداية. - الإشعار لا يصل والتنبيه انطلق: ابحث في Seq عن
could not be sent to، فسبب الفشل يكون هناك بالتفصيل. - البحث العربي لا يجد ما تتوقعه: اختلاف الهمزات أو التاء المربوطة يكفي ليفشل البحث، فابحث بخاصية ثابتة بدل النص.
- السطر يقول ERROR ومستواه info: stdout لا يحمل مستوى، فاكتب السجل منظماً أو حلله في ناقل قبل Seq.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- Seq سيرفر واحد للسجلات المنظمة، يعمل خلال دقائق ويبحث بالخصائص بلغة تشبه SQL، وهو الأنسب لفريق .NET أو أي فريق يكتب أحداثاً منظمة ويريد أداة واحدة للبحث والتنبيه.
- ليس مفتوح المصدر، والخطة المجانية لمستخدم واحد، والفريق يبدأ من 1,090 دولاراً سنوياً لعشرة مستخدمين بحسب صفحة الأسعار في أكتوبر 2026.
- فعل
RequireApiKeyForWritingEventsمن اليوم الأول، فقد قبل Seq في المختبر أحداثاً بمفتاح مختلق قبل تفعيله. - أي لغة ترسل إليه بصيغة CLEF عبر HTTP، وContainer قديم عبر GELF ولكن بدون مستويات، وOpenTelemetry عبر OTLP.
- البحث العربي يطابق الحروف كما هي، فاجعل الخصائص الثابتة هي ما تبحث به.
- 500 ألف حدث شغلت قرابة 70 MB بعد الفهرسة، والاستعلامات عليها رجعت في أقل من ثانية ونصف حتى والجهاز مشغول.
- النسخة الاحتياطية للإعدادات فقط وتحتاج المفتاح نفسه والإصدار نفسه، والأحداث تحتاج خطة منفصلة.
تدريب عملي
- أنشئ تنبيهاً على متوسط
ElapsedMsبدل العدد:--select "mean(ElapsedMs) as avg_ms"و--having "avg_ms > 1500"، ثم شغل البرنامج وراقب الـ Webhook. - أرسل حدثاً بمستوى
Debugبمفتاح التطبيق، ولاحظ أن Seq يرفضه بسبب--minimum-level Informationفي المفتاح، ثم غير الحد وأعد الإرسال.
وفي المقال القادم بإذن الله نقارن Seq وLoki وGraylog وOpenSearch بالأرقام على البيانات نفسها.
دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.
دليلسجلات تساعدك فعلاً: من «حدث خطأ» إلى سبب المشكلةكيف تكتب سجلات تجيب عن أسئلة الحادثة، بتحقيق في عطل حقيقي بسجلات سيئة ثم بحدث واسع Wide Event لكل طلب، مع قياس الفرق.
دليلمسار السجل Log Pipeline من التطبيق إلى البحث والتنبيهكيف ينتقل السجل من التطبيق إلى المخزن عبر الناقل Shipper، وكيف تخفي البيانات الشخصية وتوجه الأخطاء وتحمي السجلات إذا توقفت الوجهة.
دليلجمع السجلات Logs مركزياً مع Grafana Lokiكيف تشغل Grafana Loki مع Alloy وGrafana، وتختار التسميات Labels التي لا توقف الإدخال، ثم تبحث وتنبه وتحذف بلغة LogQL.
دليلالسجلات المركزية Logs مع Graylog وOpenSearchكيف تثبت Graylog فوق OpenSearch، وتوجه الرسائل بال Streams وتخفي بياناتها بال Pipelines وتنبه منها، وما الذي لا يجده بحثه العربي.
دليلOpenSearch: البديل المفتوح لـ Elasticsearch وقصة الترخيصلماذا ولد OpenSearch من Elasticsearch، وكيف تشغله على سيرفرك بصلاحيات دقيقة وتنبيهات حقيقية على سجلاتك.سجل التحديثات
- أكتوبر 2026: كتابة الدليل واختباره على Seq 2026.1.17182 مع seqcli 2026.1.02654 وseq-input-gelf 4.0.02323 وSerilog.Sinks.Seq 9.1.0 على .NET 10، ومراجعة صفحة الأسعار في 22 أكتوبر 2026.