Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,30 @@ harness-instrument python app.py
library listed in `skip_libraries` is never instrumented even if its category
is enabled.

### Enrich the current span

Add runtime attributes while the target instrumented span is active:

```python
from harness_sdk import set_span_attribute, set_span_attributes

set_span_attribute("request.client.name", client_name)
set_span_attributes({
"agent.action.type": action_type,
"custom.retry.count": retry_count,
})
```

These helpers update only the current recording span. They silently do nothing when no
recording span is active. Attributes do not propagate to child spans or downstream
services.

Keys may use any customer-defined name. Values must be valid OpenTelemetry attribute
values: strings, booleans, integers, floats, or homogeneous sequences of those types.
For duplicate keys, the last write wins; instrumentation can overwrite a customer value
if it writes the same key later. Because the span already exists, these attributes
cannot influence head sampling.

## Plugins

The SDK loads extensions via [setuptools entry points](https://setuptools.pypa.io/en/latest/userguide/entry_point.html). Each plugin has a **name** (the entry-point key). Names are listed in config or environment variables; only installed plugins are loaded, in the order you configure.
Expand Down
3 changes: 2 additions & 1 deletion src/harness_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Harness Python SDK — generic instrumentation and plugin architecture."""

from harness_sdk.agent import Agent
from harness_sdk.span_enrichment import set_span_attribute, set_span_attributes

__all__ = ["Agent"]
__all__ = ["Agent", "set_span_attribute", "set_span_attributes"]
22 changes: 22 additions & 0 deletions src/harness_sdk/span_enrichment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Public helpers for enriching the current OpenTelemetry span."""

from typing import Mapping

from opentelemetry import trace
from opentelemetry.util.types import AttributeValue


def set_span_attribute(key: str, value: AttributeValue) -> None:
"""Set one attribute on the current recording span."""
span = trace.get_current_span()
if span.is_recording():
span.set_attribute(key, value)


def set_span_attributes(attributes: Mapping[str, AttributeValue]) -> None:
"""Set attributes on the current recording span."""
span = trace.get_current_span()
if not span.is_recording():
return
for key, value in attributes.items():
span.set_attribute(key, value)
75 changes: 75 additions & 0 deletions test/span_enrichment_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import asyncio

from opentelemetry.sdk.trace import TracerProvider

from harness_sdk import set_span_attribute, set_span_attributes


def _tracer():
return TracerProvider().get_tracer(__name__)


def test_set_span_attribute_updates_current_recording_span():
with _tracer().start_as_current_span("parent") as span:
set_span_attribute("request.client.name", "acme")

assert span.attributes["request.client.name"] == "acme"


def test_set_span_attributes_preserves_supported_types():
with _tracer().start_as_current_span("parent") as span:
set_span_attributes({
"string": "value",
"boolean": True,
"integer": 7,
"float": 1.5,
"sequence": ("a", "b"),
})

assert dict(span.attributes) == {
"string": "value",
"boolean": True,
"integer": 7,
"float": 1.5,
"sequence": ("a", "b"),
}


def test_last_write_wins():
with _tracer().start_as_current_span("parent") as span:
set_span_attribute("custom.key", "first")
set_span_attribute("custom.key", "second")

assert span.attributes["custom.key"] == "second"


def test_no_active_recording_span_is_noop():
assert set_span_attribute("custom.key", "value") is None
assert set_span_attributes({"custom.key": "value"}) is None


def test_empty_mapping_is_noop():
with _tracer().start_as_current_span("parent") as span:
assert set_span_attributes({}) is None
assert not span.attributes


def test_attributes_do_not_inherit_to_child_span():
tracer = _tracer()
with tracer.start_as_current_span("parent") as parent:
set_span_attribute("custom.key", "parent")

with tracer.start_as_current_span("child") as child:
assert "custom.key" not in child.attributes

assert parent.attributes["custom.key"] == "parent"


def test_async_code_updates_active_span():
async def enrich():
set_span_attribute("agent.action.type", "search")

with _tracer().start_as_current_span("parent") as span:
asyncio.run(enrich())

assert span.attributes["agent.action.type"] == "search"
Loading