السجل المفيد في عمليات CAPTCHA هو سطر JSON واحد لكل حدث، يحمل خمسة حقول تحسم أغلب الأسئلة: event وtask_id وcaptcha_type وsolve_time_ms وerror. بهذه الحقول تعرف خلال ثوانٍ كم مهمة أُرسلت، وكم منها عاد برمز صالح، وأي رمز خطأ تكرّر، وأين ضاع الوقت بين الإرسال والاستطلاع. أما سطر مثل Error solving captcha فلا يجيب عن أي سؤال منها.
الفرق ليس في كمية ما تسجّله بل في شكله: النص العادي يُقرأ بالعين، وJSON يُقرأ بالاستعلام. يغطّي هذا الدليل التنفيذين — structlog في Python وpino في Node.js — ثم يضيف إليهما تصفية فورية بـ jq وتنبيهاً تلقائياً عند ارتفاع نسبة الفشل.
ما الذي يتغيّر حين تصبح سجلات CAPTCHA قابلة للاستعلام
| نص عادي | JSON منظم |
|---|---|
Captcha solved in 12.3s |
{"event":"captcha_solved","task_id":"abc123","type":"recaptcha_v2","solve_time_ms":12300} |
| يحتاج قراءة يدوية | يُقرأ آلياً بأي أداة |
| بحث بـ grep فقط | تصفية بأي حقل: النوع أو الخطأ أو الزمن |
| أحداث متفرّقة بلا رابط | task_id يربط الإرسال ← الاستطلاع ← حقن الرمز |
الفائدة تظهر عند أول تحقيق: بدل قراءة آلاف الأسطر بحثاً عن كلمة، تسأل الملف سؤالاً محدداً — كم مهمة recaptcha_v2 زاد زمن حلها على ثلاثين ثانية أمس؟ — فتحصل على رقم.
صمّم مخطط حقول السجل قبل أول سطر كود
أكثر أخطاء التسجيل كلفةً يقع قبل كتابة الكود: حقل اسمه id في Python وtaskId في Node.js، فلا تتطابق البيانات عند تجميعها. ثبّت المخطط أولاً والتزم به في اللغتين:
| الحقل | النوع | ما يمثّله |
|---|---|---|
event |
نص | اسم الحدث: captcha_submitted، captcha_solved |
task_id |
نص | معرّف المهمة من CaptchaAI — أساس الربط |
captcha_type |
نص | recaptcha_v2، turnstile، image وغيرها |
site_url |
نص | عنوان الصفحة المستهدفة |
solve_time_ms |
عدد صحيح | الزمن الكلي من الإرسال حتى الرمز |
poll_attempts |
عدد صحيح | عدد طلبات الاستطلاع حتى النتيجة |
error |
نص | رمز الخطأ كما تعيده الخدمة |
token_length |
عدد صحيح | طول الرمز المُعاد — لا الرمز نفسه |
ثلاث قواعد تسمية تختصر عليك إعادة العمل لاحقاً:
- اكتب أسماء الأحداث بصيغة
snake_caseوبفعل يصف ما حدث فعلاً:captcha_solvedلاsolving_captcha. - احتفظ بأسماء الحقول وقيمها بالحروف اللاتينية حتى في المشاريع العربية بالكامل؛ العربية مكانها عناوين لوحة التحكم لا قيم السجل.
- وحّد الطوابع الزمنية على UTC بصيغة ISO 8601، واترك التحويل المحلي لطبقة العرض.
الخطوة 1: هيّئ structlog ليُخرج JSON في Python
structlog يكتب JSON مباشرة إلى المخرج القياسي، بلا معالج إضافي ولا تنسيق يدوي:
import structlog
import time
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
structlog.processors.JSONRenderer(),
],
logger_factory=structlog.PrintLoggerFactory(),
)
log = structlog.get_logger()
بهذا الإعداد يصير كل نداء تسجيل سطر JSON مكتملاً بطابع زمني ومستوى، جاهزاً لأي أداة تجميع.
سجّل دورة حياة الحل من الإرسال حتى الرمز
المفتاح هنا هو log.bind(): تربط الحقول الثابتة — نوع CAPTCHA وعنوان الصفحة ومفتاح الموقع مقتطعاً — مرة واحدة فترثها كل الأحداث التالية. وبمجرد صدور معرّف المهمة اربطه أيضاً ليظهر في كل سطر بعده:
import requests
API_KEY = "YOUR_API_KEY"
def solve_captcha(captcha_type, sitekey, page_url, proxy=None):
solve_log = log.bind(
captcha_type=captcha_type,
site_url=page_url,
sitekey=sitekey[:12] + "...",
)
# Submit
start = time.time()
solve_log.info("captcha_submit_start")
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": page_url,
"json": "1",
}).json()
if resp["status"] != 1:
solve_log.error("captcha_submit_failed", error=resp["request"])
return None
task_id = resp["request"]
submit_ms = int((time.time() - start) * 1000)
solve_log = solve_log.bind(task_id=task_id)
solve_log.info("captcha_submitted", submit_ms=submit_ms)
# Poll
for attempt in range(24):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": "1"
}).json()
if result["status"] == 1:
solve_ms = int((time.time() - start) * 1000)
solve_log.info(
"captcha_solved",
solve_time_ms=solve_ms,
poll_attempts=attempt + 1,
token_length=len(result["request"]),
)
return result["request"]
if result["request"] != "CAPCHA_NOT_READY":
solve_log.error(
"captcha_solve_failed",
error=result["request"],
poll_attempts=attempt + 1,
)
return None
solve_log.warning("captcha_solve_timeout", poll_attempts=24)
return None
لاحظ ما لا يُسجَّل: الرمز نفسه لا يدخل السجل، ومفتاح الموقع يُقتطع، ومحاولات الاستطلاع تُلخَّص في poll_attempts بدل تسجيلها واحدة واحدة.
ثلاثة أسطر فقط لكل مهمة ناجحة:
{"event":"captcha_submit_start","captcha_type":"recaptcha_v2","site_url":"https://example.com","sitekey":"6Le-wvkSAAAA...","timestamp":"2025-07-15T10:30:00Z","level":"info"}
{"event":"captcha_submitted","task_id":"71845302","submit_ms":245,"timestamp":"2025-07-15T10:30:00Z","level":"info"}
{"event":"captcha_solved","task_id":"71845302","solve_time_ms":18230,"poll_attempts":4,"token_length":580,"timestamp":"2025-07-15T10:30:18Z","level":"info"}
الخطوة 2: كرّر المخطط نفسه في Node.js عبر pino
pino يقدّم النمط ذاته بأسماء مختلفة: child بدل bind. هيّئه ليكتب طوابع ISO بدل الأرقام الافتراضية:
const pino = require('pino');
const log = pino({
level: 'info',
timestamp: pino.stdTimeFunctions.isoTime,
});
اربط كل مهمة بسجل فرعي خاص بها
كل مهمة تحصل على child logger مستقل، فيبقى taskId ملازماً لأحداثها حتى انتهاء المهلة:
const axios = require('axios');
const API_KEY = 'YOUR_API_KEY';
async function solveCaptcha(captchaType, sitekey, pageUrl) {
const taskLog = log.child({
captchaType,
siteUrl: pageUrl,
sitekey: sitekey.substring(0, 12) + '...',
});
const start = Date.now();
taskLog.info('captcha_submit_start');
const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
params: {
key: API_KEY, method: 'userrecaptcha',
googlekey: sitekey, pageurl: pageUrl, json: 1,
},
});
if (submit.data.status !== 1) {
taskLog.error({ error: submit.data.request }, 'captcha_submit_failed');
return null;
}
const taskId = submit.data.request;
const boundLog = taskLog.child({ taskId });
boundLog.info({ submitMs: Date.now() - start }, 'captcha_submitted');
for (let attempt = 1; attempt <= 24; attempt++) {
await new Promise(r => setTimeout(r, 5000));
const poll = await axios.get('https://ocr.captchaai.com/res.php', {
params: { key: API_KEY, action: 'get', id: taskId, json: 1 },
});
if (poll.data.status === 1) {
boundLog.info({
solveTimeMs: Date.now() - start,
pollAttempts: attempt,
tokenLength: poll.data.request.length,
}, 'captcha_solved');
return poll.data.request;
}
if (poll.data.request !== 'CAPCHA_NOT_READY') {
boundLog.error({ error: poll.data.request, pollAttempts: attempt }, 'captcha_solve_failed');
return null;
}
}
boundLog.warn({ pollAttempts: 24 }, 'captcha_solve_timeout');
return null;
}
الفرق الوحيد الذي يستحق الانتباه هو ترتيب الوسائط: pino يستقبل كائن الحقول أولاً ثم نص الرسالة، وstructlog يعكس الترتيب. أبقِ أسماء الحقول متطابقة بين اللغتين ليقرأ استعلام واحد مخرجات الاثنين.
من الملف إلى الإجابة: تصفية الأحداث بـ jq
سجل JSON يجعل jq بديلاً كافياً عن لوحة مراقبة كاملة في المشاريع الصغيرة والمتوسطة:
# With jq
cat captcha.log | jq 'select(.level == "error" and .event == "captcha_solve_failed")'
ومن هنا تبني الباقي بأسطر مشابهة: عدّ الأخطاء حسب رمزها، أو استخرج solve_time_ms لحساب الوسيط، أو تتبّع task_id واحداً عبر مراحله لإعادة بناء قصة مهمة فشلت.
نبّه نفسك قبل أن ينبّهك المستخدمون
قراءة السجلات بعد المشكلة أفضل من لا شيء، لكن الأفضل أن ينبّهك النظام أولاً. راقب نسبة الفشل في نافذة متحرّكة بدل كل خطأ منفرداً — فخطأ واحد ضجيج، وعشرون خطأً في مئة محاولة إشارة:
# Count errors vs successes in a rolling window
from collections import deque
class ErrorRateMonitor:
def __init__(self, window_size=100, threshold=0.2):
self.results = deque(maxlen=window_size)
self.threshold = threshold
def record(self, success):
self.results.append(success)
if len(self.results) >= 50:
error_rate = 1 - sum(self.results) / len(self.results)
if error_rate > self.threshold:
log.warning(
"captcha_error_rate_high",
error_rate=round(error_rate, 3),
window=len(self.results),
)
اضبط العتبة على ما يناسب سير عملك، وتذكّر أن ارتفاع الفشل غالباً لا يعني خللاً في الحل: مفتاح موقع تغيّر، أو صفحة أُعيد بناؤها، أو رصيد قارب على النفاد.
سيناريو محلي: نافذة حجز مواعيد تفتح في التاسعة صباحاً
تخيّل فريق QA في القاهرة أو الرياض يراقب بوابة حجز مواعيد تفتح في التاسعة صباحاً بتوقيت +03، فيتكدّس الطلب في أول عشر دقائق. الشكوى تصل دائماً بالصيغة نفسها: الحل صار بطيئاً في الصباح. والسجل المنظم يفصل الاحتمالات في استعلام واحد:
- ارتفاع
solve_time_msوpoll_attemptsمعاً مع بقاءerrorفارغاً: المهام تنتظر دورها ولا تفشل، والقيد في عدد الـ threads لا في الخدمة. - ثبات
poll_attemptsمع تكرار رمز خطأ بعينه: المشكلة في الطلب — مفتاح موقع قديم أو عنوان صفحة تغيّر بعد تحديث البوابة. - فارق ثلاث ساعات بالضبط بين أوقات الأحداث وأوقات الشكاوى: أنت تقارن UTC بالتوقيت المحلي، وهو سبب متكرّر لتحقيقات بلا نتيجة.
الحالة الأولى قرار تشغيلي مباشر: تسعير CaptchaAI قائم على عدد الـ threads المتزامنة مع حلول غير محدودة لكل thread، فالانتقال من BASIC — 5 threads بسعر $15 شهرياً — إلى ADVANCE — 50 threads بسعر $90 شهرياً — يوسّع نافذة التزامن في الذروة دون تغيير سطر كود. والسجل هو ما يثبت ذلك بدل التخمين.
أخطاء شائعة في سجلات CAPTCHA وكيف تعالجها
| المشكلة | السبب | العلاج |
|---|---|---|
| حجم السجل ينمو بسرعة | تسجيل كل محاولة استطلاع | سجّل الإرسال والنجاح والفشل فقط |
| تعذّر ربط الأحداث | task_id غير مرتبط مبكراً |
اربطه فور صدوره بـ log.bind() أو log.child() |
| السجلات غير قابلة للبحث | تنسيق نص عادي | حوّلها إلى JSON بـ structlog أو pino |
| بيانات حساسة في السجل | تسجيل مفتاح الـ API كاملاً | لا تسجّل المفاتيح، واقتطع مفتاح الموقع |
| أوقات لا تطابق الشكاوى | خلط التوقيت المحلي بـ UTC | سجّل بـ UTC وحوّل عند العرض |
| رسائل خطأ بلا سياق | تسجيل نص الاستثناء وحده | أضف captcha_type وsite_url لكل خطأ |
الأسئلة الشائعة
كم يوماً أحتفظ بسجلات مهام CAPTCHA؟
من سبعة إلى ثلاثين يوماً يغطي أغلب التحقيقات التشغيلية. الأحداث التفصيلية تفقد قيمتها سريعاً، فاحتفظ بها مدة قصيرة، وأبقِ ملخّصاً يومياً — عدد المهام ومعدل النجاح ووسيط solve_time_ms — لمقارنة الشهر بالشهر.
كيف أربط سجل الحل بطلب المستخدم داخل تطبيقي؟
أضف معرّف الطلب لديك — request_id أو trace_id — إلى الحقول المرتبطة عند إنشاء السجل الفرعي، تماماً كما تربط task_id. عندها تقودك أي تذكرة دعم من رقم الطلب إلى مهمة CAPTCHA المرتبطة به والعكس.
هل أسجّل مفتاح الـ API أو مفتاح الموقع؟
مفتاح الـ API لا يُسجَّل أبداً، حتى مقتطعاً؛ فالسجلات تُنسخ وتُصدَّر إلى أنظمة أخرى. أما مفتاح الموقع فقيمة عامة ظاهرة في الصفحة، ويكفي أول اثني عشر حرفاً منه للتمييز بين المواقع.
ما الحقول التي تكشف أن القيد في عدد الـ threads لا في الحل؟
راقب poll_attempts وsolve_time_ms مع بقاء error فارغاً: ارتفاعهما بلا أخطاء يعني انتظاراً في التزامن، أما ارتفاع الأخطاء مع ثبات زمن الحل فيشير إلى خلل في الطلب أو الصفحة.
ابنِ سير عمل CAPTCHA قابلاً للملاحظة مع CaptchaAI
احصل على مفتاح الـ API من captchaai.com وشغّل أول مهمة بسجل JSON كامل.