cloud-devops

مقاييس Prometheus مخصصة في Node.js +

٢٩ يونيو ٢٠٢٦

Custom Prometheus Metrics in Node.js + Express (2026)

أنت تقوم بعرض مقاييس Prometheus في Node.js عن طريق تثبيت نقطة نهاية /metrics تُرجع المخرجات النصية لسجل prom-client. يبني هذا البرنامج التعليمي واحدًا على Express: مقاييس العمليات الافتراضية، وعداد طلبات مخصص، ورسم بياني (histogram) للمدة، وتسميات منخفضة العلاقة (low-cardinality) يمكن لـ Prometheus كشطها بأمان — تم تشغيل كل سطر والتحقق منه قبل النشر.

ملخص

يتطلب ربط مقاييس Prometheus في Node.js سجلاً واحدًا، ومقياسين مخصصين، ووسيطًا (middleware) واحدًا. ستقوم بتجهيز تطبيق Express باستخدام prom-client 15.1.31 و Express 5.2.1، مع عرض نقطة نهاية /metrics التي تبلغ عن مقاييس عملية Node الافتراضية بالإضافة إلى عداد http_requests_total ورسم بياني http_request_duration_seconds. الأمر برمته عبارة عن ملفين صغيرين وحوالي 15 دقيقة. الخطأ الذي يدمر الإعدادات الساذجة بهدوء — التسمية حسب عنوان URL الخام بدلاً من قالب المسار (route template)، مما يؤدي لانفجار عدد السلاسل الزمنية — يتم إصلاحه في الخطوة 4، مع إعادة إنتاج المخرجات لإثبات ذلك.

ما ستتعلمه

  • كيفية إنشاء سجل prom-client وجمع مقاييس Node.js الافتراضية باستخدام collectDefaultMetrics
  • كيفية تحديد عداد مخصص (http_requests_total) مع التسميات (labels)
  • كيفية تسجيل زمن انتقال الطلب كرسم بياني (http_request_duration_seconds) باستخدام startTimer
  • كيفية ربط وسيط Express واحد يسجل كليهما عند كل طلب
  • كيفية عرض نقطة نهاية /metrics بنوع المحتوى الذي يتوقعه Prometheus
  • كيفية الحفاظ على انخفاض علاقة التسمية (label cardinality) باستخدام قالب المسار، وليس عنوان URL الخام
  • كيفية قراءة والتحقق من تنسيق عرض Prometheus
  • كيفية إصلاح الأخطاء الشائعة التي يواجهها الجميع مع prom-client

المتطلبات الأساسية

  • Node.js 18+ (يتطلب prom-client إصدار ^16 || ^18 || >=20؛ ويتطلب Express 5 إصدار >= 18). يوصى باستخدام إصدار LTS الحالي — Node 20 أو 22.1
  • prom-client 15.1.3 و Express 5.2.1، مثبتين لضمان إمكانية إعادة إنتاج البناء الخاص بك.
  • إلمام أساسي بمسارات Express و async/await.

لا يلزم أي شيء آخر: لا يحتاج prom-client إلى خادم Prometheus قيد التشغيل لتقديم المقاييس، لذا يمكنك بناء واختبار نقطة النهاية بالكامل محليًا باستخدام curl. خادم Prometheus الحقيقي يهم فقط بمجرد رغبتك في كشط ورسم ما قمت بعرضه بيانيًا.

الخطوة 1 — إنشاء هيكل مشروع Node.js + Express

أنشئ مشروعًا، وقم بتمكين وحدات ES، وقم بتثبيت الحزمتين المثبتتين:

mkdir prom-demo && cd prom-demo
npm init -y
npm pkg set type=module
npm install prom-client@15.1.3 express@5.2.1

يتيح لك ضبط type=module استخدام صيغة import. يتم شحن prom-client كـ CommonJS، ولكن Node تسمح لك باستخدام الواردات المسماة منه، لذا فإن import { Counter } from 'prom-client' تعمل دون أي وسيط توافق.2

الخطوة 2 — إنشاء سجل prom-client وجمع المقاييس الافتراضية

السجل (registry) هو الكائن الذي يحمل مقاييسك ويقدمها بتنسيق نص Prometheus. أنشئ metrics.js وابدأ بسجل بالإضافة إلى مقاييس العمليات المدمجة في Node:

// metrics.js
import { Counter, Histogram, Registry, collectDefaultMetrics } from 'prom-client';

// One registry for this process. Every metric registers itself here.
export const register = new Registry();

// Optional: a label attached to every series (handy when many instances scrape).
register.setDefaultLabels({ app: 'demo-API' });

// Node/process metrics (CPU, memory, event-loop lag, GC...) — collected on scrape.
collectDefaultMetrics({ register });

تقوم collectDefaultMetrics بتسجيل مجموعة من مقاييس وقت التشغيل الموصى بها ويتم جمعها عند الكشط، وليس بناءً على مؤقت — يقرأ prom-client القيم الحالية في كل مرة تستدعي فيها register.metrics().2 هذا الاستدعاء الواحد يمنحك، من بين أمور أخرى:

  • process_cpu_seconds_total، process_resident_memory_bytes، process_start_time_seconds
  • nodejs_eventloop_lag_seconds (بالإضافة إلى متغيرات _p50، _p90، _p99)
  • nodejs_heap_size_total_bytes، nodejs_gc_duration_seconds، nodejs_version_info

بعض مقاييس process_* لواصف الملفات والذاكرة مخصصة لنظام Linux فقط، وهو ما تشير إليه وثائق prom-client صراحةً.2 استدعِ collectDefaultMetrics مرة واحدة لكل سجل — استدعاؤها مرتين يؤدي لخطأ (المزيد عن ذلك في استكشاف الأخطاء وإصلاحها).

الخطوة 3 — تحديد عداد ورسم بياني مخصصين

تخبرك المقاييس الافتراضية عن العملية؛ بينما تخبرك المقاييس المخصصة عن تطبيقك. أضف اثنين إلى metrics.js. العداد (counter) يرتفع فقط، وهو مناسب تمامًا لـ "كم عدد الطلبات التي خدمتها"؛3 بينما يسجل الرسم البياني (histogram) توزيعًا، وهو ما تريده لزمن الانتقال:3

// metrics.js (continued)

// A counter: total HTTP requests, split by method, route, and status code.
export const httpRequestsTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

// A histogram: request duration in seconds (default buckets suit web latencies).
export const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

خيارا التسمية هنا متعمدان ويتبعان اتفاقيات Prometheus الخاصة: تنتهي العدادات بـ _total، وتستخدم المدد الوحدة الأساسية seconds، وليس المللي ثانية.4 هذان الاسمان — http_requests_total و http_request_duration_seconds — هما في الواقع الأمثلة المتعارف عليها التي يستخدمها دليل تسمية Prometheus.4

نظرًا لعدم تمرير مصفوفة buckets، يستخدم الرسم البياني الإعدادات الافتراضية لـ prom-client، والتي تم ضبطها لزمن انتقال طلبات الويب:2

[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]   // seconds

إذا كانت أزمنة الانتقال لديك تتجمع في مكان آخر (على سبيل المثال، إصابات ذاكرة التخزين المؤقت في أقل من مللي ثانية أو عمليات التصدير التي تستغرق عدة ثوانٍ)، فقم بتجاوزها باستخدام buckets: [...]. اجعل القائمة قصيرة: كل دلو (bucket) هو سلسلة زمنية خاصة به، لذا فإن قوائم الدلاء الواسعة تضاعف التخزين بنفس الطريقة التي تفعلها التسميات.

الخطوة 4 — تسجيل كل طلب باستخدام وسيط Express واحد

الآن قم بربط المقاييس بحركة المرور. النمط الأنظف هو وسيط واحد يبدأ مؤقتًا عند وصول الطلب ويسجل كلا المقياسين عند انتهاء الاستجابة. أضفه إلى metrics.js:

// metrics.js (continued)

// Express middleware: time every request, record both metrics when it finishes.
export function metricsMiddleware(req, res, next) {
  const endTimer = httpRequestDuration.startTimer();
  res.on('finish', () => {
    // Use the ROUTE TEMPLATE (/users/:id), never req.url (/users/123) — or every
    // id becomes its own time series and your cardinality explodes.
    const route = req.route?.path ?? 'unmatched';
    const labels = { method: req.method, route, status_code: res.statusCode };
    endTimer(labels);
    httpRequestsTotal.inc(labels);
  });
  next();
}

تُرجع startTimer() دالة؛ استدعاؤها لاحقًا يرصد الثواني المنقضية في الرسم البياني ويسمح لك بإرفاق تسميات لا تعرفها إلا في النهاية، مثل رمز الحالة.2 التسجيل في حدث finish يعني أنك تلتقط رمز الحالة الحقيقي ووقت الاستجابة الكامل، بما في ذلك التسلسل (serialization).

أهم سطر على الإطلاق هو req.route?.path. بحلول الوقت الذي يتم فيه تشغيل finish، يكون Express قد ملأ req.route بـ القالب الذي تطابق — /users/:id، وليس /users/123 الملموس. هذا هو ما يبقي تسمية route محدودة. الطلبات التي لا تطابق أي مسار (خطأ 404) ليس لها req.route، لذا فإن البديل ?? 'unmatched' يدمجها في سلسلة واحدة بدلاً من السماح لكل مسار يتم فحصه بأن يصبح مقياسًا خاصًا به.

الخطوة 5 — عرض نقطة نهاية المقاييس

يقوم Prometheus بسحب البيانات من نقطة نهاية HTTP عادية تُرجع المقاييس بتنسيق العرض النصي الخاص به. أنشئ server.js:

// server.js
import express from 'express';
import { register, metricsMiddleware } from './metrics.js';

const app = express();
app.use(metricsMiddleware);

app.get('/', (req, res) => res.send('Hello'));
app.get('/users/:id', (req, res) => res.json({ id: req.params.id }));

// Prometheus scrapes this endpoint on an interval.
app.get('/metrics', async (req, res) => {
  try {
    res.set('Content-Type', register.contentType);
    res.end(await register.metrics());
  } catch (err) {
    res.status(500).end(err.message);
  }
});

const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => {
  console.log(`Listening on http://localhost:${port}`);
});

هناك تفصيلان مهمان. أولاً، register.metrics() تُرجع وعداً (promise)، لذا يجب انتظارها (await).2 ثانياً، register.contentType هو الترويسة (header) الدقيقة التي يتوقعها Prometheus — وهي text/plain; version=0.0.4; charset=utf-8.5 كتابة text/plain بشكل يدوي تتجاهل رمز الإصدار وقد تسبب مشاكل مع أدوات السحب الأكثر صرامة؛ قراءتها من السجل (registry) تضمن دائماً إرسال القيمة الصحيحة، حتى لو قمت بتبديل السجل إلى تنسيق OpenMetrics. تعيد كتلة try/catch حالة 500 بدلاً من ترك الطلب معلقاً في حال فشل العرض.

الخطوة 6 — التشغيل وسحب المقاييس

ابدأ تشغيل الخادم وأرسل بعض الطلبات:

node server.js
# in another terminal:
curl -s localhost:3000/
curl -s localhost:3000/users/123
curl -s localhost:3000/users/456
curl -s localhost:3000/metrics

تبدأ استجابة /metrics بمقاييس العمليات الافتراضية، تليها مقاييسك المخصصة. إليك قسم العداد (counter) بعد تلك الطلبات الأربعة (أحدها 404 من مسار غير مطابق):

# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",route="/",status_code="200",app="demo-API"} 1
http_requests_total{method="GET",route="/users/:id",status_code="200",app="demo-API"} 2
http_requests_total{method="GET",route="unmatched",status_code="404",app="demo-API"} 1

لاحظ السطر الثاني: /users/123 و /users/456 أنتجا سلسلة زمنية واحدة بقيمة 2، لأن كليهما طابق قالب /users/:id. هذا هو إصلاح عدد العناصر الفريدة (cardinality) من الخطوة 4 وهو يعمل بنجاح.

التحقق من تنسيق عرض Prometheus

يتم عرض كل مقياس كتعليقات # HELP و # TYPE تليها العينات. يتوسع المدرج التكراري (histogram) إلى عدة سلاسل لكل مجموعة من الملصقات (labels): واحدة تراكمية _bucket لكل حد، و _sum، و _count.3 إليك المدرج التكراري لـ /users/:id:

# HELP http_request_duration_seconds Duration of HTTP requests in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.005",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="0.01",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="0.025",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="0.05",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="0.1",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="0.25",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="0.5",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="1",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="2.5",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="5",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="10",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_bucket{le="+Inf",app="demo-API",method="GET",route="/users/:id",status_code="200"} 2
http_request_duration_seconds_sum{app="demo-API",method="GET",route="/users/:id",status_code="200"} 0.0011
http_request_duration_seconds_count{app="demo-API",method="GET",route="/users/:id",status_code="200"} 2

الأوعية (Buckets) تراكمية: كل _bucket{le="..."} يحسب كل ملاحظة أقل من أو تساوي ذلك الحد، لذا فهي تزداد دائماً، و _bucket{le="+Inf"} يساوي دائماً _count.3 وقع كلا الطلبين تحت 5 مللي ثانية، لذا يقرأ كل وعاء القيمة 2. الـ _sum هو الوقت الفعلي وسيختلف على جهازك. لتحويل هذه القيم إلى رسم بياني لوقت الاستجابة (latency)، يحسب Prometheus النسب المئوية (percentiles) من الأوعية باستخدام histogram_quantile().6

يظهر ملصق app="demo-API" في كل سلسلة بسبب setDefaultLabels في الخطوة 2 — وهي طريقة نظيفة لتمييز الخدمة التي أنتجت البيانات دون تكرار نفسك في كل مقياس.

إبقاء عدد العناصر الفريدة للملصقات تحت السيطرة

هذا هو القسم الذي يفصل بين إعداد المقاييس الذي يصمد في بيئة الإنتاج وبين الإعداد الذي قد يتسبب في تعطل خادم Prometheus الخاص بك. كل مجموعة فريدة من قيم الملصقات تمثل سلسلة زمنية منفصلة. وثائق Prometheus صريحة بشأن هذا الأمر:

تذكر أن كل مجموعة فريدة من أزواج الملصقات (مفتاح-قيمة) تمثل سلسلة زمنية جديدة، مما قد يؤدي إلى زيادة هائلة في كمية البيانات المخزنة. لا تستخدم الملصقات لتخزين أبعاد ذات عدد عناصر فريدة مرتفع (قيم ملصقات كثيرة ومختلفة)، مثل معرفات المستخدمين، أو عناوين البريد الإلكتروني، أو أي مجموعات قيم غير محدودة أخرى.4

الخطأ الشائع في Node.js هو تسمية الطلبات باستخدام req.url. مع استخدام req.url، تصبح المسارات /users/123 و /users/124 و /users/125 ثلاث سلاسل زمنية — ويمكن لتطبيق يضم مليون مستخدم وعدد قليل من نقاط النهاية أن يولد ملايين السلاسل، وكل واحدة منها تتضاعف مرة أخرى مع كل وعاء مدرج تكراري. استخدم قالب المسار (req.route?.path) وسيبقى العدد مساوياً لعدد المسارات التي حددتها بالفعل، وهو ما أثبته مخرج الخطوة 6: معرفان مختلفان للمستخدمين، سلسلة زمنية واحدة.

هناك قاعدتان ذهبيتان للحفاظ على سلامة النظام. اجعل قيم الملصقات محصورة في مجموعة معروفة وصغيرة — الطرق (methods)، قوالب المسارات، رموز الحالة، كلها جيدة؛ أما المعرفات (IDs)، والبريد الإلكتروني، والمسارات الخام، فلا تستخدمها أبداً. وتذكر أن كل مدرج تكراري يصدر سلسلة واحدة لكل وعاء بالإضافة إلى وعاء +Inf، و _sum، و _count — وهذا يعني 14 سلسلة لكل مجموعة ملصقات للوضع الافتراضي المكون من 11 وعاءً — لذا فإن قائمة أوعية كبيرة مضروبة في بضعة ملصقات تتضاعف بشكل أسرع مما يتوقعه الناس.

الأخطاء الشائعة في prom-client

A metric with the name http_requests_total has already been registered. لقد قمت بإنشاء نفس اسم المقياس مرتين في نفس السجل. يعني هذا عادةً أنه تم استيراد وحدة (module) تحدد المقاييس مرتين، أو أن إعادة التحميل السريع (hot reload) لخادم التطوير أعادت تشغيل التعريفات. حدد كل مقياس مرة واحدة بالضبط في وحدة مخصصة (مثل metrics.js هنا) واستورد النسخ في مكان آخر — لا تقم بإعادة تعريفها أبداً.2

A metric with the name process_cpu_user_seconds_total has already been registered. نفس السبب، ولكن يتم تحفيزه عن طريق استدعاء collectDefaultMetrics({ register }) مرتين لنفس السجل. استدعها مرة واحدة عند بدء التشغيل. إذا كان برنامج إعادة التحميل السريع يستمر في إعادة تشغيل ملف الإدخال الخاص بك، فقم بحماية الاستدعاء أو أعد التشغيل بشكل نظيف.

/metrics تفتقد مقاييسك المخصصة. يصدر المقياس المصنف ترويسة # HELP و # TYPE الخاصة به ولكن بدون أسطر عينات حتى يتم رصده مرة واحدة على الأقل، لأن prom-client لا يمكنه معرفة قيم الملصقات مسبقاً.2 أرسل طلباً عبر مساراتك أولاً، ثم اسحب البيانات — أو استدعِ histogram.zero({ ... }) لتسجيل مجموعات الملصقات المعروفة مسبقاً. إذا كانت المقاييس الافتراضية فقط هي التي تحتوي على قيم، فتأكد من تمرير مقاييسك المخصصة عبر registers: [register] (أو تمت إضافتها إلى نفس السجل الذي تعرضه).

يرفض Prometheus عملية السحب رغم أن /metrics تبدو صحيحة في المتصفح. يجب أن يكون جسم الاستجابة نص عرض صالحاً، وليس JSON — إرجاع res.json(...) هنا لن يتم تحليله أبداً. اضبط الترويسة صراحةً باستخدام res.set('Content-Type', register.contentType): إن Prometheus 3.0+ صارم ويمكن أن يفشل في عملية سحب تكون فيها Content-Type مفقودة أو غير صالحة ما لم تقم بتكوين fallback_scrape_protocol، بينما يتراجع Prometheus 2.x بهدوء إلى المحلل النصي.7 قراءة القيمة من السجل تبقيها صحيحة في كلتا الحالتين.5

الخطوات التالية

لديك الآن مقاييس Prometheus جاهزة للإنتاج في Node.js: سجل، ومقاييس عمليات افتراضية، وعداد طلبات، ومدرج تكراري لوقت الاستجابة، وبرمجية وسيطة (middleware) واحدة تسجل كل ذلك مع عدد عناصر فريدة محدود. وجه خادم Prometheus إلى http://localhost:3000/metrics مع فاصل زمني للسحب يبلغ 15 ثانية ويمكنك البدء في رسم معدل الطلبات باستخدام rate(http_requests_total[5m]) ووقت استجابة p95 باستخدام histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])).6

من هنا، هناك اتجاهان يستحقان المتابعة. لكي تظهر traces (تتبعات) مستوى الطلب بجانب هذه المقاييس المجمعة، أضف التتبع الموزع كما هو موضح في دليل تتبع OpenTelemetry Collector و Node.js — المقاييس تخبرك أن زمن الاستجابة قد ارتفع، بينما التتبعات تخبرك أين حدث ذلك. ولتحديد أي المقاييس والتنبيهات تستحق الجهد فعلياً، فإن المبادئ الموجودة في بناء استراتيجية مراقبة حديثة ستحافظ على دقة لوحات التحكم الخاصة بك. إذا كنت تقوم بسحب هذه المقاييس من الـ pods، فإن نفس نقطة النهاية /metrics هي ما يستهدفه Prometheus ServiceMonitor بمجرد تشغيل التطبيق على Kubernetes.

Footnotes

  1. prom-client — npm package (version 15.1.3, engines, license). https://www.npmjs.com/package/prom-client 2

  2. siimon/prom-client — Prometheus client for Node.js (README, API reference). https://GitHub.com/siimon/prom-client 2 3 4 5 6 7 8

  3. Prometheus Documentation — "Metric types" (counter, gauge, histogram; _bucket/_sum/_count). https://prometheus.io/docs/concepts/metric_types/ 2 3 4

  4. Prometheus Documentation — "Metric and label naming" (base units, _total suffix, label cardinality caution). https://prometheus.io/docs/practices/naming/ 2 3

  5. Prometheus Documentation — "Exposition formats" (text format version 0.0.4, content type). https://prometheus.io/docs/instrumenting/exposition_formats/ 2

  6. Prometheus Documentation — "Histograms and summaries" (histogram_quantile, bucket selection). https://prometheus.io/docs/practices/histograms/ 2

  7. Prometheus Documentation — "Scrape protocol content negotiation" (Content-Type handling, fallback_scrape_protocol, PrometheusText0.0.4 default). https://prometheus.io/docs/instrumenting/content_negotiation/