Skip to content

docs: add governed action span metadata example - #4390

Closed
jw-ond wants to merge 1 commit into
traceloop:mainfrom
jw-ond:osuite/governed-action-span-attributes
Closed

docs: add governed action span metadata example#4390
jw-ond wants to merge 1 commit into
traceloop:mainfrom
jw-ond:osuite/governed-action-span-attributes

Conversation

@jw-ond

@jw-ond jw-ond commented Jul 31, 2026

Copy link
Copy Markdown

Summary

  • Add a sample app example for attaching governed agent action metadata to OpenTelemetry spans.
  • Document illustrative custom attributes for action ref/hash, governance verdict, approval status, proof URL, and external verifier reference.
  • Keep the example vendor-neutral while noting that OSuite or another governance emitter can provide the values.

Validation

  • python3 -m py_compile packages/sample-app/sample_app/governed_action_span_attributes.py
  • Markdown sanity check for packages/sample-app/README.md
  • git diff --check
  • GitHub compare API: ahead_by=1, behind_by=0, files=2

Summary by CodeRabbit

  • New Features

    • Added a sample application demonstrating how to record governed agent action metadata in OpenTelemetry spans.
    • Captures action identity, governance verdict, approval status, proof URL, and external verifier reference.
    • Includes guidance for attaching vendor-neutral governance metadata to traces.
  • Documentation

    • Added usage documentation for the governed action span example.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Jason Wang seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The sample app adds governed agent action metadata to an OpenTelemetry span. It defines action, governance, approval, proof, and verifier attributes, initializes Traceloop, creates an agent.action span, and documents the example.

Changes

Governed action metadata

Layer / File(s) Summary
Governed action span example
packages/sample-app/sample_app/governed_action_span_attributes.py, packages/sample-app/README.md
The sample defines governed action metadata, attaches it to an agent.action span, prints a confirmation, supports direct execution, and documents the span attributes.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the governed action span metadata example added by the pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/sample-app/sample_app/governed_action_span_attributes.py (1)

14-22: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use a project-owned namespace for custom attributes.

gen_ai.agent.action.ref and gen_ai.agent.action.hash are application-defined keys, but they use the existing gen_ai semantic-convention namespace. OpenTelemetry recommends avoiding existing semantic-convention namespaces for new application-specific attributes. If these keys remain custom, move them to a project-owned prefix and update downstream consumers. (opentelemetry.io)

Confirm this against the semantic-conventions version targeted by the repository.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sample-app/sample_app/governed_action_span_attributes.py` around
lines 14 - 22, The ACTION_ATTRIBUTES mapping uses the gen_ai namespace for
application-defined action keys; rename gen_ai.agent.action.ref and
gen_ai.agent.action.hash to project-owned attribute prefixes, update downstream
consumers of those keys, and verify the chosen names against the repository’s
targeted OpenTelemetry semantic-conventions version.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/sample-app/sample_app/governed_action_span_attributes.py`:
- Around line 27-30: Remove the empty instruments argument from the
Traceloop.init call in the governed action span attributes setup, allowing
init_instrumentations() to use its default value and avoid the dict/set
subtraction error.
- Around line 27-31: Update the Traceloop.init call in the standalone sample to
configure a ConsoleSpanExporter imported from opentelemetry.sdk.trace.export,
ensuring spans are emitted locally without requiring TRACELOOP_API_KEY and the
existing tracer flow remains executable.

---

Nitpick comments:
In `@packages/sample-app/sample_app/governed_action_span_attributes.py`:
- Around line 14-22: The ACTION_ATTRIBUTES mapping uses the gen_ai namespace for
application-defined action keys; rename gen_ai.agent.action.ref and
gen_ai.agent.action.hash to project-owned attribute prefixes, update downstream
consumers of those keys, and verify the chosen names against the repository’s
targeted OpenTelemetry semantic-conventions version.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d2c1519e-a188-4d0d-be86-d9e7974b261a

📥 Commits

