In short: The OpenTelemetry PHP SDK is stable, but the GenAI attribute names are still at Development status and changed again in October. Record finish reasons and token counters on every call, and keep metrics, sampled spans, captured content and evaluation scores as four separate signals.
Picture a support summariser four months into production. The dashboard is calm: p95 latency around 1.8 seconds, provider error rate at zero, no failed jobs in the queue. Then a colleague on the support team mentions, almost in passing, that the summaries sometimes stop mid-sentence.
Work out what that would take. A max_tokens value chosen in the first week no longer suits a model that has since become more verbose, and perhaps one call in six is hitting the ceiling. The provider returns HTTP 200 every time. The response carries a finish reason saying the output was cut short, and nobody recorded that field.
The scenario is constructed and its numbers are invented, but the shape is ordinary. Nothing in that telemetry is wrong. It answers questions about transport and timing, and the failure is somewhere else.
Instrumenting an AI feature is mostly a series of decisions about what to record, and the current conventions will not make those decisions for you.
Is OpenTelemetry stable enough for AI features?
It helps to separate two things that are often discussed together.
The OpenTelemetry PHP documentation lists traces, metrics and logs as stable. You install the SDK from Packagist with Composer and add an exporter for your backend.[1] That part is settled enough to build on.
The GenAI semantic conventions are a different matter. As of late October 2026 they remain at Development status. The v1.42.0 release of the core semantic conventions on 12 June 2026 deprecated every gen_ai.* attribute, metric, event and span there and moved them to a dedicated repository.[2] The schema URL section of that repository is still marked TODO.[3] The history since then shows how fast the names move. On 22 September the token usage metrics were redesigned. On 6 October inference duration got a metric of its own. On 9 October the token histograms gained error.type, and the event that carries chat history went from opt-in to recommended. On 21 October the usage counters stopped recording zero values.[4]
Both statements can be true at once, and the practical consequence is specific: the pipeline is safe to build on, the field names are not.
So do not scatter gen_ai.* string literals through application code. Put one mapping layer between your call sites and the wire format, pin the convention snapshot you built against, and record that snapshot identifier as your own attribute. The examples here use acme. as a stand-in for a prefix you own:
acme.genai.semconv_snapshot = "2026-10-17"
That single field turns a future rename from an archaeology problem into a query. When the conventions move and half your dashboards go quiet, you can see which emitters are still on the old snapshot rather than guessing from deploy timestamps. Use the date of the last repository change you read, and review it whenever you upgrade an instrumentation library.
What four questions does AI observability answer?
Teams generally want four things from AI observability, and vendors tend to sell them as one product. Keeping them separate is what makes the rest of the design tractable.
| Question | Signal | Cadence | Retention | Usual reader |
|---|---|---|---|---|
| Did the call happen, and what did it cost? | Metrics | Every call | Long, cheap | Platform and finance |
| What happened inside this particular request? | Spans | Sampled | Medium | Engineer during an investigation |
| What text went in and came out? | Content records | Opt-in, governed | Short, restricted | Incident handling |
| Was the answer any good? | Evaluation results | Offline, sampled | Separate store | Product and quality review |

Each has a different cost profile, a different privacy class and a different level of trustworthiness. Collapsing them into one pipeline means the most sensitive and least reliable data inherits the retention and alerting behaviour of the cheapest and most reliable.
Why should token cost live in metrics, not in sampled spans?
A cost dashboard can quietly become a sampling estimate.
Token counts get attached to the model call span, which is reasonable. The spans are then head-sampled to control ingest cost, which is also reasonable. The cost dashboard reads from spans, so it is now reporting a sample. Whether anyone multiplied it back up correctly, and whether the sample represents the expensive requests, are questions the dashboard does not raise.
Head sampling makes its decision early, without seeing the complete trace.[5] Tail sampling can decide after seeing the spans, at the cost of a stateful component that has to buffer traces, which the documentation describes as operationally demanding and potentially needing many nodes.[5] If your question is what a sampled trace can prove about runtime behaviour, rather than what it costs, the sampling section of[6] covers that side.
The defensible claim is narrower than "sampling loses your bill". A sampled trace dataset is not automatically an exact billing source, and reading totals off one without saying so is the error. A tail policy can preserve expensive requests for investigation, but retaining every request above a threshold still omits the usage below it. A span-to-metrics connector can preserve token totals if it receives every relevant span before any sampling or filtering. The Collector's signal-to-metrics connector, currently at alpha, accepts an expression that reads a value from each span, which is what summing tokens needs.[7] That arrangement depends on complete upstream delivery; changing the sampling policy downstream of it does not change the aggregate.
Field note. In a test with the OpenTelemetry PHP SDK (version 1.15.0), each run sent 4,000 synthetic model calls through a head sampler that kept one trace in four. Five per cent of the calls carried a long retrieved context of 20,000 to 40,000 input tokens, and the rest were small. A counter on the same calls matched the true total in all 100 runs. The tokens summed from the kept spans came to between 19 and 30 per cent of the true total. Multiplied back up by four, they still landed anywhere between 77 and 120 per cent of it, because the result depends on how many of the few expensive calls the sampler happened to keep. The calls were invented, so the exact range is not a prediction. The width of it is the point.
The conventions already provide the right instruments, although they have just changed shape. Until 22 September 2026, token usage was a single histogram, gen_ai., split by a gen_ai.token.type attribute. It is now two families.[8]
- Counters such as
gen_ai.andclient. inference. usage. input_tokens gen_ai.accumulate tokens over time. They requireclient. inference. usage. output_tokens gen_ai.token.modalityalongside the operation and provider names, and the conventions describe them as a proxy for approximating cost. - Histograms such as
gen_ai.describe how usage is distributed across individual calls. The conventions say they should not be used for totals or cost.client. inference. operation. input_tokens
Since 9 October both families are meant to cover failed operations too. They treat missing usage differently, and the rule changed again on 21 October: the per-operation histograms record 0 when the provider returned no usage, while the usage counters record only known, strictly positive contributions.[8] A failed call with no reported usage therefore adds nothing to the counters, so unknown consumption remains a limit on any cost estimate.
Inference duration is no longer gen_ai.. Since 6 October the conventions define gen_ai., a histogram in seconds, and say the older metric should not be reported for inference operations.[9][10] Every one of these is Development status.
This replacement shows why the mapping belongs in one place. An application with the old name scattered through its call sites now has a search to run. A central adapter contains the change, and the snapshot attribute identifies the emitters that have migrated.
Record available token usage on every call, independently of span sampling. The span carries the narrative for one request. The counters record aggregate token usage for all of them.

Turning that usage into money also needs the applicable model prices and billing categories. Equal input totals can cost different amounts when cache treatment, modality or pricing differs. The cache and reasoning counters describe subsets of the input and output totals, so do not add them again. Count the totals only, and calculate the invoice elsewhere. Budgeting time and spend per request, before the call is made, is the subject of[11].
The trade is that metrics are aggregates. A jump in the token counters does not name the request that caused it. If you need that link, you need exemplars or a retained span, and you should decide which before the first incident rather than during it.
How do you keep metric cardinality under control?
Every distinct combination of metric attribute values creates a separate time series. The data model offers reaggregation as a way of shedding unwanted attributes downstream, which is a repair rather than a plan.[12]
Fix the dimensions deliberately. For an AI feature, these are usually safe on the instruments that define them:
gen_ai.operation.namegen_ai.provider.namegen_ai.request.modelgen_ai.response.modelerror.type- One application-owned feature identifier
These are not:
- Conversation, user, session or response identifiers
- Prompt hashes
- Tool names, when users or tenants can define tools
- Any model version string that changes on every provider deployment
One subtlety is better caught in code than in review: the instruments do not take the same attributes. error.type is conditionally required on the duration histogram and, since 9 October, on the per-operation token histograms.[8][9] It is defined on neither usage counter. gen_ai.token.modality is required on the usage counters but deliberately left off the per-operation histograms, because percentiles across modalities do not add up.[8] A shared dimension list, the obvious way to write this, gets both wrong: it multiplies the token series on every error path and makes the histogram percentiles meaningless. Keep one permitted set per instrument.
Field note. In the same test setup, one token counter fed by 5,000 calls across 3 models and 4 features produced 12 time series. Adding error.type, with five error values, raised that to 72. Adding the response identifier, which looks harmless on a dashboard, produced 5,000 series from 5,000 calls, one per request.
The failure mode is not an exception. It is a larger invoice, then a cardinality limit on the backend, then missing series during the incident where you needed them, and by then the fix is retroactive. Make it fail earlier instead. The sketch later in this article refuses an unrecognised dimension instead of dropping it, so the mistake breaks a test on a developer's machine.
Should you capture prompts and responses?
Usually not by default. Message content is treated as opt-in throughout the conventions. The content attributes, including gen_ai.input.messages, gen_ai.output.messages and gen_ai., carry an Opt-In requirement level. The two message attributes come with an explicit warning that they are likely to contain sensitive information including personal data, and system instructions with a softer one.[9]
The environment variable most instrumentations use for this is OTEL_. The conventions mention it only as an example of an explicit opt-in, in the notes on the memory attributes.[13] An issue filed in September 2026 asks for a formal description, and points out that two existing descriptions of it disagree; it was still open at the time of the check.[14] The Python GenAI utilities define a content-capturing mode with the values NO_CONTENT, SPAN_ONLY, EVENT_ONLY and SPAN_AND_EVENT.[15] The older description treated the variable as a boolean, and at least one vendor integration guide now warns that setting it to true under the latest conventions makes the configuration invalid, so no log or trace data is collected.[16] Treat the variable as an enumeration, not a switch, and check which reading your instrumentation library implements.
These are not free text. The conventions define messages as structured values: a list of message objects, each with a role, normally one of four well-known values, and a list of typed parts. Parts are discriminated, and there are eleven shapes, not one, covering text, tool calls and their responses, blobs, files, URIs and more.[17] A flat list of strings produces a payload the conventions do not define, which no backend built against them will read.
If you validate content yourself, name the subset you support and refuse everything else with an error that says so. A validator that silently drops what it does not understand will lose your tool calls without telling you.
There is a separate event, gen_ai., for exactly this purpose. It describes chat history and request parameters so that inputs and outputs can be stored independently of traces.[9] Use it instead of inflating spans, because it lets the content stream have its own retention and access rules. The event itself became Recommended on 9 October, but the content attributes on it are still Opt-In, so recommending the event does not switch capture on.[9]
The Collector's redaction processor can remove unauthorised attribute keys and mask values matching blocked patterns, with an allowlist that denies by default. Its own documentation describes it as one line of defence, not a complete compliance measure.[18] That framing is correct, and it bears repeating internally. A processor that strips a field you should never have collected is a safety net, not the control. The decision belongs at the SDK boundary, in code, under test. The same rule applies to the audit trail of an agent that runs commands on a server: log identifiers and bounded excerpts, not prompts and full output, as[19] explains.
Which spans should you write yourself?
Auto-instrumentation gives you the provider call. It cannot give you the thing your product actually does.
A realistic tree for the summariser looks like this:
POST /support/tickets/{id}/summary server span
summarise_ticket application operation
retrieval help-index gen_ai.operation.name = retrieval
chat <model> gen_ai.operation.name = chat
execute_tool lookup_order gen_ai.operation.name = execute_tool
The inference span name follows {gen_ai., and the well-known operation values include chat, embeddings, retrieval, execute_tool, invoke_agent and create_agent, among others.[20] Both gen_ai.operation.name and gen_ai.provider.name are required on inference spans, with error.type conditionally required when the operation ends in error.[9] The provider's own field, gen_ai., is recommended on the same span, with length among the conventions' own examples.[9] That is the field the opening story never recorded.
The span the on-call engineer wants is summarise_ticket, and no library will create it. It is the one that knows a retry happened, that retrieval returned nothing useful, and that the feature abstained. Write it yourself.
Give it the fields the provider cannot know:
acme.feature = "ticket_summary"
acme.outcome = "answered" | "abstained" | "failed"
acme.attempts = 2
acme.evidence_count = 3
acme.truncated = true
None of these are convention attributes, and they should not pretend to be. Use a prefix you actually own, which is what the naming guidance asks for.[21] A bare app. is owned by nobody, so two libraries in one process can both claim app.feature and mean different things. Between them these fields answer the question an incident starts with, which is whether the feature did its job, without reading a single prompt.
Retries need an explicit decision, and the conventions have a position on one half of it. A GenAI span should cover the logical operation, including automatic retries inside the client library, and the inference duration metric should equal the span duration.[13][9] That keeps the histogram honest about what the user waited for. What it hides is how many attempts it took, so record an attempt count, and add a child span or span event for each attempt if you need the detail. Retries that your own code decides on, such as asking again after a truncated answer, are separate inference calls under summarise_ticket, and the attempts attribute on that parent counts them. Whichever convention you choose, write it down, or you get two dashboards that disagree and nobody able to say which is right.
How do you test the telemetry rules in PHP?
You can test most of these rules without an SDK, a collector or a network. Put a small class between your call sites and the exporter, and make it throw on anything the rules do not allow. This sketch covers two of them: permitted dimensions per instrument, and attribute limits that count characters.
<?php
declare(strict_types=1);
final class TelemetryRules
{
private const COMMON = ['gen_ai.operation.name', 'gen_ai.provider.name', 'gen_ai.request.model', 'acme.feature'];
// One permitted set per instrument, not one shared list.
private const PERMITTED = [
'gen_ai.client.inference.usage.input_tokens' => [...self::COMMON, 'gen_ai.token.modality'],
'gen_ai.client.inference.usage.output_tokens' => [...self::COMMON, 'gen_ai.token.modality'],
'gen_ai.client.inference.operation.input_tokens' => [...self::COMMON, 'error.type'],
'gen_ai.client.inference.operation.output_tokens' => [...self::COMMON, 'error.type'],
'gen_ai.client.inference.duration' => [...self::COMMON, 'error.type'],
];
/** Refuse an unknown dimension instead of dropping it. */
public static function dimensions(string $metric, array $dimensions): array
{
$permitted = self::PERMITTED[$metric]
?? throw new InvalidArgumentException("Unknown metric: $metric");
$extra = array_diff(array_keys($dimensions), $permitted);
if ($extra !== []) {
throw new InvalidArgumentException("$metric does not take: " . implode(', ', $extra));
}
return $dimensions;
}
/** Cut on characters first, then on an application-owned byte ceiling, never mid-character. */
public static function limit(string $value, int $maxChars = 4096, int $maxBytes = 8192): string
{
if (preg_match('//u', $value) !== 1) {
throw new InvalidArgumentException('Not valid UTF-8');
}
$out = '';
$count = 0;
foreach (preg_split('//u', $value, -1, PREG_SPLIT_NO_EMPTY) as $char) {
if ($count === $maxChars || strlen($out) + strlen($char) > $maxBytes) {
break;
}
$out .= $char;
$count++;
}
return $out;
}
}
The attribute rules come from the specification, not from taste. Permitted values are primitives, homogeneous arrays of primitives, byte arrays, arrays of AnyValue and string-keyed maps. The default AttributeCountLimit is 128, and AttributeValueLengthLimit counts characters for a string and bytes for a byte array.[22]
That distinction is easy to get wrong and invisible in English. Truncating text on bytes cuts Japanese at a third of the stated limit and Greek at a half, while ASCII looks correct. Apply the character limit the specification describes, and keep a byte ceiling beside it as an explicitly application-owned policy, because an exporter queue is sized in bytes. Set that ceiling below four times the character limit, or it can never fire, since UTF-8 uses at most four bytes per character.
A short script holds the rules in place. Save it beside the class. It prints one line per check and exits with an error if a rule stops working, for example when the matching line is removed from the class:
<?php
declare(strict_types=1);
require __DIR__ . '/telemetry-rules.php';
function check(bool $ok, string $label): void
{
echo ($ok ? 'ok ' : 'FAIL ') . $label . "\n";
if (!$ok) {
exit(1);
}
}
function refuses(callable $fn): bool
{
try {
$fn();
} catch (InvalidArgumentException) {
return true;
}
return false;
}
$counter = 'gen_ai.client.inference.usage.input_tokens';
$base = ['gen_ai.operation.name' => 'chat', 'gen_ai.provider.name' => 'acme.ai'];
// A token counter takes the modality but not error.type.
check(TelemetryRules::dimensions($counter, $base + ['gen_ai.token.modality' => 'text']) !== [], 'counter takes modality');
check(refuses(fn () => TelemetryRules::dimensions($counter, $base + ['error.type' => 'timeout'])), 'counter refuses error.type');
// 5,000 Japanese characters stop at 4,096 characters when the byte ceiling is out of the way,
// and at 2,730 whole characters (8,190 bytes) under an 8,192-byte ceiling.
$japanese = str_repeat('日', 5000);
check(strlen(TelemetryRules::limit($japanese, 4096, 16384)) === 3 * 4096, 'character limit binds');
check(strlen(TelemetryRules::limit($japanese, 4096, 8192)) === 8190, 'byte ceiling stops on a character boundary');
The sketch cannot tell you whether your dimensions are the right ones for your product, cannot measure real cardinality against real traffic, and does not implement exporting, batching, context propagation or sampling. Those belong to the SDK. What it does is turn a set of conventions into assertions that fail on a laptop.
How do you stop telemetry breaking the feature it measures?
The refuse-not-drop rule has an obvious objection, and it deserves a direct answer. If the contract throws on an unrecognised dimension, and that code runs inside a request, then a telemetry mistake can take down a working feature. That trade is not worth making.
The resolution is that refusal and production are two different moments. The contract throws so that a developer's test fails and a build stops. In the request path, the call is wrapped, and the wrapper never rethrows.
Wrap the two failures separately, because they are different faults with different owners:
$checked = null;
try {
$checked = TelemetryRules::dimensions($metric, $dimensions);
} catch (Throwable $e) {
noteDropped('contract');
}
if ($checked !== null) {
try {
$recorder->record($metric, $value, $checked);
} catch (Throwable $e) {
noteDropped('record');
}
}
// The summary still returns. Telemetry is never the reason a feature fails.
A single try around both would file an exporter outage under contract and send someone to read validation code during a collector incident.
noteDropped() needs care of its own, because it is itself telemetry. A counter that throws from inside a catch block turns a swallowed telemetry fault into an uncaught application fault, which is what the wrapper exists to prevent. Make it best-effort and non-throwing.
The counter makes dropped telemetry visible: a silent catch turns one bug into two, because the feature works and its telemetry is quietly missing. Keep its reasons to a small fixed set, such as contract, record and limit. Treat it as a hint rather than the system of record, though. The SDK and the Collector publish their own self-telemetry about queue depth and export failures, and that does not depend on a failing path reporting its own failure.
This is the one place in the design where dropping data is right. Everywhere else a dropped measurement costs you accuracy in a dashboard; here the alternative costs you the request.
The same applies to the exporter. A collector that is unreachable or slow must not add latency to a user-facing call, and batching alone does not guarantee that. In version 1.15.0 of the OpenTelemetry PHP SDK the batch span processor exports on the request thread: by default it flushes when a batch fills or the scheduled delay has passed, and again at shutdown, and each export waits for the exporter.[23] Use a bounded queue and a short export timeout, accept that the queue drops on overflow and record that it did, and measure the time a request spends in export under realistic load. An AI feature already has one slow network dependency it cannot avoid. Do not give it a second one it could.
Why keep evaluation results out of the telemetry pipeline?
The conventions define a gen_ai.evaluation.result event. It requires gen_ai.evaluation.name, conditionally requires error.type, gen_ai. and gen_ai., and recommends gen_ai. and gen_ai.response.id. The guidance is that it should be parented to the GenAI operation span being evaluated when possible, or carry gen_ai.response.id when the span ID is not available.[24]
Note what that guidance does not say. There is no span_id attribute on the event. Correlation comes from emitting the event inside the evaluated span's context, or from gen_ai.response.id when that context is gone, which for an offline evaluator is the normal case. A span ID placed in the payload as a field looks equivalent and is not: nothing downstream reads it as a parent link.
That correlation is useful. It is also the reason the two signals get merged, and merging them causes four specific problems.
Evaluations arrive late. A score attached to a trace that closed two hours ago is a reasonable record and a misleading dashboard, because the time axis now means two different things in two adjacent panels.
Evaluations are usually sampled, often heavily. A quality line drawn from one per cent of traffic, sitting beside a latency line drawn from all of it, invites a comparison neither supports.
Many evaluations are produced by a model. The score is an opinion with a version number, and that version is a hidden dimension in your series. When the grader changes, quality appears to move. Record the evaluator name and version as attributes, or the series is not comparable across time.
Explanation fields contain generated prose about user content. They belong to the same privacy class as captured messages, and they frequently end up with the retention settings of ordinary telemetry because they arrived through the ordinary pipeline.
Keep the trace and response identifiers shared, put evaluation results in their own store, and keep them off the alerting path until the evaluator has been measured against labelled examples. Share the identifiers, not the pipeline. How to build and check the evaluator itself is the subject of[25].
Which alerts can you defend?
| Signal | Response | Why it holds up |
|---|---|---|
Provider error rate by error.type |
Page | Deterministic and directly attributable |
| Operation duration p95 and p99 | Page | Deterministic and user-visible |
| Token usage per request, p99 | Ticket | Catches runaway prompts and context growth |
| Application abstention rate | Ticket | Application-defined, deterministic, product-meaningful |
| Truncated-output rate | Ticket | Catches the failure that opened this article |
| Evaluation score | No page | Sampled, often model-graded, version-sensitive |
The last two rows are the point. The truncation problem was never an evaluation problem. Response finish reasons are a deterministic field returned by the provider on a successful call, and a rate derived from them would have surfaced the issue in the first week without any judge, any labelled set or any additional model spend.
That is generally where the return is. Before reaching for a quality model, check whether the provider already told you what happened and nobody wrote it down.
Where should the telemetry go, and what will it cost?
Everything above leaves the application as OpenTelemetry data, so the choice of destination can come later and can change. The realistic options fall into three groups. These are examples of each group, not a ranking.
- Your own stack. An OpenTelemetry Collector in front of open-source storage and dashboards that you run. You pay in machines and in the hours of keeping it running. The Collector is also where the redaction processor and the span-to-metrics connector mentioned earlier would run.
- General observability platforms that accept OTLP. Grafana Cloud, Datadog and Honeycomb each document how to send OpenTelemetry data to them.[26][27][28] They differ in what they count. Grafana Cloud bills metrics on active series and data points per minute, and an active series is one that received data in the previous 20 minutes.[29] Extra dimensions like those in the field note above can raise Grafana Cloud's active-series usage, but the bill follows how long the series stay active and a monthly 95th-percentile calculation, so a brief spike counts for less than a permanent dimension. Datadog identifies a custom metric by its name plus its tag values, so the same multiplication is at work there; read its billing page for how they are counted.[30]
- LLM-specific tools. Langfuse accepts OTLP traces over HTTP, documents how it maps
gen_ai.*attributes, and supports self-hosting.[31] Arize Phoenix accepts traces over OTLP and builds on OpenInference instrumentation.[32] Check whether such a tool takes metrics as well as traces, because the token counters in this article are metrics.
Whatever you shortlist, ask the same questions:
- Which signals does it accept over OTLP: traces only, or metrics and logs as well?
- What does it count for billing: events, active series, gigabytes ingested or hosts?
- Can you keep metrics unsampled while sampling spans?
- Where do content records live, who can read them, and can their retention differ from the metrics?
- Does it read the
gen_ai.*names, and which snapshot of them? A tool that expects last quarter's names will show your new counters as unknown series. - Can you leave? Data that leaves as OTLP can be pointed somewhere else, so keep the vendor's own SDK out of the call sites.
Prices and plans change, so read each vendor's own pages for your region and volume.
What will this not tell you?
Telemetry built this way still cannot say whether an answer was correct, whether retrieval returned the best passage, or whether a user read the summary and gave up. It will not attribute cost along a dimension you did not choose in advance, and it will not detect a slow drift in quality that produces no error, no truncation and no latency change.
Those need evaluation, product analytics and human review, which is the argument for keeping them in separate pipelines with their own honest labels, instead of one dashboard that implies more than it measures.
It will also not tell you what it costs to run. Telemetry spend arrives on a different invoice from model spend, which is how it escapes notice for a quarter. The dimensions and retention chosen here are the two levers that decide it, so estimate them in the design review instead of discovering them in a bill.
A useful question to ask before shipping an AI feature: if this degrades in the three most likely ways, which recorded field changes? Name the field and the query. If the answer for any of them is that nothing visibly changes, the instrumentation is not finished, whatever the span count suggests.
References
Numbers in square brackets in the text link to these sources.
- OpenTelemetry: PHP (opens in a new tab).Used for: the stable status of PHP traces, metrics and logs.
- OpenTelemetry Semantic Conventions: v1.42.0 release notes (opens in a new tab).Used for: the 12 June 2026 move of the GenAI conventions.
- OpenTelemetry: GenAI Semantic Conventions repository (opens in a new tab).Used for: the Development status and the unfinished schema URL.
- OpenTelemetry GenAI Semantic Conventions: commit history of docs/gen-ai (opens in a new tab).Used for: the September and October changes to metrics and events.
- OpenTelemetry: Sampling (opens in a new tab).Used for: how head and tail sampling decide, and what tail sampling costs.
- iLogr: Building AI-Assisted Infrastructure Documentation That Does Not Drift.Used for: sampling as evidence of runtime behaviour.
- OpenTelemetry Collector Contrib: Signal to Metrics Connector (opens in a new tab).Used for: its alpha stability and value expressions that can sum span data.
- OpenTelemetry: GenAI Inference Token Metrics (opens in a new tab).Used for: token counters, histograms, their attributes and failed-call recording.
- OpenTelemetry: GenAI Client Inference (opens in a new tab).Used for: the inference span, duration metric, content attributes and details event.
- OpenTelemetry: GenAI Metrics (opens in a new tab).Used for: the note that the older duration metric is not for inference.
- iLogr: Controlling LLM API Cost and Latency in PHP.Used for: budgeting time and spend per request.
- OpenTelemetry: Metrics Data Model (opens in a new tab).Used for: reaggregation as a downstream repair for unwanted attributes.
- OpenTelemetry: GenAI Spans (opens in a new tab).Used for: spans covering retries, and the capture variable named as an example.
- OpenTelemetry GenAI Semantic Conventions: issue 497, formally describe the content capture environment variable (opens in a new tab).Used for: the request to describe the variable, still open.
- OpenTelemetry Python Contrib: GenAI Utilities (opens in a new tab).Used for: the four content-capturing mode names.
- Google Cloud Observability: Instrument ADK applications with OpenTelemetry (opens in a new tab).Used for: the warning against setting the capture variable to true.
- OpenTelemetry GenAI: input messages JSON schema (opens in a new tab).Used for: the four roles and eleven part shapes of structured messages.
- OpenTelemetry Collector Contrib: Redaction Processor (opens in a new tab).Used for: key allowlists, value masking and its own one-line-of-defence wording.
- iLogr: AI Agents on Linux Servers: Permissions, Sandboxing and Audit Logs.Used for: what to log, and what not to, for agents on servers.
- OpenTelemetry GenAI: attribute registry (opens in a new tab).Used for: the well-known values of gen_ai.operation.name.
- OpenTelemetry: Semantic Conventions Naming Guidance (opens in a new tab).Used for: choosing a prefix you own for custom attributes.
- OpenTelemetry: Common Specification, Attributes (opens in a new tab).Used for: permitted attribute value types and the count and length limits.
- OpenTelemetry PHP: BatchSpanProcessor source (opens in a new tab).Used for: when the batch processor exports during a request.
- OpenTelemetry: GenAI Events (opens in a new tab).Used for: the evaluation result event and how it correlates.
- iLogr: An Evaluation Harness for AI Features Before Production.Used for: building and checking an evaluator before it reaches dashboards.
- Grafana Cloud: send OTLP data (opens in a new tab).Used for: its OTLP endpoint for metrics, logs and traces.
- Datadog: OpenTelemetry (opens in a new tab).Used for: sending OpenTelemetry data through a Collector or directly over OTLP.
- Honeycomb: OpenTelemetry (opens in a new tab).Used for: OTLP ingestion over gRPC and HTTP, including metrics.
- Grafana Cloud: metrics invoice (opens in a new tab).Used for: billing on active series and data points per minute.
- Datadog: custom metrics (opens in a new tab).Used for: a custom metric as a metric name plus tag values.
- Langfuse: OpenTelemetry integration (opens in a new tab).Used for: its OTLP traces endpoint, gen_ai mapping and self-hosting.
- Arize Phoenix documentation (opens in a new tab).Used for: OTLP trace ingestion and OpenInference instrumentation.