# السجلات النظيفة بنية تحتية للوكلاء
Table of Contents
وكلاء البرمجة يستطيعون قراءة الكود. ويستطيعون تشغيل الاختبارات. ويستطيعون البحث في الوثائق. وإذا منحتهم الصلاحية، يستطيعون فحص سجلات الإنتاج عبر خوادم Observability MCP.
هذا الجزء الأخير أهم مما يبدو للوهلة الأولى. لم تعد سجلات الإنتاج حكرًا على البشر الذين يحدّقون في لوحات المراقبة. لقد أصبحت اليوم واجهة يستطيع وكيلك الاستعلام عنها وتصفيتها وتجميعها وربط عناصرها ببعضها والاستدلال عليها.
ما يعني أن السجلات الرديئة لم تعد مجرد مصدر إزعاج.
السجلات الرديئة تجعل وكيلك أسوأ.
المستهلك الجديد لسجلاتك
لسنوات، كانت نصائح التسجيل تُكتب للبشر:
- اجعل الأخطاء مقروءة.
- ضمّنها سياقًا كافيًا.
- تجنّب تسجيل الأسرار.
- لا تُغرق الإنتاج بالسجلات.
كل ذلك ما يزال صحيحًا. لكن تصحيح الأخطاء الوكيلي يضيف مستهلكًا ثانيًا: محقّقًا يقوده نموذج لغوي كبير يستخدم أدوات مثل Cloudflare Workers Observability MCP للإجابة عن الأسئلة انطلاقًا من قياسات الإنتاج عن بُعد.
هذا الوكيل بارع في أمور يكون البشر بطيئين فيها:
- البحث في سجلات أسبوع كامل عن عبارة نادرة.
- تجميع الأخطاء حسب نقطة النهاية أو المحفظة أو شريحة المستخدمين أو المزوّد أو المسار أو الحالة.
- إعادة بناء الخطوط الزمنية عبر طلبات API وإنذارات Durable Objects ومهام cron وأحداث webhook.
- مقارنة السلوك قبل حادثة ما وبعدها.
- صياغة فرضية انطلاقًا من الكود، ثم اختبارها في مواجهة أدلة الإنتاج.
لكنه أيضًا سهل التضليل. إذا كانت سجلاتك صاخبة، أو غير مهيكلة، أو مليئة بالحمولات الخام، أو تفتقر إلى حقول الربط، فسينفق الوكيل ميزانية سياقه على النفايات. سيعثر على الشيء الأعلى صوتًا بدلًا من الشيء ذي الصلة.
جودة سجلاتك تصبح جودة عينَي وكيلك.
حادثة صغيرة، مجهّلة الهوية
تخيّل تدفقًا بسيطًا:
- يبادل مستخدم أصلًا بأصل آخر.
- مباشرة بعد الصفقة، يحاول النظام سحب الأصل الجديد.
- تقبل منصة التداول طلب السحب وتعيد معرّف مرجع خارجيًا.
- تظهر المعاملة المحلية لاحقًا بحالة فاشلة.
قد يبدأ الإنسان من استجابة API. هل رفضته منصة التداول؟ هل كان العنوان خاطئًا؟ هل كانت الشبكة غير مدعومة؟ هل قصّر حسابُ الرسوم في تمويل عملية السحب؟
وكيل يملك مراقبة للإنتاج يستطيع أن يفعل ما هو أفضل، لكن فقط إذا كانت السجلات مصوغة لأغراض التحقيق.
الخط الزمني المفيد ينبغي أن يبدو هكذا:
11:52:48 withdraw.execute.started walletId=... quoteId=... exchange=... asset=... network=... amount=...
11:52:49 withdraw.transaction.persisted recordId=... externalRefId=... status=pending
11:53:20 withdraw.native_poll.scheduled recordId=... externalRefId=...
11:54:51 withdraw.native_poll.status_changed recordId=... previousStatus=pending currentStatus=failed providerRawStatus=...هذا شيء يمكن تصحيح أخطائه. يستطيع الوكيل البحث عن recordId، والتجميع حسب providerRawStatus، وفحص كل حالات الفشل لنفس زوج الأصل/الشبكة، والتحقق مما إذا كانت حالة الفشل جاءت من منصة التداول، أو من طبقة المعالجة اللاحقة المحلية، أو من إعادة محاولة webhook، أو من مستطلِع يعمل في الخلفية.
قارن ذلك الآن بحساء سجلات الإنتاج المعتاد:
Direct execution [abc123] { giant workflow params object... }balancesWithNetworkInfo [...]sortedBalances [...][WebhookEndpoints] Found 7 active endpoints[WebhookEndpoints] Endpoint 0 events=[...][TenantDO] Alarm triggeredError: failedما يزال بإمكان الوكيل البحث في هذا، لكن عليه أن يستنتج كل شيء استنتاجًا. والأسوأ أن سجلات مسار النجاح الصاخبة تدفن السطر الوحيد المهم.
حين يحقق الوكلاء في الإنتاج، لا يكون ضجيج السجلات مسألة تجميلية. إنه عبء تشغيلي.
ما الذي يجعل السجل صديقًا للوكيل؟
السجلات الصديقة للوكيل تتمتع بخمس خصائص.
1. أسماء أحداث ثابتة
كل حدث مهم ينبغي أن يحمل اسمًا ثابتًا منخفض التباين (cardinality):
logInfo('withdraw.execute.started', { walletId, quoteId, exchange, asset, network, amount,})فضّل هذا على:
console.log(`Starting withdrawal for ${walletId} on ${exchange}`)النسخة النثرية تُقرأ مرة واحدة. النسخة المهيكلة قابلة للبحث إلى الأبد.
أسماء الأحداث الثابتة تتيح للوكيل أن يسأل:
- أرني كل أحداث
withdraw.execute.startedحول هذا التوقيت. - عُدّ أحداث
withdraw.native_poll.status_changedحسبcurrentStatus. - اعثر على كل
webhook.delivery.max_attemptsفي الأيام السبعة الأخيرة. - قارن
api.request.failedحسب مسار الطريق (route).
أسماء الأحداث الجيدة مملة. وهذا هو المقصود بالضبط.
استخدم نطاق تسمية قابلًا للتوقع:
api.request.failedapi.request.slowwithdraw.execute.startedwithdraw.execute.completedwithdraw.errorwithdraw.native_poll.status_changedwebhook.delivery.failed_attemptwebhook.delivery.max_attemptsprovider.withdraw_info.live_fallbackينبغي للاسم أن يقول ما الذي حدث، لا أن يتضمن البيانات التي تغيّرت. ضع القيم المتغيرة في الحقول.
2. حقول ربط في كل مكان
لا يستطيع الوكيل إعادة بناء خط زمني بلا مقابض يمسك بها.
في الأنظمة المعامَلاتية، ينبغي أن تظهر الحقول نفسها عبر سجلات الطلب والتخزين والاستطلاع وwebhook والأخطاء:
{ orgId, projectId, walletId, quoteId, transactionId, recordId, exchange, asset, network,}لست بحاجة إلى كل حقل في كل سجل. لكن كل سجل ينبغي أن يتضمن ما يكفي لربطه بالخطوة التالية.
الحقول الأعلى قيمة عادة هي:
- معرّف المستأجر أو المؤسسة.
- معرّف المستخدم أو المحفظة.
- معرّف الطلب أو عرض السعر أو الأمر أو المعاملة، أو مفتاح idempotency.
- اسم منصة التداول/المزوّد.
- الأصل والشبكة.
- مسار الطريق (route)، لا عنوان URL الخام.
- معرّف المرجع الخارجي لدى المزوّد.
- الحالة السابقة والحالة التالية عند تغيّر الحالة.
وفي أنظمة الخلفية، ضمّن هوية الجدولة أيضًا:
logInfo('withdraw.native_poll.scheduled', { recordId, walletId, quoteId, exchange, asset, refid, nextPollAt,})هذا يتيح للوكيل الإجابة عن السؤال الحقيقي: «ما الذي حدث لهذا الشيء بعد أن أعادت API استجابتها؟»
3. سجلات لانتقالات الحالة، لا لكل خطوة
لا تسجّل كل نبضة.
سجّل كل انتقال حالة ذي معنى.
جيد:
withdraw.execute.startedwithdraw.execute.completedwithdraw.transaction.persistedwithdraw.native_poll.status_changedwithdraw.errorwebhook.delivery.max_attemptsصاخب في الغالب:
Alarm triggeredProcessing 12 pending itemsFound endpoint 0Found endpoint 1Broadcasted to 0 listenersBalance array after enrichment: [...]Successfully refreshed balancesالتمييز هنا ليس «سجلات النجاح سيئة وسجلات الأخطاء جيدة». سجلات النجاح مفيدة حين تَسِم حدًّا فاصلًا:
- قُبل أمر تداول.
- أُرسل طلب سحب.
- خُزّن صف معاملة.
- رصد مستطلِع حالة نهائية لدى المزوّد.
- وصل webhook أخيرًا إلى وجهته بعد إعادة محاولات.
لكن ثرثرة مسار النجاح داخل الحلقات الضيقة أو الإنذارات المتكررة ستهيمن على نتائج الاستعلامات.
قاعدة بسيطة: إذا كان السجل لن يساعد في إعادة بناء الخط الزمني لحادثة، فالأرجح ألا مكان له في الإنتاج عند مستوى info.
4. أخطاء مهيكلة بلا حمولات خام
يحتاج الوكلاء إلى سياق الخطأ. لكنهم لا يحتاجون إلى الأسرار أو العناوين أو استجابات API أو ملفات تعريف الارتباط (cookies) أو التوقيعات أو الرموز (tokens) أو حمولات webhook الكاملة.
لا تفعل هذا أبدًا:
console.error('Withdrawal failed:', rawProviderResponse)افعل هذا:
logError('withdraw.error', { code: serializedError.code, errorMessage: serializedError.message, hasRawResponse: Boolean(serializedError.rawResponse), walletId, quoteId, exchange, asset, network,})خزّن استجابات المزوّد الخام حيث يكون ذلك مناسبًا، خلف ضوابط الوصول المعتادة إلى بياناتك. أبقِ السجلات آمنة ومضغوطة.
هذا مهم لسببين:
- تميل السجلات إلى التدفق نحو أنظمة كثيرة لها قرّاء كثيرون.
- الوكلاء بارعون جدًا في مطابقة الأنماط، لكن كل كائن خام إضافي يحرق السياق ويزيد احتمال استخلاص الإشارة الخاطئة.
عقّم عند حدود المسجّل (logger)، لا عند كل موضع استدعاء. مسجّل مشترك صغير ينبغي أن يحجب مفاتيح مثل:
addressapiKeyapiSecretauthorizationcookiepasswordprivateKeyrawResponsesecretsignaturetokentwofaجرّد أيضًا سلاسل الاستعلام من حقول URL. فعناوين webhook ونداءات OAuth الراجعة وعناوين المزوّدين كثيرًا ما تحمل بيانات حساسة أو عالية التباين.
5. تباين منخفض افتراضيًا
كثيرًا ما يجمّع الوكلاء السجلات. أسماء السجلات وحقولها عالية التباين تجعل التجميع عديم الجدوى.
تجنّب أسماء أحداث مثل:
withdraw.failed.ASSET.NETWORK.wallet_123استخدم:
logError('withdraw.error', { asset: 'ASSET', network: 'Ethereum', walletId: 'wallet_123',})اسم الحدث ينبغي أن يكون ثابتًا. والحقول هي التي تتغير.
وبالنسبة إلى المسارات، سجّل أنماط الطرق (routes) بدلًا من المسارات الخام:
/v0/wallet/transaction/:idبدلًا من:
/v0/wallet/transaction/transaction_123هذا يتيح للوكيل تجميع حالات الفشل حسب نقطة النهاية بدلًا من إنتاج مجموعة لكل UUID.
عقد تسجيل عملي
هذا هو عقد التسجيل الذي أريده اليوم في أي واجهة Workers API سيتولى وكيلٌ تصحيح أخطائها لاحقًا.
وسيط API (Middleware)
عند حافة التطبيق:
- سجّل استجابات 500 التي لا ترمي استثناءً بوصفها
api.request.failed. - سجّل الأخطاء غير المتوقعة المرمية بوصفها
api.request.error. - سجّل الطلبات الناجحة البطيئة بوصفها
api.request.slow. - ضمّن الطريقة (method) ومسار الطريق والحالة والمدة ومعرّفات المستأجر ومعرّف المحفظة/المستخدم إن توفّر، وCloudflare Ray ID إن وُجد.
لا تُعِد تفعيل التسجيل العام للطلبات إلا إذا كان لديك أخذ عينات وسبب واضح. فسجلات «سطر لكل طلب» قد تُغرق كل شيء آخر.
معالجات الأوامر
عند بداية عملية مهمة:
logInfo('withdraw.execute.started', { quoteId, walletId, exchange, asset, network, amount, dryRun,})وعند الاكتمال:
logInfo('withdraw.execute.completed', { quoteId, recordId, walletId, exchange, asset, network, transactionId,})وعند أخطاء المزوّد المتوقعة:
logError('trade.order.failed', { walletId, exchange, routeId, side, type, volume, error,})تجنّب إلقاء أجسام الطلبات أو الاستجابات كاملة في السجلات.
كائنات Durable Objects والإنذارات
كثيرًا ما تكون إنذارات Durable Objects أسوأ المخالفين. فهي تعمل بتواتر عالٍ، والإغراء قائم لتسجيل كل دورة:
Alarm triggeredProcessing 10 retriesNo webhook endpoints foundItem already terminalمعظم ذلك ينبغي أن يختفي.
أبقِ على:
- استنفاد إعادة المحاولات.
- تغيّر الحالة.
- فشل استطلاع المزوّد.
- فشل تسليم webhook نهائيًا.
- فشل فك تشفير سر.
- فشل تخزين غير متوقع.
احذف:
- انطلق الإنذار.
- لا شيء للقيام به.
- مجدول سلفًا.
- في حالة نهائية سلفًا.
- لا مشتركين.
- نجاح البث.
الإنذار ليس هو القصة. انتقال الحالة هو القصة.
Webhooks
أنظمة webhook تحتاج إلى سجلات، لكن ليس لكل تفرّع داخلي.
مفيد:
webhook.delivery.failed_attemptwebhook.delivery.max_attemptswebhook.endpoint.secret_missingwebhook.endpoint.secret_decrypt_failedصاخب في الغالب:
Querying endpointsFound 3 endpointsEndpoint 0 has event=trueDelivery succeededScheduled retry for 2026-...قاعدة البيانات تحتفظ أصلًا بسجلات التسليم. ينبغي للسجلات أن تُبرز الحالات التي يجدر بالوكيل التحقيق فيها.
قبل وبعد
قبل:
console.log('balancesWithNetworkInfo', balancesWithNetworkInfo)console.log('enrichedBalances', enrichedBalances)console.log('sortedBalances', sortedBalances)console.error(`Failed to store transaction ${txid}:`, err)console.log('[TenantDO] Alarm triggered')console.log(`[WebhookEndpoints] Found ${endpoints.length} active endpoints`)بعد:
logWarn('wallet.balance.refresh_failed', { walletId, exchange, error,})
logInfo('withdraw.transaction.persisted', { recordId, walletId, quoteId, exchange, asset, network, transactionId,})
logWarn('webhook.delivery.max_attempts', { deliveryId, endpointId, webhookEvent, eventId, attemptCount, responseStatus, errorMessage,})النسخة الثانية نصّها أقل ومعلوماتها أكثر.
هذه هي الحيلة الجوهرية.
حلقة تصحيح الأخطاء الوكيلية
ما إن تصبح السجلات نظيفة حتى تتغير حلقة تصحيح الأخطاء.
فبدلًا من أن تسأل وكيلًا:
هل يمكنك النظر في هذا الخطأ الوحيد وتخمين ما حدث؟
يمكنك أن تطلب:
ابحث في سجلات الإنتاج عن معرّف هذه المعاملة، وأعد بناء الخط الزمني، وجمّع حالات الفشل المشابهة في الأيام السبعة الأخيرة، وقارن الحالات الخام لدى المزوّد حسب الأصل/الشبكة.
عندها يستطيع الوكيل أن:
- يقرأ مسارات الكود ذات الصلة.
- يستعلم عن السجلات حسب اسم الحدث الثابت.
- يتمحور حول حقول الربط.
- يحدد ما إذا كان الفشل جاء من معالجة الطلب، أو الإرسال إلى المزوّد، أو التخزين المحلي، أو الاستطلاع في الخلفية، أو تسليم webhook.
- يعثر على حالات مشابهة.
- يقترح ما إذا كان هذا خطأً في الكود، أو سلوكًا من المزوّد، أو مشكلة في إعدادات المستخدم، أو توقّعًا ناقصًا في المنتج.
هذا سير عمل مختلف نوعيًا عن التنقيب اليدوي في لوحات المراقبة.
لكنه لا ينجح إلا إذا تعاونت السجلات.
قائمة تحقق لسجلات جاهزة للوكلاء
استخدم هذه القائمة قبل شحن أي سير عمل جديد:
- هل لكل عملية كبرى حدثُ
startedوحدثٌ نهائي؟ - هل تُسجَّل انتقالات الحالة مع الحالتين السابقة والتالية؟
- هل أستطيع ربط سجلات API وسجلات قاعدة البيانات ومهام الخلفية وأحداث webhook بمعرّفات مشتركة؟
- هل مسارات الطرق (routes) مطبَّعة؟
- هل معرّفات المراجع لدى المزوّد مضمّنة؟
- هل حمولات المزوّد الخام مستبعدة من السجلات؟
- هل تُحجب العناوين والرموز والأسرار والتوقيعات وملفات تعريف الارتباط وقيم authorization مركزيًا؟
- هل أُزيلت سجلات مسار النجاح عالية الحجم أو أُخذت منها عينات؟
- هل تُسجَّل حالات استنفاد إعادة المحاولات والفشل النهائي؟
- هل أستطيع تجميع السجلات حسب اسم الحدث والحصول على توزيع ذي معنى؟
- لو بحث وكيل عن معاملة واحدة، فهل سيجد القصة أم مجرد الضجيج؟
ما الذي تحذفه أولًا
إذا كانت سجلات إنتاجك صاخبة أصلًا، فابدأ من هنا:
- احذف السجلات من دوال التحويل/التطبيع الصِرفة.
- احذف السجلات التي تطبع مصفوفات كاملة أو كائنات كاملة.
- احذف «نجحنا في فعل X» داخل المسارات المتكررة ما لم تكن تَسِم حدًّا لمعاملة.
- احذف سجلات «لا شيء للقيام به» من الإنذارات والمستطلِعات والطوابير ومهام cron.
- احذف السجلات المكررة لكل نقطة نهاية أو لكل عنصر داخل الحلقات.
- احذف مطبوعات الطلبات/الاستجابات الخام.
- استغنِ عن الأخطاء النثرية لصالح أسماء أحداث وحقول مهيكلة.
لست بحاجة إلى مشروع مراقبة ضخم. تحتاج إلى بضعة أحداث ثابتة وإلى الانضباط في حذف ما سواها.
التحوّل
كانت المراقبة (observability) شيئًا يستهلكه البشر بعد وقوع الأمر.
أما اليوم فهي جزء من بيئة التطوير. وكيل برمجة يملك وصولًا عبر MCP يستطيع التحقيق في الإنتاج والتثبّت من الفرضيات والعثور على الأنماط عبر الأنظمة — لكن فقط إذا كان الإنتاج يبثّ إشارة يستطيع فهمها.
السجلات النظيفة ليست مجرد سجلات أجمل.
السجلات النظيفة بنية تحتية للوكلاء.
إنها الطريقة التي تمنح بها وكيلك عيونًا.