Instrument the prompt id as an attribute on the LLM span, not as a separate log line. Attributes are the cheapest and highest-correlation place to put data you will later ask questions of: a single event with feature flags, request properties and user ids lets you ask "what is different about the requests that failed" (Honeycomb on span versus attribute). Span events, by contrast, are being deprecated in favor of log-based events correlated with the current span (OpenTelemetry's deprecation announcement), so do not encode prompt versions as span events.
Attribute names should follow a consistent convention. The GenAI semantic conventions define a gen_ai.* namespace covering agents, request model, token usage and more, though the registry notes the attributes have moved to a dedicated repository (Gen AI attribute registry, semantic-conventions-genai repository). Prompt-specific attributes are your own extension, so pick names once and write them down: prompt.slug, prompt.revision, prompt.content_hash.
import hashlib
def derive_prompt_version(slug: str, revision: int, template: str) -> dict:
normalized = "\n".join(line.rstrip() for line in template.strip().splitlines())
content_hash = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
return {
"prompt.slug": slug,
"prompt.revision": revision,
"prompt.version": f"{slug}.{revision}",
"prompt.content_hash": content_hash,
}
version = derive_prompt_version("ticket-triage", 8, TEMPLATE)
span.set_attribute("prompt.slug", version["prompt.slug"])
span.set_attribute("prompt.revision", version["prompt.revision"])
span.set_attribute("prompt.version", version["prompt.version"])
span.set_attribute("prompt.content_hash", version["prompt.content_hash"])
Note the normalization step: strip the template, strip trailing whitespace on every line, then hash, so a trailing space in one editor does not produce a different hash. Hash the template with placeholders unsubstituted, never the rendered prompt. The rendered prompt changes on every call because it contains the ticket text, so hashing it gives one hash per request and no version concept at all. If you need the rendered content for sampling, store a prompt hash and token counts on every call, but sample full prompts at a low rate because prompt bodies routinely contain PII (Techlusion's agent observability guide).
Choose the digest and stick to it. SHA-256 is the standard pick for content addressing: Git chose it over several contenders for its security properties and fast implementations (Git's transition document), and it is widely available in standard libraries (Encryption Consulting's SHA-256 overview). Never mix digests across services, because sha256(content) != sha1(content) and the join breaks silently.