تنفيذ معرف التتبع من البداية إلى النهاية

Home PDF

تمت كتابة هذه المدونة بمساعدة ChatGPT-4o.


لقد عملت على حل شامل لمعرف التتبع (Trace ID) من البداية إلى النهاية لضمان إمكانية تتبع كل طلب واستجابة في نظامنا بشكل متسق عبر الواجهة الأمامية والخلفية. يساعد هذا الحل في تصحيح الأخطاء، والمراقبة، وتسجيل الأحداث من خلال ربط كل عملية بمعرف تتبع فريد. فيما يلي شرح مفصل لكيفية عمل هذا الحل، مع أمثلة على الكود.

كيف يعمل الحل

  1. إنشاء معرف التتبع (Trace ID):
    • يتم إنشاء معرف تتبع فريد لكل طلب يصل إلى النظام. يمكن أن يكون هذا المعرف عبارة عن UUID أو أي قيمة فريدة أخرى.
    • يتم تمرير هذا المعرف عبر جميع الطبقات والخدمات التي يتفاعل معها الطلب.
  2. تمرير معرف التتبع عبر الطبقات:
    • في الواجهة الأمامية، يتم إرفاق معرف التتبع مع كل طلب يتم إرساله إلى الخادم.
    • في الخلفية، يتم تمرير معرف التتبع عبر جميع الخدمات والمكونات التي تعالج الطلب.
  3. تسجيل الأحداث:
    • يتم تسجيل كل حدث أو عملية مع معرف التتبع الخاص بها. هذا يشمل الطلبات، الاستجابات، الأخطاء، وأي عمليات أخرى.
    • يتم تخزين هذه السجلات في نظام تسجيل مركزي يمكن البحث فيه.
  4. تصحيح الأخطاء والمراقبة:
    • عند حدوث خطأ أو مشكلة، يمكن استخدام معرف التتبع لتتبع مسار الطلب عبر النظام وفهم أين حدثت المشكلة.
    • يمكن أيضًا استخدام معرف التتبع لمراقبة أداء النظام وتحليل سلوك المستخدم.

أمثلة على الكود

إنشاء معرف التتبع في الواجهة الأمامية

function generateTraceId() {
    return 'trace-' + Math.random().toString(36).substr(2, 9);
}

const traceId = generateTraceId();

fetch('/api/endpoint', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-Trace-Id': traceId
    },
    body: JSON.stringify({ data: 'example' })
});

تمرير معرف التتبع في الخلفية (Node.js مثال)

const express = require('express');
const { v4: uuidv4 } = require('uuid');

const app = express();

app.use((req, res, next) => {
    const traceId = req.headers['x-trace-id'] || uuidv4();
    req.traceId = traceId;
    res.setHeader('X-Trace-Id', traceId);
    next();
});

app.post('/api/endpoint', (req, res) => {
    const { traceId } = req;
    console.log(`Processing request with Trace ID: ${traceId}`);
    // معالجة الطلب هنا
    res.json({ status: 'success', traceId });
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

تسجيل الأحداث مع معرف التتبع

const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transports.File({ filename: 'combined.log' })
    ]
});

function logEvent(traceId, message) {
    logger.info({ traceId, message });
}

// مثال على استخدام
logEvent('trace-12345', 'Request received');

الخلاصة

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

كيف يعمل

الواجهة الأمامية (Frontend)

يتضمن الجزء الأمامي من هذا الحل إنشاء معرف تتبع (trace ID) لكل طلب وإرساله مع معلومات العميل إلى الخلفية. يُستخدم هذا المعرف لتتبع الطلب عبر مراحل مختلفة من المعالجة على الخلفية.

  1. جمع معلومات العميل: نقوم بجمع المعلومات ذات الصلة من العميل، مثل أبعاد الشاشة، نوع الشبكة، المنطقة الزمنية، والمزيد. يتم إرسال هذه المعلومات مع رؤوس الطلبات.

  2. إنشاء معرف التتبع (Trace ID): يتم إنشاء معرف تتبع فريد لكل طلب. يتم تضمين هذا المعرف في رؤوس الطلب، مما يسمح لنا بتتبع الطلب خلال دورة حياته.

  3. API Fetch: تُستخدم الدالة apiFetch لإجراء مكالمات API. وهي تقوم بتضمين معرف التتبع (trace ID) ومعلومات العميل في رؤوس (headers) كل طلب.

الواجهة الخلفية (Backend)

يتضمن الجزء الخلفي من الحل تسجيل معرف التتبع (trace ID) مع كل رسالة سجل (log message) وإدراج معرف التتبع في الردود. هذا يسمح لنا بتتبع الطلبات خلال معالجة الخلفية ومطابقة الردود مع الطلبات.

  1. معالجة Trace ID: يستقبل الواجهة الخلفية (Backend) معرف التتبع (Trace ID) من رؤوس الطلبات (Request Headers) أو يقوم بإنشاء واحد جديد إذا لم يتم توفيره. يتم تخزين معرف التتبع في كائن عام (Global Object) في Flask لاستخدامه خلال دورة حياة الطلب.

  2. التسجيل (Logging): تُستخدم مُنسِّقات السجلات المخصصة لتضمين معرف التتبع (Trace ID) في كل رسالة سجل. يضمن ذلك إمكانية ربط جميع رسائل السجل المتعلقة بطلب معين باستخدام معرف التتبع.

  3. معالجة الاستجابة: يتم تضمين معرف التتبع (Trace ID) في رؤوس الاستجابة. إذا حدث خطأ، يتم أيضًا تضمين معرف التتبع في نص استجابة الخطأ للمساعدة في عملية التصحيح.

كيبانا

Kibana هي أداة قوية لتصور بيانات السجلات والبحث فيها المخزنة في Elasticsearch. باستخدام حل Trace ID الخاص بنا، يمكنك بسهولة تتبع الطلبات وتصحيحها باستخدام Kibana. معرّف التتبع (Trace ID)، المضمن في كل إدخال سجل، يمكن استخدامه لتصفية السجلات والبحث عن سجلات محددة.

للبحث عن السجلات التي تحتوي على معرف تتبع محدد، يمكنك استخدام لغة الاستعلام Kibana (KQL). على سبيل المثال، يمكنك البحث عن جميع السجلات المتعلقة بمعرف تتبع معين باستخدام الاستعلام التالي:

trace_id:"Lc6t"

هذا الاستعلام سيعيد جميع إدخالات السجل التي تحتوي على معرف التتبع “Lc6t”، مما يسمح لك بتتبع مسار الطلب عبر النظام. بالإضافة إلى ذلك، يمكنك دمج هذا الاستعلام مع معايير أخرى لتضييق نطاق نتائج البحث، مثل التصفية حسب مستوى السجل، الطابع الزمني، أو كلمات محددة داخل رسائل السجل.

باستخدام إمكانيات التصور في Kibana، يمكنك أيضًا إنشاء لوحات تحكم تعرض المقاييس والاتجاهات بناءً على معرفات التتبع (trace IDs). على سبيل المثال، يمكنك تصور عدد الطلبات المعالجة، متوسط أوقات الاستجابة، ومعدلات الأخطاء، وكلها مرتبطة بمعرفات التتبع الخاصة بها. هذا يساعد في تحديد الأنماط والمشكلات المحتملة في أداء وموثوقية تطبيقك.

استخدام Kibana بالاقتران مع حل Trace ID الخاص بنا يوفر نهجًا شاملاً لمراقبة وتصحيح وتحليل سلوك نظامك، مما يضمن إمكانية تتبع كل طلب بشكل فعال والتحقيق فيه.

الواجهة الأمامية (Frontend)

api.js

const BASE_URL = process.env.REACT_APP_BASE_URL;
// دالة للحصول على معلومات العميل
const getClientInfo = () => {
    const { language, platform, cookieEnabled, doNotTrack, onLine } = navigator;
    const { width, height } = window.screen;
    const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
    const networkType = connection ? connection.effectiveType : 'unknown';
    const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
    const referrer = document.referrer;
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;
};
return {
    screenWidth: width,
    screenHeight: height,
    networkType,
    timeZone,
    language,
    platform,
    cookieEnabled,
    doNotTrack,
    onLine,
    referrer,
    viewportWidth,
    viewportHeight
}; };
// دالة لإنشاء معرف تتبع فريد
export const generateTraceId = (length = 4) => {
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    let traceId = '';
    for (let i = 0; i < length; i++) {
        const randomIndex = Math.floor(Math.random() * characters.length);
        traceId += characters.charAt(randomIndex);
    }
    return traceId;
};
export const apiFetch = async (endpoint, options = {}) => {
    const url = `${BASE_URL}${endpoint}`;
    const clientInfo = getClientInfo();

تم تصدير دالة apiFetch التي تأخذ معاملين: endpoint و options (بافتراض أن options هي كائن فارغ بشكل افتراضي). يتم إنشاء متغير url عن طريق دمج BASE_URL مع endpoint. ثم يتم استدعاء دالة getClientInfo() للحصول على معلومات العميل وتخزينها في متغير clientInfo.

const traceId = options.traceId || generateTraceId();
const headers = {
    'Content-Type': 'application/json',
    'X-Client-Info': JSON.stringify(clientInfo),
    'X-Trace-Id': traceId,
    ...(options.headers || {})
};
const response = await fetch(url, {
    ...options,
    headers
});
    return response;
};

App.js

try {
  const response = await apiFetch('api', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(content),
    traceId: traceId
  });

إذا كانت الاستجابة ناجحة (response.ok)، يتم تحويل البيانات إلى تنسيق JSON ومعالجتها:

if (response.ok) {
  const data = await response.json();
  //...        
}

أما إذا كانت الاستجابة غير ناجحة، يتم تحويل بيانات الخطأ إلى تنسيق JSON، ويتم استخراج رسالة الخطأ أو استخدام رسالة افتراضية في حالة عدم وجود رسالة محددة. ثم يتم إضافة معرف التتبع (Trace ID) إلى رسالة الخطأ وعرضها كإشعار:

else {
  const errorData = await response.json();
  const errorMessage = errorData.message || 'حدث خطأ غير معروف';
  let errorToastMessage = errorMessage;
  errorToastMessage += ` (معرف التتبع: ${traceId})`;
  toast.error(errorToastMessage, {
    autoClose: 8000
  });
  setError(errorToastMessage);
}

في حالة حدوث خطأ أثناء تنفيذ الكود، يتم التحقق مما إذا كان الخطأ من نوع Error، ثم يتم تحويله إلى سلسلة نصية:

catch (error) {
  let errorString = error instanceof Error ? error.message : JSON.stringify(error);
}

const duration = (Date.now() - startTime) / 1000;

if (error.response) {
    // تم إجراء الطلب واستجاب الخادم برمز حالة خارج النطاق 2xx
    errorString += ` (HTTP ${error.response.status}: ${error.response.statusText})`;
    console.error('بيانات خطأ الاستجابة:', error.response.data);
} else if (error.request) {
    // تم إجراء الطلب ولكن لم يتم استقبال أي استجابة
    errorString += ' (لم يتم استقبال أي استجابة)';
    console.error('بيانات خطأ الطلب:', error.request);
} else {
    // حدث خطأ أثناء إعداد الطلب مما أدى إلى حدوث خطأ
    errorString += ` (خطأ في إعداد الطلب: ${error.message})`;
}
errorString += ` (معرف التتبع: ${traceId})`;
if (error instanceof Error) {
    errorString += `\nStack: ${error.stack}`;
}

إذا كان error عبارة عن كائن من نوع Error، يتم إضافة سلسلة نصية تحتوي على \nStack: متبوعة بمحتوى error.stack إلى المتغير errorString.

errorString += JSON.stringify(error);

errorString += ` (المدة: ${duration} ثانية)`;

toast.error(`خطأ: ${errorString}`, {
    autoClose: 8000
  });
  setError(errorString);
} finally {
  toast.dismiss(toastId);
}

الخلفية (Backend)

__init__.py

# -*- encoding: utf-8 -*-
import os
import json
import time
import uuid
import string
import random
from flask import Flask, request, Response, g, has_request_context
from flask_cors import CORS
from .routes import initialize_routes
from .models import db, insert_default_config
import logging
from logging.handlers import RotatingFileHandler
from prometheus_client import Counter, generate_latest, Gauge
from flask_migrate import Migrate
from logstash_formatter import LogstashFormatterV1

تم ترجمة الكود أعلاه إلى:

from .routes import initialize_routes
from .models import db, insert_default_config
import logging
from logging.handlers import RotatingFileHandler
from prometheus_client import Counter, generate_latest, Gauge
from flask_migrate import Migrate
from logstash_formatter import LogstashFormatterV1

ملاحظة: الكود لم يتم ترجمته لأنه يحتوي على أسماء مكتبات ودوال محددة يجب أن تبقى كما هي.

app = Flask(__name__)
app.config.from_object('api.config.BaseConfig')
db.init_app(app)
initialize_routes(app)
CORS(app)
migrate = Migrate(app, db)
class RequestFormatter(logging.Formatter):
    def format(self, record):
        if has_request_context():
            record.trace_id = getattr(g, 'trace_id', 'unknown')
        else:
            record.trace_id = 'unknown'
        return super().format(record)

تمت ترجمة الكود أعلاه إلى:

class RequestFormatter(logging.Formatter):
    def format(self, record):
        if has_request_context():
            record.trace_id = getattr(g, 'trace_id', 'unknown')
        else:
            record.trace_id = 'unknown'
        return super().format(record)

في الكود أعلاه، يتم تعريف فئة RequestFormatter التي ترث من logging.Formatter. يتم تعديل طريقة format لفحص ما إذا كان هناك سياق طلب (request context) متاح. إذا كان متاحًا، يتم تعيين trace_id من الكائن g إلى record.trace_id. إذا لم يكن هناك سياق طلب، يتم تعيين trace_id إلى القيمة 'unknown'. أخيرًا، يتم استدعاء الطريقة الأصلية format من الفئة الأم لإكمال التنسيق.

class CustomLogstashFormatter(LogstashFormatterV1):
    def format(self, record):
        if has_request_context():
            record.trace_id = getattr(g, 'trace_id', 'unknown')
        else:
            record.trace_id = 'unknown'
        return super().format(record)

تمت ترجمة الكود أعلاه إلى:

class CustomLogstashFormatter(LogstashFormatterV1):
    def format(self, record):
        if has_request_context():
            record.trace_id = getattr(g, 'trace_id', 'unknown')
        else:
            record.trace_id = 'unknown'
        return super().format(record)

ملاحظة: تم الحفاظ على الكود كما هو لأنه يحتوي على أسماء دوال ومتغيرات بالإنجليزية، والتي عادةً ما تبقى كما هي في الترجمة.

def setup_loggers():
    logstash_handler = RotatingFileHandler(
        'app.log', maxBytes=100000000, backupCount=1)
    logstash_handler.setLevel(logging.DEBUG)
    logstash_formatter = CustomLogstashFormatter()
    logstash_handler.setFormatter(logstash_formatter)
txt_handler = RotatingFileHandler(
    'plain.log', maxBytes=100000000, backupCount=1)
txt_handler.setLevel(logging.DEBUG)
txt_formatter = RequestFormatter(
    '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d] [trace_id: %(trace_id)s]')
txt_handler.setFormatter(txt_formatter)
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
root_logger.addHandler(logstash_handler)
root_logger.addHandler(txt_handler)

تم الحفاظ على الكود كما هو لأنه يحتوي على أسماء متغيرات ودوال بالإنجليزية ولا يحتاج إلى ترجمة.

app.logger.addHandler(logstash_handler)
app.logger.addHandler(txt_handler)
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.setLevel(logging.DEBUG)
werkzeug_logger.addHandler(logstash_handler)
werkzeug_logger.addHandler(txt_handler)

تم الحفاظ على الكود كما هو لأنه يحتوي على أسماء مكتبات ومتغيرات لا يجب ترجمتها.

setup_loggers()
def generate_trace_id(length=4):
    characters = string.ascii_letters + string.digits
    return ''.join(random.choice(characters) for _ in range(length))

ترجمة:

def generate_trace_id(length=4):
    characters = string.ascii_letters + string.digits
    return ''.join(random.choice(characters) for _ in range(length))

ملاحظة: الكود أعلاه يبقى كما هو لأنه مكتوب بلغة برمجة Python ولا يتم ترجمته. الوظيفة generate_trace_id تقوم بإنشاء معرف تتبع (Trace ID) عشوائي باستخدام أحرف أبجدية وأرقام.

@app.before_request
def before_request():
    request.start_time = time.time()
    trace_id = request.headers.get('X-Trace-Id', generate_trace_id())
    g.trace_id = trace_id

تمت ترجمة الكود أعلاه إلى:

@app.before_request
def before_request():
    request.start_time = time.time()
    trace_id = request.headers.get('X-Trace-Id', generate_trace_id())
    g.trace_id = trace_id

في هذا الكود، يتم تعيين وقت بداية الطلب (start_time) باستخدام time.time()، ثم يتم الحصول على trace_id من رأس الطلب (headers) باستخدام المفتاح X-Trace-Id. إذا لم يتم العثور على X-Trace-Id في الرأس، يتم إنشاء trace_id جديد باستخدام الدالة generate_trace_id(). أخيرًا، يتم تعيين trace_id في الكائن g الذي يمكن الوصول إليه في جميع أنحاء التطبيق.

    client_info = request.headers.get('X-Client-Info')
    if client_info:
        try:
            client_info_json = json.loads(client_info)
            logging.info(f"معلومات العميل: {client_info_json}")
        except json.JSONDecodeError:
            logging.warning("تنسيق JSON غير صالح لرأس X-Client-Info")
@app.after_request
def after_request(response):
    response.headers['X-Trace-Id'] = g.trace_id
    if response.status_code != 200:
        logging.error(f'كود حالة الاستجابة: {response.status_code}')
        logging.error(f'نص الاستجابة: {response.get_data(as_text=True)}')
    if response.content_type == 'application/json':
        try:
            response_json = response.get_json()
            response_json['trace_id'] = g.trace_id
            response.set_data(json.dumps(response_json))
        except Exception as e:
            logging.error(f"خطأ في إضافة trace_id إلى الاستجابة: {e}")

إرجاع الاستجابة ```

السجل

يمكنك البحث عن جميع السجلات المتعلقة بمعرف تتبع معين باستخدام الاستعلام التالي:

trace_id:"Lc6t"
{
  "_index": "flask-logs-2024.07.05",
  "_type": "_doc",
  "_id": "Ae9zgZABqOMSOpxCZC5X",
  "_version": 1,
  "_score": 1,
  "_source": {
    "tags": [
      "_grokparsefailure"
    ],
    "filename": "generate.py",
    "funcName": "post",
    "message": "تمت معالجة الطلب بنجاح",
    "@version": 1,
    "name": "root",
    "host": "ip-172-31-35-xxx.ec2.internal",
    "relativeCreated": 685817.8744316101,
    "levelname": "INFO",
    "created": 1720158740.894831,
    "thread": 139715118360128,
    "threadName": "Thread-5",
    "levelno": 20,
    "pathname": "/home/project/project-name/api/routes/generate.py",
    "msecs": 894.8309421539307,
    "processName": "MainProcess",
    "lineno": 287,
    "path": "/home/project/project-name/app.log",
    "args": [],
    "source_host": "ip-172-31-35-xxx.ec2.internal",
    "module": "generate",
    "trace_id": "Lc6t",
    "stack_info": null,
    "process": 107613,
    "@timestamp": "2024-07-05T05:52:20.894Z"
  },
  "fields": {
    "levelname.keyword": [
      "INFO"
    ],
    "tags.keyword": [
      "_grokparsefailure"
    ],
    "relativeCreated": [
      685817.9
    ],
    "processName.keyword": [
      "MainProcess"
    ],
    "filename.keyword": [
      "generate.py"
    ],
    "funcName": [
      "post"
    ],
    "path": [
      "/home/project/project-name/app.log"
    ],
    "processName": [
      "MainProcess"
    ],
    "@version": [
      1
    ],
    "host": [
      "ip-172-31-35-xxx.ec2.internal"
    ],
    "msecs": [
      894.83093
    ],
    "source_host.keyword": [
      "ip-172-31-35-xxx.ec2.internal"
    ],
    "host.keyword": [
      "ip-172-31-35-xxx.ec2.internal"
    ],
    "levelname": [
      "INFO"
    ],
    "process": [
      107613
    ],
    "threadName.keyword": [
      "Thread-5"
    ],
    "trace_id": [
      "Lc6t"
    ],
    "source_host": [
      "ip-172-31-35-xxx.ec2.internal"
    ],
    "created": [
      1720158700
    ],
    "module": [
      "generate"
    ],
    "module.keyword": [
      "generate"
    ],
    "name.keyword": [
      "root"
    ],
    "thread": [
      139715118360128
    ],
    "message": [
      "تمت معالجة الطلب بنجاح"
    ],
    "levelno": [
      20
    ],
    "trace_id.keyword": [
      "Lc6t"
    ],
    "threadName": [
      "Thread-5"
    ],
    "pathname": [
      "/home/project/project-name/api/routes/generate.py"
    ],
    "tags": [
      "_grokparsefailure"
    ],
    "pathname.keyword": [
      "/home/project/project-name/api/routes/generate.py"
    ],
    "@timestamp": [
      "2024-07-05T05:52:20.894Z"
    ],
    "filename": [
      "generate.py"
    ],
    "lineno": [
      287
    ],
    "message.keyword": [
      "تمت معالجة الطلب بنجاح"
    ],
    "name": [
      "root"
    ],
    "funcName.keyword": [
      "post"
    ],
    "path.keyword": [
      "/home/project/project-name/app.log"
    ]
  }
}

كما هو موضح أعلاه، يمكنك رؤية معرف التتبع (trace ID) في السجل.


Back 2025.01.18 Donate