الحدّ الأدنى لمتطلّبات logger مصنوعة ومتطلّبات اختبار التكامل

· آخر تحديث: · · Windows Development, Logging, Integration Testing, Test Design, Reliability

سجل التعديلات (4 تحديثات، آخر تحديث 3 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22243014)
أُصلِحت بقايا يابانية في تعليقات إعادة المسح ومبرّر JSON وترخيص Job. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240900)
أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621473)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

小村 豪 (2026). الحدّ الأدنى لمتطلّبات logger مصنوعة ومتطلّبات اختبار التكامل. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621473 https://comcomponent.com/ar/blog/2026/04/02/001-custom-logger-minimum-requirements-and-integration-test-checklist/

DOI (أحدث نسخة)
10.5281/zenodo.21621473
DOI (هذه النسخة)
10.5281/zenodo.22279921

إن أمكن استخدام logging framework جاهز، فذلك أأمن. ومع ذلك ثمّة مواضع لا يُتجنَّب فيها صنع logger بسبب قيود جانب التطبيق أو ظروف التشغيل. وما يسهل الحيرة فيه أوّلاً هو إلى أيّ حدّ يُنفَّذ حتى يصير التصميم «لا فضفاضاً أكثر من اللازم، ولا ثقيلاً أكثر من اللازم».

ضيّق هذه المقالة الهدف على سجلّ التطبيق لتحقيق الأعطال. لا نحمل دفعة واحدة أثر التدقيق، ولا التتبّع الموزّع، ولا منصّة المقاييس، ولا التجميع السحابي. نعرّف أوّلاً تكويناً أدنى يفيد في الميدان، ثم نرتّب زوايا اختبار التكامل حتى يمكن الوثوق بذلك التكوين حقاً.

القارئ المستهدف وافتراضات هذه المقالة

البند المحتوى
القارئ المستهدف المطوّر الذي يضمّن سجلّ تشخيص مصنوع في تطبيق أعمال أو أداة. نفترض فريقاً صغيراً بلا مسؤول متفرّغ لمنصّة السجلّ
نطاق تطبيق التصميم لا يعتمد على اللغة. ما دامت البيئة قادرة على الإلحاق بملفّ، فالحكم نفسه في C# وفي C++
أمثلة الشيفرة نعرضها بـ C# 12 / .NET 8 وPowerShell 7. في لغة أخرى يمكن الاستبدال إذا اتُّخذ الحكم نفسه بالترتيب نفسه
السجلّ المستهدف سجلّ التشخيص المستخدم لعزل أعطال التطبيق
خارج النطاق أثر التدقيق، التتبّع الموزّع، منصّة المقاييس، التجميع السحابي

المصطلحات في هذه المقالة

قبل دخول حديث التصميم نلخّص الكلمات التي تظهر بلا شرح في الفصول اللاحقة.

المصطلح المعنى
JSON Lines (.jsonl) تنسيق نصّ يكتب قيمة JSON واحدة في السطر، ويفصل بأسطر جديدة (\n). ترميز الأحرف UTF-8، ومقرّر ألّا يُلحق BOM1
fields منظَّمة وعاء يحمل القيم المراد البحث عنها أزواجاً من مفتاح وقيمة، منفصلاً عن جملة message. شكل مثل {"file":"orders.csv","row":128}
single writer تصميم يجعل من يكتب فعلاً في الملفّ موضعاً واحداً (خيطاً واحداً). مهما بلغ عدد خيوط جانب الاستدعاء، يُضيَّق منفذ الكتابة إلى واحد
bounded queue طابور بحدّ أعلى. جانب الاستدعاء يضع في الطابور ويعود، والكتابة يقوم بها جانب single writer. لأنّ ثمّة حدّاً أعلى يلزم تقرير سياسة الامتلاء
drain (الاستنزاف) عند الإنهاء، كتابة السجلّات المتبقية في الطابور حتى آخرها. معالجة تمنع «وُضع ثم سقط واختفى»
flush (التفريغ) إخراج المخزن في الذاكرة إلى الملفّ الفعلي. السجلّ الذي لم يمرّ هنا يختفي عند الإنهاء غير الطبيعي
الدوران (rotation) التبديل إلى ملفّ جديد حين يكبر الملفّ أو يتغيّر التاريخ
الاحتفاظ (retention) الحدّ الأعلى لعدد ملفّات السجلّ القديمة أو عدد الأيام التي تُبقى

أوّلاً تحقّق من خيار «عدم الصنع»

كما كُتب في المقدّمة، إن أمكن استخدام logging framework جاهز فذلك أأمن. نذكر أسماء ملموسة حتى يمكن الحكم. إن كفت هنا، فلا حاجة إلى قراءة بقية المقالة.

البيئة الخيار ما يأتي معه من البداية
.NET Microsoft.Extensions.Logging واجهة ILogger القياسية في .NET. آلية مستويات السجلّ (من Trace إلى Critical)، والفئة، وموفّر يُستبدل به موضع الإخراج. تدخل مرجعاً ضمنياً في كثير من .NET SDK2
.NET Serilog سجلّ تشخيص يفترض أحداثاً منظَّمة. يسمّي وسائط قالب الرسالة، ويحتفظ بتلك القيمة خاصّية للحدث3
.NET NLog يدعم السجلّ المنظَّم والتقليدي معاً. في تنسيق الإخراج تخطيط JSON، وإخراج الملفّ يحمل تسمية تلقائية وأرشفة4
C++ spdlog مكتبة سجلّ تُستخدم من C++11 فصاعداً. إخراج ملفّ rotating يتبدّل بالحجم، وdaily يتبدّل بالتاريخ5

حين ثمّة ظرف يمنع استخدامها (لا يمكن زيادة الاعتماد، أو بيئة التشغيل محدودة، أو قيد في الشيفرة القائمة، إلخ)، يظهر أثر المتطلّبات الدنيا بعد هذا.

الجواب أوّلاً

النقاط التي يُراد ضبطها في الإصدار الأوّل كالتالي.

  • التنسيق JSON Lines بترميز UTF-8
  • لا يُكسر سجلّ واحد لكلّ سطر
  • الحقول الإلزامية الوقت والمستوى والفئة والرسالة وfields منظَّمة وsessionId وprocessId
  • الأساس عمليّة واحدة وملفّ واحد
  • إن انخفض الحمل فالكتابة المتزامنة، وإن كثر الإخراج فـ single writer + bounded queue
  • نفّذ flush متزامناً لـ Error / Critical وبدء الجلسة ونهايتها
  • أدخل الدوران والاحتفاظ من v1
  • إن تعذّر موضع الحفظ، لا تهرب صامتاً إلى موضع آخر

إذا ضُيِّق إلى هذا القدر، صعب الانهيار في التنفيذ وفي التشغيل معاً.

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 21، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

ضيّق نطاق الهدف أوّلاً

ما يسهّل صعوبة logger مصنوعة هو محاولة معالجة كلّ شيء من البداية. إن جُمعت سجلّات التشخيص وسجلّ التدقيق وقياس الأداء والتتبّع الموزّع وتحليل سلوك المستخدم في آلية واحدة، زادت المتطلّبات دفعة واحدة.

هدف هذه المرّة سجلّ التشخيص المستخدم لعزل أعطال التطبيق. أي نؤثر القدرة على تتبّع «متى» و«في أيّ معالجة» و«ماذا حدث» و«أيّ سياق كان عندئذٍ» لاحقاً. بالتضييق على هذا وحده يسهل حكم التصميم الأوّل كثيراً.

المتطلّبات اللازمة في الحدّ الأدنى

1. التنسيق UTF-8 JSON Lines

يمكن الإبقاء على السجلّ بلصق نصّ عادي أيضاً، لكن يصعب التعامل الآلي معه لاحقاً. وعلى العكس، إن بُدئ بتنسيق ثنائي خاص ثقيل، سقطت قابلية الرصد في التشغيل.

الوسط السهل التعامل هو JSON Lines بترميز UTF-8. إذا كان السطر الواحد سجلاً واحداً، سهل القراءة كنصّ أيضاً، وسهل التحليل لاحقاً من النصوص والأدوات. وإن انقطعت الكتابة في الوسط، يسهل أيضاً عزل أيّ سطر انكسر، وهذا يناسب العمل.

ما هو مقرّر كتنسيق ثلاث نقاط فقط.1

  • السطر الواحد قيمة JSON واحدة صالحة (لا تُدرَج أسطر فارغة)
  • فاصل السطر \n
  • ترميز الأحرف UTF-8. لا يجوز إلحاق BOM (U+FEFF)

الامتداد .jsonl عرف. حظر BOM سهل التفويت رغم أثره. إن كُتب بـ BOM فشل تحليل السطر الأوّل وحده في أدوات أخرى، وظهر بشكل يصعب فهمه: «السطر الأوّل وحده منكسر».

2. ثبّت الحقول الإلزامية أوّلاً

الحقول التي يُراد جمعها في الحدّ الأدنى هذه السبعة.

  • timestamp
  • level
  • category
  • message
  • fields
  • sessionId
  • processId

سجلّ واحد فعلي يصير مثلاً كالتالي (يُطوى لضيق الصفحة، لكن على الملفّ الفعلي سطر واحد بلا سطر جديد).

{"ts":"2026-04-02T01:15:03.4821567Z","level":"Error","category":"import","message":"فشل الاستيراد","fields":{"file":"orders.csv","row":128,"reason":"date parse failed"},"sessionId":"20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d","pid":8412}

إذا صُفّ ما قبل الجلسة نفسها وما بعدها، بدا كالتالي.

{"ts":"2026-04-02T01:15:00.1002233Z","level":"Info","category":"startup","message":"بدأ التطبيق","fields":{"version":"1.4.2"},"sessionId":"20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d","pid":8412}
{"ts":"2026-04-02T01:15:03.4821567Z","level":"Error","category":"import","message":"فشل الاستيراد","fields":{"file":"orders.csv","row":128,"reason":"date parse failed"},"sessionId":"20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d","pid":8412}
{"ts":"2026-04-02T01:15:03.9900011Z","level":"Info","category":"shutdown","message":"جارٍ إنهاء التطبيق","fields":{"exitCode":1},"sessionId":"20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d","pid":8412}

يجوز قصر اسم المفتاح، لكن الأهمّ ألّا تغيّره بعد أن تقرّره. إن اختلط في الوسط ts وtimestamp، صار نصّ التحليل الذي يُكتب لاحقاً مزعجاً دفعة واحدة.

إن جُعل السجلّ سلسلة message وحدها، ضاق الأمر لاحقاً حين تزيد شروط البحث. وعلى العكس إن زيدت الحقول أكثر من اللازم، ارتفع عبء جانب الاستدعاء فجأة. أأمن أن تُثبَّت أوّلاً على هذا القدر، وأن يُنظر في الإضافة بعد أن تصير لازمة حقاً.

ماذا يُدخل في sessionId

ما دام مذكوراً في الحقول الإلزامية، يلزم تقرير وحدة sessionId. في هذه المقالة بدء واحد للعمليّة جلسة واحدة. ليست جلسة ولوج المستخدم، ولا «صفقة» في الأعمال.

بهذا التعريف يمكن التالي.

  • استخراج سجلّات بدء واحد مجمّعة
  • حتى إن انفصلت الملفّات بالدوران، يمكن لاحقاً وصل سجلّات البدء نفسه
  • يمكن عزل عطلين وقعا صباحاً ومساء على الجهاز نفسه دون خلط

الترقيم يُقرَّر مرّة واحدة عند بدء العمليّة، ويُعاد استخدامه حتى تنتهي تلك العمليّة. تكفي إحدى الطريقتين التاليتين.

الطريقة مثال الموضع المناسب
وقت البدء + معرّف العمليّة + GUID 20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d هذا الافتراضي. الرأس يقرؤه الإنسان، والذيل يمنع التصادم
UUID (GUID) وحده 9f0a1c72-3b58-4f2a-9a2e-6e7c1f0d55b1 جمع سجلّات أجهزة متعدّدة لاحقاً في موضع واحد. لا حاجة إلى القراءة بالعين

لا تكتفِ بوقت البدء + معرّف العمليّة وحدهما. processId يعيد نظام التشغيل استخدامه. عند إعادة البدء في الثانية نفسها في حلقة انهيار، أو عند رجوع الزمن المحلي، قد يصير sessionId مطابقاً تماماً للمرّة السابقة. إن استُخدمت القيمة نفسها في اسم الملفّ أيضاً، اختلطت سجلّات بدءين مختلفين في ملفّ واحد لحظة الفتح بوضع الإلحاق، وانهار افتراض «بدء واحد = جلسة واحدة» صامتاً. وطريقة الكسر هادئة، فلا تُلاحظ لاحقاً.

حمل processId حقلاً منفصلاً أيضاً للتفريق: sessionId لـ«أيّ بدء»، وprocessId لـ«الكيان على نظام التشغيل عندئذٍ».

3. اجعل الأساس عمليّة واحدة وملفّاً واحداً

تصميم الإلحاق بالملفّ نفسه من عمليّات متعدّدة يكثّر عوامل الحوادث فوق ما يبدو. لأنّ الاستبعاد المتبادل والكتابة الجزئية وتوقيت الدوران والتعامل عند الإنهاء غير الطبيعي تصعب دفعة واحدة.

اجعل أوّلاً عمليّة واحدة وملفّاً واحداً أساساً. إن أردت جمع عمليّات متعدّدة، أأمن التجميع لاحقاً، أو إقامة عمليّة تجميع مخصّصة صراحة.

4. فرّق استراتيجية الكتابة حسب الحمل

في مرحلة حجم السجلّ المنخفض أوضح الكتابة المتزامنة وأسهل لتحقيق الأعطال. إن فُرض عدم التزامن بلا داعٍ، سهل فقدان السجلّ قبل الإنهاء مباشرة، أو غموض شرط flush عند الاستثناء.

ومن جهة أخرى، إن كثر الحجم وصار I/O المتزامن عنق زجاجة، يُعتمد single writer + bounded queue. أي شكل جانب الاستدعاء يضع في طابور بحدّ أعلى ويعود، ومن يكتب في الملفّ موضع واحد فقط. هذه الفكرة نفسها ليست نادرة، وفي إرشاد تصميم السجلّ في .NET أيضاً يُرشد إلى عدم الكتابة مباشرة في موضع حفظ بطيء، بل الوضع المتزامن في طابور بالذاكرة والإرسال بمعالجة خلفية.2

المهمّ عندئذٍ تقرير سياسة امتلاء الطابور مسبقاً. لا تُغمض هل تُرمى السجلّات القديمة، أم تُسقط الجديدة، أم تُخرج تحذيراً.

5. قرّر شرط flush

Error وCritical، ثم سجلّ بدء الجلسة ونهايتها، إن بقيت بـ flush متزامن أعانت تحقيق العطل. إن فُرِّغ كلّ Info العادي أيضاً صار الأمر بطيئاً، لذلك واقعي ألّا تُعامل الكلّ المعاملة نفسها.

6. أدخل الدوران والاحتفاظ من v1

كثيراً ما يُظنّ الدوران «يمكن إدخاله لاحقاً»، لكنّه وظيفة تضيق فجأة بعد دخول التشغيل. أيّاً كانت الطريقة، بالحجم أو يومياً أو عند كلّ بدء، ينبغي على الأقلّ أن تكون حالة «لا يزيد بلا نهاية» و«كم يُبقى» مقرّرة.

7. عند فشل الحفظ لا تحفظ بديلاً من تلقاء نفسك

تصميم الكتابة صامتاً في موضع آخر حين يتعذّر موضع حفظ السجلّ يصعّب التحقيق لاحقاً. يكفي أن لا يجد مسؤول التشغيل السجلّ في «الموضع الذي يُفترض وجوده» حتى يتأخّر أوّل تحرّك للتعامل مع العطل.

إن تعذّر الحفظ، أخرج الفشل بوسيلة ظاهرة صراحة: إشعار جانب التطبيق، أو سجلّ الأحداث، أو الخطأ القياسي. على الأقلّ ينبغي تجنّب حالة «لا يُعرف أين ذهب».

صورة التكوين الأدنى لـ v1

في الإصدار الأوّل كثيراً ما يكفي هذا القدر.

  • UTF-8 JSON Lines
  • عمليّة واحدة وملفّ واحد
  • اسم ملفّ بوحدة الجلسة
  • دوران قائم على الحجم أو على وحدة البدء
  • حدّ أعلى لعدد ما يُحتفظ به
  • flush متزامن لـ Error / Critical
  • واجهة تقبل fields منظَّمة

ما فوق هذه الوظائف أسهل صيانة في النتيجة أن يُضاف بعد أن يظهر في التشغيل الفعلي «ما ضاق حقاً».

كيف يصير جزء الكتابة في v1 إن كُتب بـ C#

من المتطلّبات أعلاه، إذا نُفّذ التنسيق والحقول الإلزامية وsingle writer وشرط flush فقط، فهذا حجم الشيفرة (C# 12 / .NET 8). الدوران والاحتفاظ مفترض إضافتهما في المرحلة التالية، فلم يُدخلا عمداً.

using System.Text;
using System.Text.Json;

public sealed class JsonLinesLogger : IDisposable
{
    // أخرج النص كما هو (بما فيه غير ASCII). المرمِّز الافتراضي يحوّل غير ASCII كلّه إلى \uXXXX
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    };

    private static readonly IReadOnlyDictionary<string, object?> NoFields =
        new Dictionary<string, object?>();

    private readonly object _gate = new();      // احمِ single writer بهذا القفل
    private readonly StreamWriter _writer;
    private readonly string _sessionId;
    private readonly int _processId = Environment.ProcessId;

    public JsonLinesLogger(string path, string sessionId)
    {
        _sessionId = sessionId;

        // JSON Lines يمنع BOM، فصرِّح بـ UTF-8 بلا BOM
        _writer = new StreamWriter(
            new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read),
            new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
    }

    public void Write(
        string level,
        string category,
        string message,
        IReadOnlyDictionary<string, object?>? fields = null)
    {
        var record = new Dictionary<string, object?>
        {
            ["ts"] = DateTimeOffset.UtcNow.ToString("o"),
            ["level"] = level,
            ["category"] = category,
            ["message"] = message,
            ["fields"] = fields ?? NoFields,   // null にせず、常に 7 項目そろえる
            ["sessionId"] = _sessionId,
            ["pid"] = _processId,
        };

        // 改行を含む message が来ても 1 行が壊れないよう、JSON 化してから書く
        var line = JsonSerializer.Serialize(record, JsonOptions);

        lock (_gate)
        {
            _writer.WriteLine(line);
            if (level is "Error" or "Critical")
            {
                _writer.Flush();   // 重大なログだけ同期 flush する
            }
        }
    }

    public void Dispose()
    {
        lock (_gate)
        {
            _writer.Flush();   // 正常終了時に必ず書き切る
            _writer.Dispose();
        }
    }
}

جانب الاستدعاء يصير كالتالي. sessionId يُقرَّر مرّة واحدة عند البدء، وتُستخدم القيمة نفسها في اسم الملفّ أيضاً.

// استخدم JsonLinesLogger المذكور أعلاه
var startedAt = DateTimeOffset.Now;

// الوقت وPID وحدهما لا يكفيان. عند إعادة التشغيل في الثانية نفسها في حلقة انهيار،
// أو عند رجوع الزمن المحلّي، قد يُجمع PID الذي أعاد Windows استخدامه
// فيصير المعرّف مطابقاً للمرّة السابقة. JsonLinesLogger يفتح بـ FileMode.Append،
// فتختلط عندها سجلّات بدءين مختلفين في ملفّ واحد،
// وينهار افتراض «بدء واحد = جلسة واحدة» صامتاً
var sessionId = $"{startedAt:yyyyMMdd-HHmmss}-{Environment.ProcessId}-{Guid.NewGuid():N}";
var logDir = @"C:\ProgramData\MyApp\logs";
Directory.CreateDirectory(logDir);

using var logger = new JsonLinesLogger(Path.Combine(logDir, $"app-{sessionId}.jsonl"), sessionId);

logger.Write("Info", "startup", "بدأ التطبيق",
    new Dictionary<string, object?> { ["version"] = "1.4.2" });

logger.Write("Error", "import", "فشل الاستيراد",
    new Dictionary<string, object?> { ["file"] = "orders.csv", ["row"] = 128 });

شيفرة قصيرة، لكن ما قُرّر داخل كلّه.

ما قُرّر أين يعمل
UTF-8 بلا BOM UTF8Encoding(encoderShouldEmitUTF8Identifier: false)
عدم تحويل اليابانية إلى \uXXXX JavaScriptEncoder.UnsafeRelaxedJsonEscaping6
سجلّ واحد لكلّ سطر لا يُلصق message مباشرة، بل يُكتب ناتج JsonSerializer.Serialize بـ WriteLine
إدخال الحقول الإلزامية السبعة في كلّ مرّة حتى إن لم تُحدَّد fields تُدخل NoFields فلا تُنقص
single writer لا يُمسّ _writer إلا داخل lock (_gate)
شرط flush Flush() فوري عند Error / Critical فقط، وعند الإنهاء حتماً Flush() في Dispose()

UnsafeRelaxedJsonEscaping لا يهرّب < > & '، لذلك لا تضمّن هذا الإخراج كما هو في صفحة HTML أو عنصر script.6 اقتصر الاستخدام على القراءة كملفّ سجلّ.

إخفاقات شائعة ينبغي تجنّبها

نذكر أيضاً أمثلة نموذجية يُراد تجنّبها.

  • حشر الكلّ في سلسلة message
  • مشاركة الملفّ نفسه بين عمليّات متعدّدة
  • جعل الكلّ غير متزامن دون تقرير شرط flush
  • تأجيل الدوران والاحتفاظ
  • الهروب صامتاً إلى مجلّد آخر عند فشل الحفظ
  • إدخال النقل عبر الشبكة أو الحفظ في قاعدة محلية حتى في v1

كلّها تبدو مريحة للوهلة الأولى، لكنّها بنود تسهّل تثقيل العزل أو التشغيل.

فكّر في اختبار التكامل بملفّ حقيقي وخيط حقيقي وعمليّة حقيقية

logger مكوّن يصعب الاطمئنان إليه باختبار الوحدة وحده. حتى إن تُحقّق من تنسيق السلاسل أو تحويل JSON فقط، ما يصير مشكلة في التشغيل الفعلي هو I/O والتوازي والدوران وflush عند الإنهاء وأخطاء الصلاحيّات.

لذلك يصير في اختبار التكامل التحقّق باستخدام ملفّ حقيقي وخيط حقيقي، وإن لزم عمليّة حقيقية. على الأقلّ يُراد تجنّب حالة «يمرّ عادة، لكن لا يُوثق به عند العطل».

بنود اختبار تكامل يُراد تمريرها

سلامة الكتابة المنفردة

  • هل السطر الواحد سجلّ JSON واحد
  • هل يمكن إعادة القراءة بـ UTF-8
  • هل تدخل الحقول الإلزامية في كلّ مرّة
  • هل لم ينكسر إلى أسطر متعدّدة باختلاط سطر جديد

التنفيذ المتوازي داخل العمليّة نفسها

  • هل لا تنكسر السجلّات حتى عند الكتابة المتزامنة من خيوط متعدّدة
  • هل لا زيادة ولا نقص في عدد السجلّات
  • عند استخدام queue هل سياسة الترتيب أو الفقد وفق المواصفة

سلوك flush والإنهاء

  • هل ينعكس Error / Critical فوراً
  • هل يفرغ ما في queue عند الإنهاء السليم
  • هل يبقى سجلّ الإنهاء اللازم حتى على مسار قريب من إنهاء باستثناء

الدوران والاحتفاظ

  • هل يُبدَّل إلى ملفّ جديد إذا استُوفي شرط الدوران
  • هل تُحذف الملفّات القديمة فوق حدّ الاحتفاظ وفق المواصفة
  • هل لا ينكسر سطر JSON حتى قبل الدوران وبعده مباشرة

الحالات الشاذّة

  • التعامل حين لا يوجد دليل موضع الحفظ
  • التعامل حين لا صلاحيّة كتابة
  • الإشعار أو القيمة المعادة عند الفشل بما يعادل امتلاء القرص
  • السلوك عند امتلاء queue

التعامل مع عمليّات متعدّدة

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

كيف يُكشف «الكسر»

صفّ الزوايا وحده لا يكفي لكتابة الاختبار. من البنود أعلاه، ما يسهل الحيرة في التنفيذ هو كيف يُحكم أنّ «السجلّ غير منكسر». العين لا تكفي قطعاً، لذلك نتحقّق آلياً من الثلاثة التالية.

ما يُنظر إليه طريقة الحكم الكسر الذي يُوجد بهذا
عدد الأسطر هل يطابق عدد ما كُتب عدد أسطر الملفّ فقد، كتابة مزدوجة، تسرّب drain
كلّ سطر هل يمكن تحليل كلّ الأسطر منفردة كـ JSON اختلاط سطر جديد، مقاطعة الكتابة، اختلاط BOM
كلّ سجلّ هل تكتمل الحقول الإلزامية السبعة في كلّ مرّة نسيان حقل، اختلاط null

«نظرت إلى أوّل 10 أسطر وبدا الأمر سليماً» لا يفيد أوّلاً في اختبار الكتابة المتوازية. لأنّ ما ينكسر حتماً سطر واحد في مكان ما في الوسط. المعنى في تمرير كلّ الأسطر.

إن كُتب بـ PowerShell 7، يمكن التحقّق من هذه الثلاثة مجتمعة كالتالي. مفترض استدعاؤه من لاحق الاختبار.

# تحقّق من كلّ أسطر السجلّ المُخرَج. إن انكسر موضع واحد فارمِ وأسقط
$path     = 'C:\ProgramData\MyApp\logs\app-20260402-101500-8412-9f3c1d2a5b7e4f689a0c3d5e7f1b2c4d.jsonl'
$expected = 10000   # عدد مرّات استدعاء Write من جانب الاختبار

# «وجود الاسم» وحده لا يكفي. حتى سجلّ مثل `"level": null`
# يحمل اسم الخاصيّة، فيمرّ فحص الوجود وحده.
# لالتقاط اختلاط null يلزم النظر في القيمة والنوع أيضاً
$required = [ordered]@{
    'ts'        = { param($v) $v -is [string] -and $v -ne '' }
    'level'     = { param($v) $v -is [string] -and $v -ne '' }
    'category'  = { param($v) $v -is [string] -and $v -ne '' }
    'message'   = { param($v) $v -is [string] }   # السلسلة الفارغة مسموحة أما null فلا
    'fields'    = { param($v) $v -is [pscustomobject] }
    'sessionId' = { param($v) $v -is [string] -and $v -ne '' }
    'pid'       = { param($v) ($v -is [int] -or $v -is [long]) -and $v -gt 0 }
}

# فحص BOM يكون على البايتات الخام قبل فكّ الترميز.
# Get-Content -Encoding utf8 يتجاوز BOM في البداية ثمّ يرجع الأسطر،
# فلا يُلتقَط اختلاط BOM مهما نظرت في السلسلة بعد الفكّ
# (في 5.1 استخدم -Encoding Byte بدل -AsByteStream)
$head = @(Get-Content -LiteralPath $path -AsByteStream -TotalCount 3)
if ($head.Count -ge 3 -and $head[0] -eq 0xEF -and $head[1] -eq 0xBB -and $head[2] -eq 0xBF) {
    throw 'يوجد UTF-8 BOM في بداية الملف. غير صالح كـ JSON Lines'
}

$lines = @(Get-Content -LiteralPath $path -Encoding utf8)
if ($lines.Count -ne $expected) {
    throw "عدد الأسطر غير متطابق. المتوقَّع $expected سطر / الفعلي $($lines.Count) سطر"
}

$lineNo = 0
foreach ($line in $lines) {
    $lineNo++

    try {
        $record = $line | ConvertFrom-Json
    }
    catch {
        throw "السطر $lineNo لا يُقرأ كـ JSON: $line"
    }

    $names = $record.PSObject.Properties.Name
    foreach ($name in $required.Keys) {
        if ($names -notcontains $name) {
            throw "السطر $lineNo بلا حقل إلزامي: $name"
        }
        if (-not (& $required[$name] $record.$name)) {
            $shown = if ($null -eq $record.$name) { '(null)' } else { "'$($record.$name)'" }
            throw "قيمة $name في السطر $lineNo غير صالحة: $shown"
        }
    }
}

"OK: كلّ $($lines.Count) سطر تُقرأ كسجلّ واحد لكلّ سطر"

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

  • الكتابة المتوازية: اكتب n مرّة من خيوط متعدّدة، ومرّر بجعل $expected يساوي n
  • الدوران والاحتفاظ: أجرِ التحقّق نفسه على كلّ الملفّات المتبقية بعد الدوران، وطابق عدد الأسطر بالمجموع
  • drain عند الإنهاء: تحقّق بعد إدخال معالجة الإنهاء، وانظر هل يطابق عدد ما وُضع

الحالات الشاذّة لا تُقاس بهذا الشكل، فتُنظر منفصلة. اصنع أوّلاً شروطاً مثل عدم وجود دليل موضع الحفظ أو انعدام صلاحيّة الكتابة، ثم هيّئ logger، وتحقّق من أنّ الفشل يظهر بإحدى: استثناء أو قيمة معادة أو إشعار. إن كان التنفيذ هنا «لا يحدث شيء ويُبتلع»، صار ذلك أكثر أشكال الكسر إزعاجاً في التشغيل.

العدد الذي يُراد تمريره في الحدّ الأدنى في v1

إن حُاول الكلّ أوّلاً ثقل الاختبار أكثر من اللازم. ما يُراد تمريره في الحدّ الأدنى في v1 هذا القدر، ستّة تقريباً.

  1. كتابة سليمة من خيط واحد
  2. كتابة متزامنة من خيوط متعدّدة
  3. flush لـ Error / Critical
  4. الدوران والاحتفاظ
  5. إشعار الفشل عند شذوذ موضع الحفظ
  6. drain وflush النهائي عند الإنهاء السليم

بمرور هذه الستّ وحدها تبتعد كثيراً عن «خرجت سلاسل لكن لا يُوثق بالـ logger في التشغيل».

خلاصة

الهدف الأوّل لـ logger مصنوعة ليس غنى الوظائف، بل «أن يُوثق بها عند العطل». لذلك فعّال تثبيت التنسيق على UTF-8 JSON Lines، وتضييق الحقول الإلزامية، وجعل عمليّة واحدة وملفّاً واحداً أساساً، وتقرير سلوك flush والدوران والاحتفاظ والفشل مبكّراً.

ثم يلزم التحقّق ممّا إذا كان ذلك التصميم يعمل حقاً باختبار تكامل يستخدم ملفّاً حقيقياً وخيطاً حقيقياً وعمليّة حقيقية. قبل تكبير التنفيذ، إن ثُبّت أوّلاً التكوين الأدنى ومجموعة الاختبار الدنيا، سهل الإنماء لاحقاً بلا إفراط.

روابط مرجعية

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

ما تنسيق السجلّ الذي ينبغي لـ logger مصنوعة؟
يُنصح بـ JSON Lines بترميز UTF-8، بتنسيق لا ينكسر فيه سجلّ واحد لكلّ سطر. لصق النصّ العادي يصعب التعامل الآلي معه لاحقاً، والتنسيق الثنائي الخاص يُسقط قابلية الرصد في التشغيل. JSON Lines سهل القراءة كنصّ، وسهل التحليل من النصوص والأدوات، ويسهل عزل السطر المنكسر إن انقطعت الكتابة في الوسط، وهذا يناسب العمل. الحقول الإلزامية تُثبَّت على سبعة: timestamp وlevel وcategory وmessage وfields المنظَّمة وsessionId وprocessId.
أيّهما ينبغي لكتابة السجلّ، المتزامن أم غير المتزامن؟
يُفرَّق حسب الحمل. في مرحلة حجم السجلّ المنخفض أوضح الكتابة المتزامنة وأسهل لتحقيق الأعطال. إن فُرض عدم التزامن بلا داعٍ، سهل فقدان السجلّ قبل الإنهاء مباشرة، أو غموض شرط flush عند الاستثناء. إن كثر الحجم وصار I/O المتزامن عنق زجاجة، اعتُمد single writer + bounded queue، وقُرِّر مسبقاً هل تُرمى السجلّات القديمة أم تُسقط الجديدة عند امتلاء الطابور. سجلّات Error/Critical وبدء الجلسة ونهايتها إن بقيت بـ flush متزامن أعانت تحقيق العطل.
هل يجوز لعمليّات متعدّدة الكتابة في ملفّ السجلّ نفسه؟
ينبغي تجنّبه. تصميم الإلحاق بالملفّ نفسه من عمليّات متعدّدة يصعّب دفعة واحدة الاستبعاد المتبادل والكتابة الجزئية وتوقيت الدوران والتعامل عند الإنهاء غير الطبيعي، فتكثر عوامل الحوادث فوق ما يبدو. الأساس عمليّة واحدة وملفّ واحد. إن أردت جمع عمليّات متعدّدة، أأمن التجميع لاحقاً، أو إقامة عمليّة تجميع مخصّصة صراحة.
ماذا ينبغي التحقّق منه في اختبار تكامل logger مصنوعة؟
يُتحقَّق بملفّ حقيقي وخيط حقيقي وعمليّة حقيقية. اختبار الوحدة لتنسيق السلاسل أو تحويل JSON وحده لا يلتقط I/O والتوازي والدوران وflush عند الإنهاء وأخطاء الصلاحيّات التي تصير مشكلة في التشغيل الفعلي. ما يُراد تمريره في الحدّ الأدنى في v1 ستّة: كتابة سليمة من خيط واحد، وكتابة متزامنة من خيوط متعدّدة، وflush لـ Error/Critical، والدوران والاحتفاظ، وإشعار الفشل عند شذوذ موضع الحفظ، وdrain وflush النهائي عند الإنهاء السليم. بمرور هذه الستّ تبتعد كثيراً عن logger لا يُوثق بها في التشغيل.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة