تطوير واجهات REST API
مراقبة API والتسجيل
فهم قابلية مراقبة API
تعتبر مراقبة API والتسجيل ضروريين للحفاظ على صحة النظام وتصحيح المشكلات وفهم سلوك المستخدم والتأكد من استيفاء اتفاقيات مستوى الخدمة. تشمل قابلية المراقبة السجلات والمقاييس والتتبع التي توفر رؤية لسلوك API الخاص بك في بيئات الإنتاج.
الركائز الثلاث لقابلية المراقبة:
- السجلات: سجلات تفصيلية للأحداث المنفصلة (الطلبات، الأخطاء، التحذيرات)
- المقاييس: قياسات عددية بمرور الوقت (أوقات الاستجابة، معدلات الطلب، معدلات الخطأ)
- التتبع: تدفق الطلب من البداية إلى النهاية عبر الخدمات الموزعة
تسجيل الطلبات
يساعدك تسجيل الطلبات الشامل على فهم أنماط استخدام API وتحديد الأخطاء وتصحيح مشاكل الإنتاج.
التسجيل المنظم مع Winston
// winston-logger.js
const winston = require('winston');
// إنشاء نسخة المسجل
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
defaultMeta: { service: 'api-service' },
transports: [
// كتابة الأخطاء إلى error.log
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// كتابة جميع السجلات إلى combined.log
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 5242880,
maxFiles: 5
})
]
});
// إضافة إخراج وحدة التحكم في التطوير
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
module.exports = logger;
middleware تسجيل الطلبات
const express = require('express');
const logger = require('./winston-logger');
const { v4: uuidv4 } = require('uuid');
const app = express();
// middleware تسجيل الطلبات
app.use((req, res, next) => {
// توليد معرف طلب فريد
req.id = uuidv4();
req.startTime = Date.now();
// تسجيل الطلب الوارد
logger.info('طلب وارد', {
requestId: req.id,
method: req.method,
path: req.path,
query: req.query,
ip: req.ip,
userAgent: req.get('user-agent')
});
// تسجيل الاستجابة
const originalSend = res.send;
res.send = function(data) {
const duration = Date.now() - req.startTime;
logger.info('تم إكمال الطلب', {
requestId: req.id,
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration}ms`,
contentLength: res.get('content-length')
});
// تسجيل الطلبات البطيئة
if (duration > 1000) {
logger.warn('تم اكتشاف طلب بطيء', {
requestId: req.id,
method: req.method,
path: req.path,
duration: `${duration}ms`
});
}
return originalSend.call(this, data);
};
next();
});
// middleware تسجيل الأخطاء
app.use((err, req, res, next) => {
logger.error('خطأ في الطلب', {
requestId: req.id,
method: req.method,
path: req.path,
error: {
message: err.message,
stack: err.stack,
code: err.code
},
statusCode: res.statusCode
});
res.status(err.statusCode || 500).json({
error: {
message: err.message,
requestId: req.id
}
});
});
فوائد التسجيل المنظم: السجلات المنسقة بـ JSON قابلة للتحليل بسهولة بواسطة أدوات تجميع السجلات (ELK Stack، Splunk، Datadog). قم بتضمين معرفات الطلبات لتتبع الطلبات عبر الخدمات. سجل دائمًا الطوابع الزمنية للتحليل المستند إلى الوقت.
التسجيل المتقدم مع Morgan
const morgan = require('morgan');
const logger = require('./winston-logger');
// رمز مخصص لمعرف الطلب
morgan.token('id', (req) => req.id);
// رمز مخصص لوقت الاستجابة بالميلي ثانية
morgan.token('response-time-ms', (req, res) => {
const duration = Date.now() - req.startTime;
return `${duration}ms`;
});
// إنشاء تدفق لإرسال سجلات Morgan إلى Winston
const stream = {
write: (message) => logger.http(message.trim())
};
// تنسيق Morgan مع رموز مخصصة
const format = ':id :method :url :status :response-time-ms - :res[content-length]';
// استخدام middleware Morgan
app.use(morgan(format, { stream }));
// تنسيق Morgan مخصص للتسجيل التفصيلي
const detailedFormat = (tokens, req, res) => {
return JSON.stringify({
requestId: tokens.id(req, res),
method: tokens.method(req, res),
url: tokens.url(req, res),
status: tokens.status(req, res),
responseTime: tokens['response-time'](req, res),
contentLength: tokens.res(req, res, 'content-length'),
userAgent: tokens['user-agent'](req, res),
ip: tokens['remote-addr'](req, res)
});
};
app.use(morgan(detailedFormat, { stream }));
مقاييس الأداء
جمع ومراقبة مقاييس الأداء الرئيسية لتحديد الاختناقات وتتبع صحة النظام بمرور الوقت.
تكامل مقاييس Prometheus
const express = require('express');
const prometheus = require('prom-client');
const app = express();
// إنشاء سجل لتسجيل المقاييس
const register = new prometheus.Registry();
// تمكين المقاييس الافتراضية (CPU، الذاكرة، تأخر حلقة الحدث)
prometheus.collectDefaultMetrics({ register });
// مقاييس مخصصة
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'مدة طلبات HTTP بالثواني',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5, 10] // ثوانٍ
});
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'إجمالي عدد طلبات HTTP',
labelNames: ['method', 'route', 'status_code']
});
const activeConnections = new prometheus.Gauge({
name: 'http_active_connections',
help: 'عدد اتصالات HTTP النشطة'
});
const databaseQueryDuration = new prometheus.Histogram({
name: 'database_query_duration_seconds',
help: 'مدة استعلامات قاعدة البيانات بالثواني',
labelNames: ['query_type', 'table'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2]
});
// تسجيل المقاييس المخصصة
register.registerMetric(httpRequestDuration);
register.registerMetric(httpRequestTotal);
register.registerMetric(activeConnections);
register.registerMetric(databaseQueryDuration);
// middleware المقاييس
app.use((req, res, next) => {
activeConnections.inc();
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000; // تحويل إلى ثوانٍ
const route = req.route ? req.route.path : req.path;
httpRequestDuration
.labels(req.method, route, res.statusCode)
.observe(duration);
httpRequestTotal
.labels(req.method, route, res.statusCode)
.inc();
activeConnections.dec();
});
next();
});
// كشف نقطة نهاية المقاييس لـ Prometheus
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
// مثال: تتبع مقاييس استعلام قاعدة البيانات
async function executeQuery(queryType, table, query, params) {
const start = Date.now();
try {
const result = await db.query(query, params);
const duration = (Date.now() - start) / 1000;
databaseQueryDuration
.labels(queryType, table)
.observe(duration);
return result;
} catch (error) {
const duration = (Date.now() - start) / 1000;
databaseQueryDuration
.labels(queryType, table)
.observe(duration);
throw error;
}
}
// الاستخدام
app.get('/api/posts', async (req, res) => {
const posts = await executeQuery('SELECT', 'posts', 'SELECT * FROM posts LIMIT 10');
res.json(posts);
});
المقاييس الرئيسية للتتبع:
- معدل الطلب: الطلبات في الثانية (RPS)
- معدل الخطأ: نسبة الطلبات الفاشلة (4xx، 5xx)
- وقت الاستجابة: نسب P50، P95، P99
- الإنتاجية: البيانات المنقولة في الثانية
- الاتصالات النشطة: عدد الاتصالات المتزامنة
- أداء قاعدة البيانات: مدة الاستعلام، استخدام مجموعة الاتصال
فحوصات الصحة
تنفيذ نقاط نهاية فحص الصحة لمراقبة توفر API والتبعيات.
فحص صحة أساسي
// فحص صحة بسيط
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
فحص صحة شامل
const Redis = require('ioredis');
const mysql = require('mysql2/promise');
const redis = new Redis();
const dbPool = mysql.createPool({ /* config */ });
// فحص صحة مفصل مع فحوصات التبعيات
app.get('/health', async (req, res) => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks: {}
};
// فحص قاعدة البيانات
try {
await dbPool.query('SELECT 1');
health.checks.database = { status: 'healthy', latency: '<50ms' };
} catch (error) {
health.status = 'degraded';
health.checks.database = {
status: 'unhealthy',
error: error.message
};
}
// فحص Redis
try {
const start = Date.now();
await redis.ping();
const latency = Date.now() - start;
health.checks.redis = {
status: 'healthy',
latency: `${latency}ms`
};
} catch (error) {
health.status = 'degraded';
health.checks.redis = {
status: 'unhealthy',
error: error.message
};
}
// فحص استخدام الذاكرة
const memUsage = process.memoryUsage();
health.checks.memory = {
rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`,
heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`
};
// إرجاع 503 إذا كان غير صحي
const statusCode = health.status === 'ok' ? 200 : 503;
res.status(statusCode).json(health);
});
// فحص الجاهزية (هل يمكن للخدمة قبول الحركة؟)
app.get('/ready', async (req, res) => {
try {
// فحص التبعيات الحرجة
await dbPool.query('SELECT 1');
await redis.ping();
res.status(200).json({ ready: true });
} catch (error) {
res.status(503).json({
ready: false,
error: error.message
});
}
});
// فحص الحياة (هل الخدمة قيد التشغيل؟)
app.get('/live', (req, res) => {
res.status(200).json({ alive: true });
});
أفضل ممارسات فحص الصحة: نفذ نقاط نهاية منفصلة /health (حالة مفصلة)، /ready (يمكن قبول الحركة)، و /live (هل العملية حية). يجب أن تكون فحوصات الصحة خفيفة الوزن (<100ms). لا تكشف معلومات حساسة في استجابات فحص الصحة.
تتبع الأخطاء والتنبيه
دمج خدمات تتبع الأخطاء لالتقاط وتجميع والتنبيه على أخطاء الإنتاج.
تكامل Sentry
const Sentry = require('@sentry/node');
const express = require('express');
const app = express();
// تهيئة Sentry
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1, // عينة 10% من المعاملات
integrations: [
// تمكين تتبع HTTP
new Sentry.Integrations.Http({ tracing: true }),
// تمكين تتبع middleware Express.js
new Sentry.Integrations.Express({ app })
]
});
// يجب أن يكون معالج الطلب أول middleware
app.use(Sentry.Handlers.requestHandler());
// معالج التتبع لمراقبة الأداء
app.use(Sentry.Handlers.tracingHandler());
// مساراتك
app.get('/api/posts', async (req, res) => {
try {
const posts = await fetchPosts();
res.json(posts);
} catch (error) {
// التقاط الخطأ يدويًا مع السياق
Sentry.captureException(error, {
tags: {
endpoint: '/api/posts',
method: 'GET'
},
extra: {
userId: req.user?.id,
query: req.query
}
});
res.status(500).json({ error: 'فشل في جلب المنشورات' });
}
});
// يجب أن يكون معالج الأخطاء بعد المسارات
app.use(Sentry.Handlers.errorHandler());
// معالج أخطاء مخصص
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).json({
error: err.message,
requestId: req.id
});
});
نظام تنبيه مخصص
const nodemailer = require('nodemailer');
const logger = require('./winston-logger');
class AlertingService {
constructor() {
this.errorCounts = new Map();
this.alertThreshold = 10; // تنبيه بعد 10 أخطاء في 5 دقائق
this.timeWindow = 5 * 60 * 1000; // 5 دقائق
this.transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
}
async trackError(errorType, error, context = {}) {
const key = `${errorType}:${error.message}`;
const now = Date.now();
if (!this.errorCounts.has(key)) {
this.errorCounts.set(key, []);
}
const timestamps = this.errorCounts.get(key);
timestamps.push(now);
// إزالة الطوابع الزمنية القديمة
const cutoff = now - this.timeWindow;
const recentTimestamps = timestamps.filter(ts => ts > cutoff);
this.errorCounts.set(key, recentTimestamps);
// تحقق من تجاوز العتبة
if (recentTimestamps.length >= this.alertThreshold) {
await this.sendAlert(errorType, error, recentTimestamps.length, context);
this.errorCounts.delete(key); // إعادة تعيين بعد التنبيه
}
}
async sendAlert(errorType, error, count, context) {
const subject = `[تنبيه] ${errorType}: ${error.message}`;
const body = `
نوع الخطأ: ${errorType}
رسالة الخطأ: ${error.message}
الحدوث: ${count} في آخر 5 دقائق
تتبع المكدس: ${error.stack}
السياق:
${JSON.stringify(context, null, 2)}
الوقت: ${new Date().toISOString()}
`;
try {
await this.transporter.sendMail({
from: process.env.ALERT_FROM_EMAIL,
to: process.env.ALERT_TO_EMAIL,
subject,
text: body
});
logger.info('تم إرسال التنبيه', { errorType, count });
} catch (err) {
logger.error('فشل في إرسال التنبيه', { error: err.message });
}
}
// تنظيف عدد الأخطاء القديمة بشكل دوري
cleanup() {
const now = Date.now();
const cutoff = now - this.timeWindow;
for (const [key, timestamps] of this.errorCounts.entries()) {
const recentTimestamps = timestamps.filter(ts => ts > cutoff);
if (recentTimestamps.length === 0) {
this.errorCounts.delete(key);
} else {
this.errorCounts.set(key, recentTimestamps);
}
}
}
}
const alerting = new AlertingService();
// التنظيف كل دقيقة
setInterval(() => alerting.cleanup(), 60000);
// الاستخدام في معالجة الأخطاء
app.use(async (err, req, res, next) => {
logger.error('خطأ API', {
error: err.message,
stack: err.stack,
path: req.path
});
// تتبع الخطأ للتنبيه
await alerting.trackError('API_ERROR', err, {
method: req.method,
path: req.path,
statusCode: res.statusCode
});
res.status(err.statusCode || 500).json({
error: err.message
});
});
مراقبة أداء التطبيق (APM)
توفر أدوات APM رؤى عميقة في أداء التطبيق واستعلامات قاعدة البيانات ومكالمات API الخارجية والتتبع الموزع.
تكامل New Relic
// newrelic.js (يجب أن يكون أول ملف مطلوب)
'use strict';
exports.config = {
app_name: ['My API'],
license_key: process.env.NEW_RELIC_LICENSE_KEY,
logging: {
level: 'info'
},
transaction_tracer: {
enabled: true,
transaction_threshold: 'apdex_f',
record_sql: 'obfuscated'
},
error_collector: {
enabled: true,
ignore_status_codes: [404]
},
distributed_tracing: {
enabled: true
}
};
// server.js
require('newrelic'); // يجب أن يكون الأول!
const express = require('express');
const newrelic = require('newrelic');
const app = express();
// مقاييس مخصصة
app.get('/api/posts', async (req, res) => {
// تسجيل مقياس مخصص
newrelic.recordMetric('Custom/Posts/Fetched', 1);
// إنشاء معاملة مخصصة
const transaction = newrelic.getTransaction();
transaction.acceptDistributedTraceHeaders('HTTP', req.headers);
try {
const posts = await fetchPosts();
// إضافة سمات مخصصة
newrelic.addCustomAttributes({
userId: req.user?.id,
postCount: posts.length,
cached: posts.cached || false
});
res.json(posts);
} catch (error) {
// ملاحظة خطأ (يتم تسجيله تلقائيًا، لكن يمكن إضافة السياق)
newrelic.noticeError(error, {
userId: req.user?.id,
endpoint: '/api/posts'
});
res.status(500).json({ error: 'فشل في جلب المنشورات' });
}
});
// مراقبة وظيفة الخلفية
async function processJob(job) {
// إنشاء معاملة خلفية
return newrelic.startBackgroundTransaction(
'processEmailJob',
async () => {
const transaction = newrelic.getTransaction();
transaction.acceptDistributedTraceHeaders('Other', job.headers);
try {
await sendEmail(job.data);
newrelic.recordMetric('Custom/Email/Sent', 1);
} catch (error) {
newrelic.noticeError(error);
throw error;
}
}
);
}
أدوات APM الشائعة:
- New Relic: APM شامل مع التتبع الموزع
- Datadog: منصة مراقبة كاملة المكدس
- Elastic APM: APM مفتوح المصدر مع تكامل ELK Stack
- AppDynamics: APM على مستوى المؤسسة مع مقاييس الأعمال
تجميع وتحليل السجلات
مركزية السجلات من خدمات متعددة للبحث والتحليل واستكشاف الأخطاء بكفاءة.
تكامل ELK Stack (Elasticsearch، Logstash، Kibana)
// winston-elasticsearch.js
const winston = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');
const esTransportOpts = {
level: 'info',
clientOpts: {
node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200',
auth: {
username: process.env.ES_USERNAME,
password: process.env.ES_PASSWORD
}
},
index: 'api-logs',
indexPrefix: 'api',
indexSuffixPattern: 'YYYY-MM-DD' // فهارس يومية
};
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new ElasticsearchTransport(esTransportOpts),
new winston.transports.Console()
]
});
module.exports = logger;
التسجيل المنظم لتحليل أفضل
const logger = require('./winston-elasticsearch');
// التسجيل بهيكل متسق
app.get('/api/posts/:id', async (req, res) => {
const context = {
requestId: req.id,
userId: req.user?.id,
postId: req.params.id,
ip: req.ip,
userAgent: req.get('user-agent')
};
logger.info('جلب المنشور', context);
try {
const post = await fetchPost(req.params.id);
logger.info('تم جلب المنشور بنجاح', {
...context,
cached: post.cached || false,
responseTime: Date.now() - req.startTime
});
res.json(post);
} catch (error) {
logger.error('فشل في جلب المنشور', {
...context,
error: {
message: error.message,
stack: error.stack,
code: error.code
}
});
res.status(500).json({ error: 'فشل في جلب المنشور' });
}
});
أفضل ممارسات مستويات السجل:
- ERROR: أخطاء التطبيق التي تحتاج إلى اهتمام فوري
- WARN: حالات تحذير قد تؤدي إلى أخطاء
- INFO: أحداث أعمال مهمة (تسجيل دخول المستخدم، تقديم الطلب)
- DEBUG: معلومات تشخيصية مفصلة لتصحيح الأخطاء
- TRACE: معلومات مفصلة جدًا (عادةً معطلة في الإنتاج)
لوحة مراقبة في الوقت الفعلي
// مقاييس في الوقت الفعلي مع Socket.IO
const socketIO = require('socket.io');
const io = socketIO(server);
// جمع المقاييس
const metrics = {
requestsPerSecond: 0,
activeUsers: 0,
averageResponseTime: 0,
errorRate: 0
};
let requestCount = 0;
let responseTimes = [];
// تحديث المقاييس
setInterval(() => {
metrics.requestsPerSecond = requestCount;
metrics.averageResponseTime = responseTimes.length > 0
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
: 0;
// البث إلى جميع العملاء المتصلين
io.emit('metrics', metrics);
// إعادة تعيين العدادات
requestCount = 0;
responseTimes = [];
}, 1000); // كل ثانية
// تتبع الطلبات
app.use((req, res, next) => {
requestCount++;
const start = Date.now();
res.on('finish', () => {
responseTimes.push(Date.now() - start);
});
next();
});
تمرين: نفذ حل مراقبة كامل لـ REST API مع المتطلبات التالية:
الجزء 1: التسجيل
- قم بإعداد مسجل Winston بتنسيق JSON ونقل ملف دوار يومي
- أنشئ middleware تسجيل الطلبات الذي يسجل:
- معرف الطلب (UUID)
- الطريقة، المسار، معلمات الاستعلام
- رمز حالة الاستجابة والمدة
- عنوان IP ووكيل المستخدم
- سجل الطلبات البطيئة (>1 ثانية) كتحذيرات
- سجل الأخطاء مع تتبعات المكدس الكاملة
- دمج عميل Prometheus
- تتبع المقاييس التالية:
- مدة طلب HTTP (مدرج تكراري مع P50، P95، P99)
- إجمالي طلبات HTTP (عداد حسب الطريقة والمسار والحالة)
- الاتصالات النشطة (مقياس)
- مدة استعلام قاعدة البيانات (مدرج تكراري حسب نوع الاستعلام)
- كشف نقطة نهاية /metrics لكشط Prometheus
- نفذ نقطة نهاية /health مع فحوصات قاعدة البيانات و Redis
- نفذ نقطة نهاية /ready (لمجسات جاهزية Kubernetes)
- نفذ نقطة نهاية /live (لمجسات حياة Kubernetes)
- أعد رموز حالة صحيحة (200 صحي، 503 غير صحي)
- أنشئ خدمة تنبيه تتتبع معدلات الخطأ
- أرسل تنبيه بريد إلكتروني عند تجاوز معدل الخطأ 10 أخطاء في 5 دقائق
- قم بتضمين تفاصيل الخطأ وتتبع المكدس والسياق في التنبيه