Reviewing files that changed from the base of the PR and between 93429cf and 6bd02af.

📒 Files selected for processing (2)
  • packages/sample-app/README.md
  • packages/sample-app/sample_app/governed_action_span_attributes.py

Comment on lines +27 to +30
Traceloop.init(
app_name="governed-action-span-attributes",
disable_batch=True,
instruments={},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n 'traceloop-sdk|Traceloop.init|instruments' . \
  --glob 'pyproject.toml' \
  --glob 'uv.lock' \
  --glob '*.py'

uv run --with traceloop-sdk python - <<'PY'
from inspect import getsource, signature
from traceloop.sdk import Traceloop
from traceloop.sdk.tracing import init_instrumentations

print(signature(Traceloop.init))
print(getsource(init_instrumentations))
PY

Repository: traceloop/openllmetry

Length of output: 50379


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "## relevant source files"
wc -l packages/traceloop-sdk/traceloop/sdk/__init__.py packages/traceloop-sdk/traceloop/sdk/instruments.py packages/traceloop-sdk/traceloop/sdk/tracing.py 2>/dev/null || true

echo "## packages/traceloop-sdk/traceloop/sdk/__init__.py lines 1-220"
sed -n '1,220p' packages/traceloop-sdk/traceloop/sdk/__init__.py

echo "## packages/traceloop-sdk/traceloop/sdk/tracing.py lines 1-140"
sed -n '1,140p' packages/traceloop-sdk/traceloop/sdk/tracing.py

echo "## packages/traceloop-sdk/traceloop/sdk/instruments.py"
sed -n '1,80p' packages/traceloop-sdk/traceloop/sdk/instruments.py

echo "## targeted samples with empty dict"
rg -n "Traceloop\.init\([^)]*instruments=\{\}" packages/sample-app/sample_app -g '*.py'

Repository: traceloop/openllmetry

Length of output: 9793


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "## tracing package files"
git ls-files packages/traceloop-sdk/traceloop/sdk | sed -n '1,120p'

echo "## init_instrumentations references"
rg -n "init_instrumentations|TracerWrapper|def __init__|instruments|block_instruments" packages/traceloop-sdk/traceloop/sdk -g '*.py'

echo "## candidate files with init_instrumentations"
rg -l "init_instrumentations" packages/traceloop-sdk/traceloop/sdk -g '*.py'

Repository: traceloop/openllmetry

Length of output: 12747


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "## packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py lines 140-210"
sed -n '140,210p' packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py

echo "## packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py lines 480-540"
sed -n '480,535p' packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py

echo "## packages/sample-app/sample_app/governed_action_span_attributes.py lines 1-60"
sed -n '1,60p' packages/sample-app/sample_app/governed_action_span_attributes.py

echo "## other empty-instruments samples"
rg -n "Traceloop\.init\([^)]*instruments=\{\}" packages/sample-app/sample_app -g '*.py'

echo "## init_instrumentations import target"
grep -n "from .*init_instrumentations" packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py
sed -n '1,45p' packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py

Repository: traceloop/openllmetry

Length of output: 7234


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "## behavioral probe for empty instruments handling"
python3 - <<'PY'
class Instrument:
    pass

Instrument.A = object()

instruments_dict = {}
block_instruments_dict = {}

block_instruments_dict = block_instruments_dict or set()
instruments_after_none = instruments_dict if instruments_dict is not None else {Instrument.A}
try:
    instruments_subtracted = instruments_after_none - block_instruments_dict
    print("dict_subtraction_result_repr=" + repr(instruments_subtracted))
except Exception as e:
    print("dict_subtraction_error_type=" + type(e).__name__)
    print("dict_subtraction_error_message=" + str(e))

instruments_set = set()
instruments_subtracted_set = (instruments_set if instruments_set is not None else {Instrument.A}) - (block_instruments_dict or set())
print("set_subtraction_result_repr=" + repr(instruments_subtracted_set))
PY

echo "## uv package availability"
if command -v uv >/dev/null 2>&1; then
  echo "uv present"
  uv run python3 - <<'PY'
try:
    from traceloop.sdk.tracing.tracing import init_instrumentations
    from traceloop.sdk.instruments import Instruments
    import inspect
    print(init_instrumentations.__module__)
    print(inspect.signature(init_instrumentations).parameters["instruments"].annotation)
    init_instrumentations(False, lambda *a, **k: None, instruments={}, block_instruments={})
    print("empty_dict_call=ok")
except Exception as e:
    print(type(e).__name__ + ": " + str(e))
PY
else
  echo "uv not present"
fi

echo "## local tests mentioning empty instruments"
rg -n "instruments=\{\}|instruments=set\(\)|block_instruments" packages/traceloop-sdk/tests packages/sample-app/sample_app -g '*.py'

Repository: traceloop/openllmetry

Length of output: 723


Use no instruments arg to disable instrumentation.

Traceloop.init(instruments={}) passes a dict to init_instrumentations(), which later does instruments = instruments - block_instruments. That raises TypeError: unsupported operand type(s) for -: 'dict' and 'set'. Use the default value instead.

Proposed fix
-    Traceloop.init(
-        app_name="governed-action-span-attributes",
-        disable_batch=True,
-        instruments={},
-    )
+    Traceloop.init(
+        app_name="governed-action-span-attributes",
+        disable_batch=True,
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sample-app/sample_app/governed_action_span_attributes.py` around
lines 27 - 30, Remove the empty instruments argument from the Traceloop.init
call in the governed action span attributes setup, allowing
init_instrumentations() to use its default value and avoid the dict/set
subtraction error.

Comment on lines +27 to +31
Traceloop.init(
app_name="governed-action-span-attributes",
disable_batch=True,
instruments={},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)governed_action_span_attributes\.py$|pyproject\.toml$|Traceloop' || true

echo
echo "== target file =="
if [ -f packages/sample-app/sample_app/governed_action_span_attributes.py ]; then
  nl -ba packages/sample-app/sample-app/governed_action_span_attributes.py 2>/dev/null || nl -ba packages/sample-app/sample_app/governed_action_span_attributes.py
fi

echo
echo "== dependency/version clues =="
rg -n "traceloop|opentelemetry|ConsoleSpanExporter|API_KEY|TRACLOOP" . -g '!**/.git/**' -g '!**/node_modules/**' | head -200

echo
echo "== package metadata =="
fd -a 'pyproject\.toml$' . | while read -r f; do
  echo "--- ${f#"$PWD/"}"
  sed -n '1,220p' "$f" | rg -n "traceloop|opentelemetry|packages|dependencies|requires-python|python" -C 2 || true
done

Repository: traceloop/openllmetry

Length of output: 2443


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file =="
sed -n '1,160p' packages/sample-app/sample_app/governed_action_span_attributes.py | awk '{printf "%6d  %s\n", NR, $0}'

echo
echo "== sample pyproject =="
sed -n '1,220p' packages/sample-app/pyproject.toml | awk '{printf "%6d  %s\n", NR, $0}'

echo
echo "== traceloop sdk relevant init/export docs in repo =="
python3 - <<'PY'
import pathlib, re
for p in pathlib.Path("packages").rglob("*.py"):
    try:
        text = p.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        continue
    if "def init" in text and ("Traceloop.init" in text or "class Traceloop" in text):
        print("----", p)
        lines = text.splitlines()
        for i, line in enumerate(lines, start=1):
            if "class Traceloop" in line or "def init" in line:
                a=max(1,i-12); b=min(len(lines),i+120)
                for j in range(a,b+1):
                    print(f"{j:6d}  {lines[j-1]}")
PY

echo
echo "== search ConsoleSpanExporter and API key env usage =="
rg -n "ConsoleSpanExporter|Traceloop\.init|api_key|API_KEY|TRACLOOP|disable_batch" packages -g '*.py' -g '*.toml' | head -300

echo
echo "== deterministic parse probe for target snippet =="
python3 - <<'PY'
import ast, pathlib
p=pathlib.Path("packages/sample-app/sample_app/governed_action_span_attributes.py")
text=p.read_text()
tree=ast.parse(text)
for node in ast.walk(tree):
    if isinstance(node, ast.Call):
        if isinstance(node.func, ast.Attribute) and node.func.attr == "init":
            print("Traceloop.init args:", {kw.arg: ast.unparse(kw.value) for kw in node.keywords})
PY

Repository: traceloop/openllmetry

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file relevant lines =="
awk '{printf "%6d  %s\n", NR, $0}' packages/sample-app/sample_app/governed_action_span_attributes.py | sed -n '1,120p'
awk '{printf "%6d  %s\n", NR, $0}' packages/sample-app/sample_app/governed_action_span_attributes.py | sed -n '120,200p'

echo
echo "== console/exporter imports in target and nearby sample files =="
rg -n "import tr\." packages/sample-app/sample_app packages/sample-app -g '*.py' | head -100
rg -n "ConsoleSpanExporter|import exporter|exporter=" packages/sample-app/sample_app packages/sample-app -g '*.py' | head -150

echo
echo "== parse Traceloop.init call in target =="
python3 - <<'PY'
import ast, pathlib
p=pathlib.Path("packages/sample-app/sample_app/governed_action_span_attributes.py")
text=p.read_text()
tree=ast.parse(text)
for node in ast.walk(tree):
    if isinstance(node, ast.Call):
        func=node.func
        if isinstance(func, ast.Attribute) and func.attr == "init":
            if isinstance(func.value, ast.Name) and func.value.id == "Traceloop":
                print("Traceloop.init call at line", node.lineno)
                print("keywords:", {kw.arg or "<positional>": ast.unparse(kw.value) for kw in node.keywords})
                print("has exporter keyword:", any(kw.arg == "exporter" for kw in node.keywords))
PY

echo
echo "== focused search SDK init and API key handling =="
rg -n "def init\(|API_KEY|TRACLOOP_API|tracer_provider|ConsoleSpanExporter|disable_batch|get_tracer_provider|set_tracer_provider" packages -g '*.py' | head -250

echo
echo "== focused python files containing relevant SDK init implementation =="
rg -l "class Traceloop|def init\(" packages -g '*.py' | head -20
for f in $(rg -l "class Traceloop|def init\(" packages -g '*.py' | head -10); do
  echo "--- $f"
  rg -n "class Traceloop|def init\(|API_KEY|TRACLOOP_API|tracer_provider|ConsoleSpanExporter|disable_batch|get_tracer_provider|set_tracer_provider" "$f" -C 8
done

Repository: traceloop/openllmetry

Length of output: 2120


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== focused list files =="
rg -l "class Traceloop|def init\(" packages -g '*.py' | python3 - <<'PY'
import sys
lines=sys.stdin.read().strip().splitlines()
for line in lines:
    if line:
        print(line)
PY

echo
echo "== focused implementation excerpts =="
while IFS= read -r f; do
  echo "--- $f"
  rg -n "class Traceloop|def init\(|API_KEY|TRACLOOP_API|tracer_provider|ConsoleSpanExporter|disable_batch|get_tracer_provider|set_tracer_provider|_init_.*API|exporter" "$f" -C 6 || true
done

echo
echo "== target file parser probe =="
python3 - <<'PY'
import ast, pathlib
p=pathlib.Path("packages/sample-app/sample_app/governed_action_span_attributes.py")
text=p.read_text()
tree=ast.parse(text)
for node in ast.walk(tree):
    if isinstance(node, ast.Call):
        func=node.func
        if isinstance(func, ast.Attribute) and func.attr == "init":
            if isinstance(func.value, ast.Name) and func.value.id == "Traceloop":
                print("Traceloop.init line", node.lineno)
                print("keywords:", [(kw.arg or "<positional>", ast.unparse(kw.value)) for kw in node.keywords])
                print("has exporter keyword:", any(kw.arg == "exporter" for kw in node.keywords))
PY

echo
echo "== deterministic env/default init-flow probe from source text =="
python3 - <<'PY'
import re, pathlib, ast
files=[pathlib.Path(p) for p in open("/tmp/focused_files.txt")]
# Fallback list if no temp file, but avoid shell subshell failure above.
PY

Repository: traceloop/openllmetry

Length of output: 5011


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT

echo "== focused files list =="
rg -l "class Traceloop|def init\(" packages -g '*.py' > "$tmp" || true
cat "$tmp"

echo
echo "== focused implementation excerpts =="
while IFS= read -r f; do
  echo "--- $f"
  rg -n "class Traceloop|def init\(|API_KEY|TRACLOOP_API|tracer_provider|ConsoleSpanExporter|disable_batch|get_tracer_provider|set_tracer_provider|exporter" "$f" -C 8 || true
done < "$tmp"

echo
echo "== target file parser probe =="
python3 - <<'PY'
import ast, pathlib
p=pathlib.Path("packages/sample-app/sample_app/governed_action_span_attributes.py")
text=p.read_text()
tree=ast.parse(text)
for node in ast.walk(tree):
    if isinstance(node, ast.Call):
        func=node.func
        if isinstance(func, ast.Attribute) and func.attr == "init":
            if isinstance(func.value, ast.Name) and func.value.id == "Traceloop":
                print("Traceloop.init line", node.lineno)
                print("keywords:", [(kw.arg or "<positional>", ast.unparse(kw.value)) for kw in node.keywords])
                print("has exporter keyword:", any(kw.arg == "exporter" for kw in node.keywords))
PY

Repository: traceloop/openllmetry

Length of output: 11822


Use ConsoleSpanExporter for this standalone sample.

Traceloop.init has no local exporter, and this sample does not set TRACELOOP_API_KEY. That makes the SDK print the default missing-API-key error and return before installing the tracer wrapper, so the local tracer.start_as_current_span(...) call and success message only run when credentials are provided externally.

Proposed fix
 from opentelemetry import trace
+from opentelemetry.sdk.trace.export import ConsoleSpanExporter
 ...
     Traceloop.init(
         app_name="governed-action-span-attributes",
         disable_batch=True,
+        exporter=ConsoleSpanExporter(),
         instruments={},
     )

As required by the Python guidelines, use ConsoleSpanExporter from opentelemetry.sdk.trace.export for debugging OpenTelemetry spans and hierarchy issues.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Traceloop.init(
app_name="governed-action-span-attributes",
disable_batch=True,
instruments={},
)
Traceloop.init(
app_name="governed-action-span-attributes",
disable_batch=True,
exporter=ConsoleSpanExporter(),
instruments={},
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sample-app/sample_app/governed_action_span_attributes.py` around
lines 27 - 31, Update the Traceloop.init call in the standalone sample to
configure a ConsoleSpanExporter imported from opentelemetry.sdk.trace.export,
ensuring spans are emitted locally without requiring TRACELOOP_API_KEY and the
existing tracer flow remains executable.

Source: Coding guidelines

@jw-ond

jw-ond commented Jul 31, 2026

Copy link
Copy Markdown
Author

Closing this in favor of #4391. The replacement keeps the same sample intent, incorporates the CodeRabbit feedback ( attributes, no empty , console span processor), and uses commits authored from my GitHub-linked email so the CLA check can evaluate cleanly.

@jw-ond

jw-ond commented Jul 31, 2026

Copy link
Copy Markdown
Author

Superseded by #4391.

@jw-ond jw-ond closed this Jul 31, 2026
@jw-ond

jw-ond commented Jul 31, 2026

Copy link
Copy Markdown
Author

Superseded by #4391. The replacement keeps the same sample intent, incorporates the CodeRabbit feedback by using app.governance attribute names, removing the empty instruments argument, and using a console span processor. It also uses commits authored from my GitHub-linked email so the CLA check can evaluate cleanly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants