ai-ml

تتبع استدعاءات أدوات Claude Agent باستخدام OpenTelemetry (2026)

١٥ يوليو ٢٠٢٦

Trace Claude Agent Tool Calls with OpenTelemetry (2026)

عندما يتصرف وكيل استدعاء الأدوات (tool-calling agent) بشكل خاطئ — يتم اختيار أداة خاطئة، أو يستغرق الاستدعاء ثماني ثوانٍ، أو تكلف العملية عشرة أضعاف ما توقعت — فإن تتبع console.log البسيط نادراً ما يخبرك بأي خطوة تسببت في ذلك. تمنح الاصطلاحات الدلالية (semantic conventions) لـ GenAI في OpenTelemetry عمليات تشغيل الوكيل شكلاً قياسياً: span واحد لكل استدعاء نموذج، وspan واحد لكل استدعاء أداة، وspan أب واحد للعملية بأكملها، وجميعها تحمل نفس أسماء سمات gen_ai.* بغض النظر عن النموذج أو الإطار الذي تستخدمه1.

ملخص

ستقوم ببناء وكيل TypeScript صغير يستدعي Claude API باستخدام أداة واحدة، وتغلفه بثلاثة أنواع من spans الخاصة بـ OpenTelemetry: invoke_agent للعملية بأكملها، وchat لكل استدعاء نموذج، وexecute_tool لكل استدعاء أداة. يحمل كل span سمات gen_ai.* الحالية — بما في ذلك gen_ai.provider.name، التي حلت محل gen_ai.system بهدوء في المواصفات2 — ويقوم SpanProcessor مخصص بقراءة سمات استخدام التوكنات (token-usage) مباشرة من الـ spans المنتهية لطباعة تكلفة دولارية حقيقية لكل استدعاء. لا يوجد جامع (collector)، لا Docker، ولا SDK من مورد — فقط @opentelemetry/API ومصدرر console. بيئة التشغيل: @anthropic-ai/sdk@0.111.0، @opentelemetry/API@1.9.1، Node.js 20+. وقت البناء: 25-30 دقيقة.

ما ستتعلمه

  • كيف تقوم الاصطلاحات الدلالية لـ OpenTelemetry GenAI بتسمية الـ spans والسمات لاستدعاءات النماذج، وتشغيل الوكلاء، واستدعاءات الأدوات1
  • كيفية تسجيل Node tracer provider وطباعة spans حقيقية في الـ console بدون أي بنية تحتية خارجية
  • كيفية تغليف حلقة استدعاء أدوات Claude في spans من نوع invoke_agent وchat وexecute_tool التي تتداخل بشكل صحيح عبر حدود await
  • كيفية قراءة سمات gen_ai.usage.* من span منتهي وتحويلها إلى تكلفة دولارية فعلية باستخدام SpanProcessor مخصص
  • لماذا لم تعد gen_ai.system هي السمة المطلوب استخدامها، وما الذي حل محلها
  • كيفية توجيه هذه الـ spans نفسها إلى backend حقيقي بمجرد انتهائك من مرحلة النمذجة الأولية (prototyping)

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

  • Node.js 20 أو أحدث (Node 24 هو خط Active LTS الحالي وقت كتابة هذا المقال؛ انتقل Node 22 إلى Maintenance LTS في 2025-10-21)3
  • @anthropic-ai/sdk@0.111.0 — الـ SDK الأساسي لـ Messages API، وليس Agent SDK؛ هذه حلقة استدعاء أدوات بسيطة بدون تدخل إطار عمل للوكلاء4
  • @opentelemetry/API@1.9.1، و@opentelemetry/sdk-trace-node@2.9.0، و@opentelemetry/resources@2.9.04
  • TypeScript 7.0.2 مع تفعيل strict وnoUncheckedIndexedAccess4
  • مفتاح ANTHROPIC_API_KEY إذا كنت تريد تشغيل هذا على API المباشر — الكود أدناه تم التحقق من أنواعه مقابل الـ SDK الحقيقي ومنطق التتبع فيه تم تنفيذه من البداية للنهاية مقابل استجابات وهمية (stubbed responses)؛ انظر إلى قسم التحقق لمعرفة ما تم تشغيله بالضبط وما لم يتم تشغيله

لماذا لا نكتفي بتسجيل استدعاءات الأدوات؟

سطر السجل (log line) يخبرك بأن شيئاً ما حدث. أما الـ span فيخبرك أين يقع بالنسبة لكل شيء آخر — أي استدعاء نموذج تبعه، وأي استدعاء أداة تسبب فيه، وكم استغرق من الوقت بالنسبة للأب الخاص به، وكيف يتصل بـ spans من أجزاء أخرى من نظامك إذا كنت تستخدم OpenTelemetry لأي شيء آخر.

تقوم الاصطلاحات الدلالية لـ GenAI بتوحيد هذا الشكل خصيصاً لعمل LLM: الـ span من نوع chat يحمل دائماً gen_ai.request.model وgen_ai.usage.output_tokens بنفس أسماء السمات سواء كنت تستدعي Claude أو GPT أو Gemini، لذا فإن لوحة التحكم (dashboard) المبنية لمورد واحد تعمل غالباً مع الآخرين أيضاً1. هذه القابلية للنقل هي الهدف الأساسي من الاصطلاح الدلالي — ولهذا السبب توجد المواصفات كمفردات مشتركة بدلاً من أن يخترع كل مورد لغته الخاصة.

لا تزال اصطلاحات GenAI في تطور — حتى منتصف عام 2026 ظلت في حالة "تطوير" (Development) بشكل عام، رغم أن الـ spans الأساسية لاستدعاءات العميل خرجت من الحالة التجريبية في وقت سابق من العام5. توقع استمرار تغير أسماء السمات في الهوامش؛ تم التحقق من هذا البرنامج التعليمي مقابل المواصفات المنشورة في 2026-07-15، وقد حدث تغيير واحد بالفعل يربك الكثير من البرامج التعليمية الحالية: gen_ai.system اختفت من السجل الحالي، وحلت محلها gen_ai.provider.name2.

الخطوة 1: إعداد المشروع

mkdir claude-agent-tracing && cd claude-agent-tracing
npm init -y
npm install @anthropic-ai/sdk@0.111.0 @opentelemetry/API@1.9.1 \
  @opentelemetry/sdk-trace-node@2.9.0 @opentelemetry/resources@2.9.0
npm install -D TypeScript@7.0.2 tsx@4.23.1 @types/node@26.1.1

أضف "type": "module" إلى package.json — كل حزمة هنا تشحن بصيغة ESM، وخلط أنظمة الوحدات (module systems) هو المكان الذي تفشل فيه معظم هذه البرامج التعليمية. ثم أنشئ tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"]
  }
}

إعداد moduleResolution: "bundler" هو ما يجعل الأمر بسيطاً — فهو يسمح لـ tsc بحل صادرات حزمة الـ SDK بنفس الطريقة التي يفعلها tsx عند التشغيل، لذا فإن ما يتم التحقق من نوعه هو ما يعمل فعلياً.

الخطوة 2: تسجيل tracer

يقوم ملف tracing.ts بإعداد NodeTracerProvider مع ConsoleSpanExporter بحيث تُطبع الـ spans في الـ terminal الخاص بك — لا جامع، لا Jaeger، ولا استدعاءات شبكة. يجب تشغيل هذا الملف قبل أي شيء آخر ينشئ span، لذا تقوم كل الملفات الأخرى باستيراده أولاً:

// tracing.ts
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import {
  ConsoleSpanExporter,
  SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-node";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { trace } from "@opentelemetry/API";
import { CostLoggingProcessor } from "./cost-processor.js";

const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({
    "service.name": "weather-agent",
  }),
  spanProcessors: [
    new SimpleSpanProcessor(new ConsoleSpanExporter()),
    new CostLoggingProcessor(),
  ],
});

provider.register();

export const tracer = trace.getTracer("weather-agent", "1.0.0");

يقوم SimpleSpanProcessor بتصدير كل span في اللحظة التي ينتهي فيها، وهو ما تريده لعرض توضيحي في الـ terminal — في بيئة الإنتاج ستستخدم BatchSpanProcessor ومصدر OTLP بدلاً من ذلك، وهو بالضبط شكل خط الأنابيب القائم على Collector6. يأخذ spanProcessors مصفوفة، لذا لا شيء يمنعك من تشغيل مصدر console ومعالج مخصص جنباً إلى جنب — هذا هو دور CostLoggingProcessor، وستقوم بكتابته في الخطوة 5.

الخطوة 3: بناء حلقة الوكيل المزودة بأدوات القياس

يمتلك الوكيل أداة واحدة (get_weather، تعيد بيانات ثابتة حتى لا يعتمد البرنامج التعليمي على API حقيقي للطقس) وحلقة تستدعي Claude، وتنفذ أي استدعاءات أدوات يطلبها، وتكرر ذلك حتى يتوقف Claude عن الطلب. كل طبقة تحصل على الـ span الخاص بها:

// agent.ts
import Anthropic from "@anthropic-ai/sdk";
import type { MessageParam, Tool } from "@anthropic-ai/sdk/resources/messages";
import { tracer } from "./tracing.js";
import { SpanStatusCode, type Span } from "@opentelemetry/API";
import { randomUUID } from "node:crypto";

const MODEL = "claude-sonnet-5";

const WEATHER_TOOL: Tool = {
  name: "get_weather",
  description: "Get the current weather for a city.",
  input_schema: {
    type: "object",
    properties: {
      city: { type: "string", description: "City name, e.g. 'Cairo'" },
    },
    required: ["city"],
  },
};

function getWeather(city: string): string {
  const fixtures: Record<string, string> = {
    Cairo: "32°C, clear skies",
    Dublin: "14°C, light rain",
  };
  return fixtures[city] ?? `${city}: 21°C, partly cloudy`;
}

/** Runs each tool call inside its own `execute_tool {name}` span. */
function runTool(name: string, id: string, input: unknown): string {
  return tracer.startActiveSpan(`execute_tool ${name}`, (span: Span) => {
    span.setAttribute("gen_ai.operation.name", "execute_tool");
    span.setAttribute("gen_ai.tool.name", name);
    span.setAttribute("gen_ai.tool.call.id", id);
    try {
      const city = (input as { city: string }).city;
      const result = getWeather(city);
      span.setStatus({ code: SpanStatusCode.OK });
      return result;
    } catch (err) {
      span.recordException(err as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw err;
    } finally {
      span.end();
    }
  });
}

/** Runs one model turn inside a `chat {model}` span with GenAI attributes. */
async function chatTurn(
  client: Anthropic,
  messages: MessageParam[],
): Promise<Anthropic.Message> {
  return tracer.startActiveSpan(`chat ${MODEL}`, async (span: Span) => {
    span.setAttribute("gen_ai.operation.name", "chat");
    span.setAttribute("gen_ai.provider.name", "anthropic");
    span.setAttribute("gen_ai.request.model", MODEL);
    try {
      const response = await client.messages.create({
        model: MODEL,
        max_tokens: 1024,
        tools: [WEATHER_TOOL],
        messages,
      });

      span.setAttribute("gen_ai.response.model", response.model);
      span.setAttribute("gen_ai.usage.input_tokens", response.usage.input_tokens);
      span.setAttribute("gen_ai.usage.output_tokens", response.usage.output_tokens);
      if (response.usage.cache_creation_input_tokens) {
        span.setAttribute(
          "gen_ai.usage.cache_creation.input_tokens",
          response.usage.cache_creation_input_tokens,
        );
      }
      if (response.usage.cache_read_input_tokens) {
        span.setAttribute(
          "gen_ai.usage.cache_read.input_tokens",
          response.usage.cache_read_input_tokens,
        );
      }
      if (response.stop_reason) {
        span.setAttribute("gen_ai.response.finish_reasons", [response.stop_reason]);
      }
      span.setStatus({ code: SpanStatusCode.OK });
      return response;
    } catch (err) {
      span.recordException(err as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw err;
    } finally {
      span.end();
    }
  });
}

/** Root span for the whole agent turn — one or more chat turns plus any tool calls. */
export async function runAgent(client: Anthropic, userMessage: string): Promise<string> {
  return tracer.startActiveSpan(`invoke_agent weather-assistant`, async (root: Span) => {
    root.setAttribute("gen_ai.operation.name", "invoke_agent");
    root.setAttribute("gen_ai.agent.name", "weather-assistant");
    root.setAttribute("gen_ai.conversation.id", randomUUID());

    const messages: MessageParam[] = [{ role: "user", content: userMessage }];

    try {
      for (let turn = 0; turn < 4; turn++) {
        const response = await chatTurn(client, messages);
        messages.push({ role: "assistant", content: response.content });

        if (response.stop_reason !== "tool_use") {
          const text = response.content
            .filter((b): b is Anthropic.TextBlock => b.type === "text")
            .map((b) => b.text)
            .join("\n");
          root.setStatus({ code: SpanStatusCode.OK });
          return text;
        }

        const toolResults: Anthropic.ToolResultBlockParam[] = [];
        for (const block of response.content) {
          if (block.type === "tool_use") {
            const result = runTool(block.name, block.id, block.input);
            toolResults.push({
              type: "tool_result",
              tool_use_id: block.id,
              content: result,
            });
          }
        }
        messages.push({ role: "user", content: toolResults });
      }
      throw new Error("Agent did not finish within the turn budget");
    } catch (err) {
      root.recordException(err as Error);
      root.setStatus({ code: SpanStatusCode.ERROR });
      throw err;
    } finally {
      root.end();
    }
  });
}

هناك تفصيلان يستحقان الذكر. أولاً، tracer.startActiveSpan يأخذ callback غير متزامن (async) هنا، ولا يزال التداخل يعمل بشكل صحيح عبر كل await — حيث يقوم مدير السياق في Node بنشر الـ span النشط من خلال AsyncLocalStorage، لذا فإن الـ span من نوع chat الذي يبدأ داخل callback الخاص بـ runAgent، والـ span من نوع execute_tool الذي يبدأ داخل حلقة بعد ذلك، كلاهما يلتقطان الأب الصحيح تلقائياً. أنت لا تمرر span أو كائن سياق (context object) يدوياً أبداً.

ثانياً، كل اسم لسمة (attribute) في الـ span هنا هو عبارة عن سلسلة نصية بسيطة — "gen_ai.usage.input_tokens"، وليس ثابتاً مستورداً. يمكنك استيراد ثوابت مثل ATTR_GEN_AI_USAGE_INPUT_TOKENS من @opentelemetry/semantic-conventions@1.43.0، ولكن فقط من المسار الفرعي /incubating — فحص الحزمة مباشرة يظهر أن ثوابت GenAI موجودة في experimental_attributes، ويتم إعادة تصديرها من خلال @opentelemetry/semantic-conventions/incubating، وليس من خلال التصدير الرئيسي المستقر للحزمة7. كتابة السلاسل النصية يدوياً تتجنب الاعتماد على استيراد مُصنف كـ "قيد التطوير" (incubating) لـ 13 اسماً من السمات التي يحددها هذا البرنامج التعليمي، رغم أن الثوابت خيار معقول إذا كنت تفضل أن يقوم tsc باكتشاف أي خطأ إملائي في اسم السمة بدلاً منك.

الخطوة 4: التشغيل وقراءة التتبع (trace)

قم بتوصيل نقطة دخول حقيقية:

// index.ts
import "./tracing.js"; // side-effecting import: register the tracer FIRST
import Anthropic from "@anthropic-ai/sdk";
import { runAgent } from "./agent.js";

const client = new Anthropic();

const answer = await runAgent(client, "What's the weather in Cairo?");
console.log("\n=== Final answer ===");
console.log(answer);
export ANTHROPIC_API_KEY=sk-ant-...
npx tsx index.ts

لاحظ ترتيب الاستيراد في index.ts: يأتي ./tracing.js أولاً، في سطر مستقل، رغم أن agent.ts يستورده أيضاً. من الناحية التقنية، تقوم مواصفات ES module بتقييم التبعيات قبل تشغيل الكود العلوي للوحدة المستوردة، لذا فإن الاستيراد غير المباشر من agent.ts سيسجل الـ tracer في الوقت المناسب في كلتا الحالتين — لكن الاعتماد على ذلك أمر هش بمجرد أن يقوم شخص ما بإعادة ترتيب الاستيرادات أثناء إعادة هيكلة الكود (refactor). استيراد tracing.js بشكل صريح، وأولاً، في كل نقطة دخول هو النمط الذي لا يتسبب في أعطال لاحقاً.

بما أن خط الإنتاج الآلي هذا لا يملك ANTHROPIC_API_KEY فعال، فقد تم التحقق من منطق التتبع نفسه مقابل عميل وهمي (stubbed client) يعيد استجابات تجريبية مصممة تماماً مثل الـ API الحقيقي — نفس حقول usage و stop_reason و content التي يعيدها الـ SDK الحقيقي. إليك مخرجات الكونسول الحقيقية من ذلك التشغيل، والتي تظهر span واحد من نوع chat، و span واحد من نوع execute_tool، و span ثانٍ من نوع chat، والـ span الرئيسي invoke_agent، بترتيب انتهائها:

{
  name: 'chat claude-sonnet-5',
  traceId: 'bfae7c5f1f4e5ca6ccd96ed8c6eb1c2a',
  attributes: {
    'gen_ai.operation.name': 'chat',
    'gen_ai.provider.name': 'anthropic',
    'gen_ai.request.model': 'claude-sonnet-5',
    'gen_ai.response.model': 'claude-sonnet-5',
    'gen_ai.usage.input_tokens': 512,
    'gen_ai.usage.output_tokens': 41,
    'gen_ai.response.finish_reasons': [ 'tool_use' ]
  },
  status: { code: 1 }
}
{
  name: 'execute_tool get_weather',
  traceId: 'bfae7c5f1f4e5ca6ccd96ed8c6eb1c2a',
  attributes: {
    'gen_ai.operation.name': 'execute_tool',
    'gen_ai.tool.name': 'get_weather',
    'gen_ai.tool.call.id': 'toolu_01demo'
  },
  status: { code: 1 }
}
{
  name: 'chat claude-sonnet-5',
  traceId: 'bfae7c5f1f4e5ca6ccd96ed8c6eb1c2a',
  attributes: {
    'gen_ai.operation.name': 'chat',
    'gen_ai.provider.name': 'anthropic',
    'gen_ai.request.model': 'claude-sonnet-5',
    'gen_ai.response.model': 'claude-sonnet-5',
    'gen_ai.usage.input_tokens': 568,
    'gen_ai.usage.output_tokens': 19,
    'gen_ai.response.finish_reasons': [ 'end_turn' ]
  },
  status: { code: 1 }
}
{
  name: 'invoke_agent weather-assistant',
  traceId: 'bfae7c5f1f4e5ca6ccd96ed8c6eb1c2a',
  attributes: {
    'gen_ai.operation.name': 'invoke_agent',
    'gen_ai.agent.name': 'weather-assistant',
    'gen_ai.conversation.id': '1d6f4098-6663-4f30-a0a4-70f06fba80c4'
  },
  status: { code: 1 }
}

=== Final answer ===
It's 32°C and clear in Cairo right now.

(تم اختصار الحقول التي تهمنا في هذا الشرح — مخرجات ConsoleSpanExporter الحقيقية تتضمن أيضاً spanId و parentSpanContext و duration و instrumentationScope في كل span.) تشترك جميع الـ spans الأربعة في traceId واحد، وهو ما يجعل هذا "تتبعاً" (trace) بدلاً من أربعة أسطر سجلات غير مرتبطة — و parentSpanContext.spanId في كل من الـ spans الثلاثة التابعة (الظاهرة في المخرجات غير المختصرة) يطابق الـ id الخاص بـ span الـ invoke_agent، مما يؤكد أن التداخل من الخطوة 3 يعمل فعلياً عند التشغيل، وليس فقط في هيكل الكود. status: { code: 1 } تعني SpanStatusCode.OK — القيمة 0 تعني غير محدد، و 2 تعني خطأ.

الخطوة 5: تحويل سمات الـ span إلى قيمة مالية

الفائدة من وضع gen_ai.usage.* على الـ span بدلاً من مجرد تسجيلها بشكل منفصل: أي معالج (processor) لاحق — سواء كان هذا المعالج أو معالج OTel Collector حقيقي — يمكنه حساب تكلفة المكالمة دون لمس كود التطبيق الخاص بك على الإطلاق.

// cost-processor.ts
import type { ReadableSpan, SpanProcessor } from "@opentelemetry/sdk-trace-node";

// Verified against platform.claude.com/docs/en/about-claude/pricing (2026-07-15).
// USD per million tokens. Sonnet 5 is in its introductory pricing window
// through 2026-08-31; it becomes $3 / $15 after that.
const PRICE_PER_MTOK: Record<string, { input: number; output: number }> = {
  "claude-sonnet-5": { input: 2, output: 10 },
  "claude-opus-4-8": { input: 5, output: 25 },
  "claude-haiku-4-5": { input: 1, output: 5 },
};

export class CostLoggingProcessor implements SpanProcessor {
  onStart(): void {}

  onEnd(span: ReadableSpan): void {
    if (!span.name.startsWith("chat ")) return;

    const model = span.attributes["gen_ai.response.model"] as string | undefined;
    const inputTokens = span.attributes["gen_ai.usage.input_tokens"] as number | undefined;
    const outputTokens = span.attributes["gen_ai.usage.output_tokens"] as number | undefined;
    const rates = model ? PRICE_PER_MTOK[model] : undefined;
    if (!rates || inputTokens === undefined || outputTokens === undefined) return;

    const cost =
      (inputTokens / 1_000_000) * rates.input + (outputTokens / 1_000_000) * rates.output;

    console.log(
      `[cost] ${span.name}: ${inputTokens} in + ${outputTokens} out tokens ≈ $${cost.toFixed(6)}`,
    );
  }

  shutdown(): Promise<void> {
    return Promise.resolve();
  }

  forceFlush(): Promise<void> {
    return Promise.resolve();
  }
}

قم بتشغيل نفس العرض التوضيحي مرة أخرى وستظهر سطران إضافيان، يتم حسابهما مباشرة عند انتهاء كل span من نوع chat:

[cost] chat claude-sonnet-5: 512 in + 41 out tokens ≈ $0.001434
[cost] chat claude-sonnet-5: 568 in + 19 out tokens ≈ $0.001326

هذا يمثل تقريباً 0.0028 دولار للتشغيل الكامل المكون من جولتين — عملية بحث واحدة عن الطقس، وإجابة نهائية واحدة. هناك نقطة هامة يجب معرفتها قبل أن تتفاجأ بفاتورة حقيقية: بمجرد أن تكون tools غير فارغة، يضيف Claude عبئاً ثابتاً من توكنات "مطالبة النظام" (system-prompt) لكل مكالمة — من 354 إلى 474 توكن لـ Sonnet 5، اعتماداً على tool_choice4. هذا غير مرئي في المطالبة التي كتبتها، ولكنه حقيقي، ويتم فوترته، وهو بالضبط نوع الأشياء التي لن تلاحظها إلا من خلال مراقبة gen_ai.usage.input_tokens على الـ span بدلاً من تقدير عدد التوكنات من نص رسالتك الخاص.

التحقق

ما تم تشغيله فعلياً: ملفات tracing.ts و cost-processor.ts و agent.ts و index.ts — الملفات الأربعة معاً، بما يطابق ما هو معروض أعلاه، بالإضافة إلى نظام اختبار داخلي خامس يقوم بعمل stub لـ client.messages.create — تم فحص أنواعها باستخدام tsc --noEmit (strict: true, noUncheckedIndexedAccess: true) مقابل حزم @anthropic-ai/sdk@0.111.0 و @opentelemetry/* الحقيقية والمثبتة بالإصدارات المحددة في المتطلبات الأساسية. لم تظهر أي أخطاء.

تم تنفيذ منطق التتبع نفسه عبر tsx مقابل client.messages.create وهمي يعيد استجابتين تجريبيتين مصممتين مثل الـ API الحقيقي (نفس حقول usage و stop_reason و content) — هذا هو التشغيل الذي جاءت منه مخرجات الكونسول في الخطوة 4، وتم إعادة إنتاجها حرفياً، بما في ذلك حسابات التكلفة التي تم التحقق منها يدوياً: 512 / 1e6 × 2 + 41 / 1e6 × 10 = 0.001434، 568 / 1e6 × 2 + 19 / 1e6 × 10 = 0.001326.

ما لم يتم تشغيله: مكالمة client.messages.create() الحية مقابل API Claude الحقيقي في index.ts. خط إنتاج الكتابة الآلي هذا لا يملك ANTHROPIC_API_KEY. إذا كنت تملك واحداً، فإن تشغيل npx tsx index.ts يجب أن يتصرف بشكل مطابق لمخرجات الخطوة 4 — نفس شكل الـ span، ونفس أسماء السمات — مع أعداد توكنات حقيقية واستجابة نموذج حقيقية بدلاً من البيانات التجريبية.

استكشاف الأخطاء وإصلاحها

تُطبع الـ spans بدون أب (parent)، رغم أنك استدعيتها داخل runAgent. من المحتمل أنك تستدعي tracer.startSpan() بدلاً من tracer.startActiveSpan(). تقوم startSpan بإنشاء span ولكنها لا تجعله السياق النشط، لذا فإن أي شيء يبدأ بعدها لن يجد أباً ليرتبط به. تقوم startActiveSpan بكلا الأمرين في استدعاء واحد — استخدمها إلا إذا كان لديك سبب محدد لعدم القيام بذلك.

الـ span من نوع chat يفتقد لـ gen_ai.usage.* تماماً. تأكد من أنك تقرأ response.usage، وليس شيئاً من استجابة HTTP الخام — نوع Message في الـ SDK يقوم بالفعل بتحليل usage.input_tokens و usage.output_tokens كأرقام، لذا إذا كانت قيمتها undefined، فمن المحتمل أن كائن الاستجابة نفسه ليس هو ما تعتقده (على سبيل المثال، خطأ تم التقاطه ومعاملته كاستجابة).

أسطر التكلفة لا تُطبع أبداً. يتم تفعيل CostLoggingProcessor.onEnd فقط للـ spans التي تبدأ باسم chat * — إذا قمت بتغيير اسم الـ span، فقم بتحديث فحص startsWith أيضاً. تأكد أيضاً من أن المعالج مسجل بالفعل: تأخذ spanProcessors في NodeTracerProvider مصفوفة، ونسيان إضافة معالجك المخصص بجانب console exporter هو طريقة سهلة لفقدانه بصمت (لا يوجد خطأ، فقط لا تظهر أسطر التكلفة).

الـ spans من نوع execute_tool تتداخل تحت الـ span الخاطئ من نوع chat، أو لا تتداخل على الإطلاق. هذا يعني عادةً أن استدعاء الأداة يتم إطلاقه من خارج حلقة for في runAgent — على سبيل المثال، في Promise.all التي تشغل أدوات متعددة دون تغليف كل واحدة منها في استدعاء tracer.startActiveSpan خاص بها من داخل السياق النشط. يحتاج كل استدعاء أداة إلى بدء الـ span الخاص به بينما لا يزال سياق الأب نشطاً في السلسلة غير المتزامنة (async chain) الحالية.

كل شيء يتم تجميعه (compiles) ولكن لا شيء يُطبع عند التشغيل. تأكد مرة أخرى من أن tracing.ts مستورد بالفعل — ومستورد أولاً — في أي ملف تقوم بتشغيله. المزود (provider) الذي لم يتم تسجيله أبداً لا يسبب خطأ، بل يقوم ببساطة بتجاهل كل span تنشئه بصمت.

هل gen_ai.provider.name هو نفسه gen_ai.system؟

gen_ai.provider.name هو السمة الحالية لتحديد مزود الذكاء الاصطناعي (anthropic, openai, gcp.vertex_ai) في الـ span — أما gen_ai.system فقد أصبحت مهجورة ولا تظهر في سجل السمات الحالي على الإطلاق2. ومن الناحية العملية، فإن الكثير من مكتبات التتبع (instrumentation libraries) لم تنتهِ من عملية الانتقال بعد — فلا تزال تقارير الأخطاء الحقيقية ضد SDKs المزودين وأطر عمل الوكلاء (agent frameworks) في منتصف عام 2026 تطلب إضافة gen_ai.provider.name و gen_ai.operation.name لأن المكتبة لا تزال تصدر التنسيق القديم8.

إذا كنت تتبع درساً تعليمياً قديماً، أو كتاب وصفات (cookbook)، أو مكتبة تتبع تلقائي لا تزال تضبط gen_ai.system، فهذا لا يعني بالضرورة أنها معطلة — بل ببساطة لم يتم تحديثها بعد — ولكن من الجدير التحقق من الاسم الذي تصدره مكتبة معينة فعلياً قبل بناء لوحة بيانات (dashboard) أو ضبط تنبيه بناءً على أحدهما.

هذا أيضاً تذكير بأن اتفاقيات GenAI لا تزال في حالة تغير فعلي: لقد انتقلت المواصفات بالكامل من موقع opentelemetry.io/docs/specs/semconv/ الرئيسي إلى مستودع مخصص لها، وصفحات مرجع السمات القديمة هناك تقول الآن ببساطة "انتقلت" (Moved)1. لذا قم بحفظ المستودع الجديد في إشاراتك المرجعية، وليس الرابط القديم.

الخطوات التالية وقراءات إضافية

يقوم هذا الدرس التعليمي ببناء الحلقة من رسائل (Messages) الخام API — إذا لم تكن قد بنيت وكيلاً لاستدعاء الأدوات (tool-calling agent) من قبل وتريد معرفة آليات التعامل مع tool_use/tool_result بعمق أكبر، فإن درس الحلقة الوكيلية لاستخدام أدوات Claude يغطي ذلك أولاً.

بمجرد انتهائك من العرض التجريبي على الطرفية (terminal demo) ورغبتك في رؤية هذه الـ spans نفسها في نظام خلفي حقيقي — سواء كان Jaeger أو Honeycomb أو Datadog أو أي شيء يدعم OTLP — استبدل ConsoleSpanExporter في الخطوة 2 بمصدر تصدير OTLP موجه إلى Collector؛ حيث يغطي درس OpenTelemetry Collector + تتبع Node.js هذا الجزء من خط الأنابيب: إعدادات الـ Collector، ومعالج batch، وعرض النتيجة في واجهة مستخدم Jaeger.

وإذا كان هذا الوكيل يحتاج إلى موافقة بشرية على استدعاءات الأدوات قبل تنفيذها — وهو أمر مفيد بمجرد أن تبدأ الـ spans الخاصة بـ execute_tool في إظهار استدعاءات لم تتوقعها — راجع إضافة موافقة "البشر في الحلقة" (human-in-the-loop) إلى Claude Agent SDK.

بالنسبة للمواصفات نفسها، فإن مستودع الاتفاقيات الدلالية لـ GenAI هو المصدر الأساسي الآن بعد انتقال الوثائق من الموقع الرئيسي1، وصفحة سجل السمات هي أسرع طريقة للتحقق مما إذا كان اسم gen_ai.* معين لا يزال سارياً قبل شحنه في كود الإنتاج2.

الحواشي السفلية

  1. OpenTelemetry, مستودع الاتفاقيات الدلالية لـ GenAI — https://GitHub.com/open-telemetry/semantic-conventions-genai (رابط المخطط https://opentelemetry.io/schemas/gen-ai/1.42.0؛ المصدر المعتمد وقت كتابة هذا النص — صفحات دليل opentelemetry.io/docs/specs/semconv/gen-ai/ القديمة تعيد التوجيه الآن إلى إشعار "انتقلت" يشير إلى هنا؛ تم جلب البيانات في 2026-07-15) 2 3 4 5

  2. OpenTelemetry, سجل سمات Gen AI — https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/ (جدول سمات gen_ai.* الكامل مع الأوصاف وقيم أمثلة، بما في ذلك gen_ai.provider.name؛ لا تظهر gen_ai.system في هذا الجدول؛ تم جلب البيانات في 2026-07-15) 2 3 4

  3. endoflife.date, Node.js — https://endoflife.date/API/v1/products/nodejs (Node 24: Active LTS، نافذة الدعم من 2025-10-28 إلى 2026-10-20؛ Node 22: انتقل إلى Maintenance LTS في 2025-10-21؛ تم جلب البيانات في 2026-07-15)

  4. Anthropic, التسعير — https://platform.claude.com/docs/en/about-claude/pricing (Claude Sonnet 5: 2$/10$ لكل مليون توكن مدخلات/مخرجات، عرض تقديمي حتى 2026-08-31، ثم 3$/15$؛ جدول استهلاك توكنات system-prompt لاستخدام الأدوات؛ تم جلب البيانات في 2026-07-15 مباشرة من صفحة التسعير، وليس من ملخص بحث). إصدارات الحزم — npm view <package> version مقابل سجل npm مباشرة: @anthropic-ai/sdk@0.111.0, @opentelemetry/API@1.9.1, @opentelemetry/sdk-trace-node@2.9.0, @opentelemetry/resources@2.9.0, @opentelemetry/semantic-conventions@1.43.0, TypeScript@7.0.2, tsx@4.23.1, @types/node@26.1.1 (جميعها جُلبت في 2026-07-15). 2 3 4

  5. بناءً على ملخص مجمع من WebSearch لـ opentelemetry.io/blog/2026/genai-observability/، يصف حالة "التطوير" العامة لاتفاقيات GenAI و MCP اعتباراً من حوالي مايو 2026، مع خروج الـ spans الخاصة بالعميل (model-call) من الحالة التجريبية في وقت سابق من عام 2026. تم إدراج ذلك كسياق عام حول نضج المواصفات وليس كنسبة مئوية دقيقة للاستقرار من مصدر أساسي — تعامل معه كدليل توجيهي وليس كادعاء مؤرخ بدقة.

  6. تعريفات الأنواع المجمعة لـ @anthropic-ai/sdk@0.111.0, resources/messages/messages.d.ts (نوع الاتحاد Model الذي يسرد سلاسل معرفات الموديلات الحرفية بما في ذلك claude-sonnet-5, claude-opus-4-8, و claude-haiku-4-5؛ تم التثبيت من npm والفحص مباشرة، 2026-07-15). راجع أيضاً درس OpenTelemetry Collector + تتبع Node.js لمعرفة نمط BatchSpanProcessor + مصدر تصدير OTLP المشار إليه في الخطوة 2.

  • @opentelemetry/semantic-conventions@1.43.0، تم تثبيته من npm وفحصه مباشرة (2026-07-15): حقل exports في package.json يحدد مدخلاً مستقراً "." (build/src/index.d.ts) ومدخلاً منفصلاً "./incubating" (build/src/index-incubating.d.ts). ثوابت ATTR_GEN_AI_* (بما في ذلك ATTR_GEN_AI_PROVIDER_NAME و ATTR_GEN_AI_USAGE_INPUT_TOKENS و ATTR_GEN_AI_OPERATION_NAME) معرفة في experimental_attributes.d.ts، والتي يقوم index-incubating.d.ts بإعادة تصديرها عبر export * from './experimental_attributes' — وهي غائبة عن index.d.ts المستقر. لا يوجد ثابت ATTR_GEN_AI_SYSTEM في أي من الملفين (فقط ATTR_GEN_AI_SYSTEM_INSTRUCTIONS غير المرتبط به)، وهو ما يتفق مع كون gen_ai.system قد تم إهماله (deprecated) بدلاً من مجرد كونه غير مُصدر.

  • GitHub، مشكلة livekit/agents رقم 4639، "Missing required OpenTelemetry GenAI attributes (gen_ai.provider.name, gen_ai.operation.name)" — https://GitHub.com/livekit/agents/issues/4639 (مثال واقعي لـ spans في إطار عمل agent تفتقد أسماء السمات الحالية وقت تقديم المشكلة؛ تم جلبها في 2026-07-15). الإطار العام الذي ينص على أن "gen_ai.system تم إهماله لصالح gen_ai.provider.name، ولا يزال التبني متأخراً عبر مكتبات التجهيز (instrumentation libraries)" مدعوم بملخص مجمع من WebSearch يشير إلى هذه المشكلة وغيرها؛ ويتم التعامل معه على أنه دقيق من حيث الاتجاه وليس كجدول زمني دقيق للانتقال مستند إلى مصدر أولي.