Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3efb232
NET-1329 feat: add opt-in TTL caching for get_prompt (#327)
AkhileshNair2201 Jul 10, 2026
31fd8ef
NET-1329 feat(models): add opt-in TTL caching for get_model_pricing (…
AkhileshNair2201 Jul 13, 2026
d56072b
[NET-968] refactor: Remove pii detection & prompt injection detection…
akash-vijay-kv Jul 27, 2026
8f81279
[NET-1387] fix: Add iterable object support and stream early-exit han…
Nithish-KV Jul 28, 2026
cf86d53
fix: Update test files
Nithish-KV Jul 28, 2026
bc38784
[NET-1049] feat: Add instrumentation support for LiveKit (#350)
akash-vijay-kv Aug 3, 2026
ec7c24f
[NET-1049] feat: Add audio patching for livekit (#354)
pranavcv Aug 3, 2026
25cc8b3
[NET-1049] fix: Add a parent span within livekit-traces (#369)
akash-vijay-kv Aug 6, 2026
4c0455a
[NET-1049] feat: Enhance audio span handling in LiveKit by deferring …
pranavcv Aug 6, 2026
e84d4a6
fix: Fall back to parent_context per-key for session id
akash-vijay-kv Aug 6, 2026
0fab440
[NET-1409] feat : Add dynamic deadline and asynchronous post request …
pranavcv Aug 11, 2026
dccf5e6
Add parent span ID in audio chunk headers (#371)
pranavcv Aug 11, 2026
f5305ab
[NET-1409] feat: Add support for capturing audio duration and audio t…
akash-vijay-kv Aug 13, 2026
7ebe2a3
[NET-1409] fix : Properly handle agent audio at the end of the call (…
pranavcv Aug 14, 2026
b807d82
[NET-1409] feat: Stamp netra.agent.name on livekit agent_turn spans (…
akash-vijay-kv Aug 20, 2026
3a6e43b
[NET-1387] fix: Force commit inner stream to capture output on stream…
Nithish-KV Aug 20, 2026
a976086
[NET-1409] fix: Agent speaking span missing for preemptive agent turn…
pranavcv Aug 20, 2026
6d41a2f
[NET-1506] refactor: Restore logger.debug and guard against duplicate…
akash-vijay-kv Aug 27, 2026
5847571
[NET-1504] refactor: Update instrumenatation module architecture (#393)
akash-vijay-kv Aug 27, 2026
de7cbcc
feat: add Netra.redteam.run_redteam() to trigger an existing red-team…
jithin23-kv Aug 25, 2026
2cb78ff
fix: cancel orphaned run and attach run_id on fatal drive failure; fi…
jithin23-kv Aug 25, 2026
5be04ae
fix: rename Redteam to RedTeam class and red_team attribute per review
jithin23-kv Aug 28, 2026
7660228
fix(redteam): address review comments and rename module to red_team
jithin23-kv Aug 28, 2026
ad1dd86
fix(redteam): raise truncation cap to match backend's 100k limit
jithin23-kv Aug 28, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ name: Code Quality
on:
# Run on pushes to main branches
push:
branches: [ main, master, beta, dev, staging ]
branches: [ main, master, beta, dev, develop, staging ]
# Run on pull requests to specific branches
pull_request:
branches: [ main, master, beta, dev, staging ]
branches: [ main, master, beta, dev, develop, staging ]
# Manual trigger from GitHub UI
workflow_dispatch:

Expand Down
42 changes: 40 additions & 2 deletions .github/workflows/detailed-code-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ name: Detailed Code Checks
on:
# Run on pushes to main branches
push:
branches: [ main, master, beta, dev, staging ]
branches: [ main, master, beta, dev, develop, staging ]
# Run on pull requests to specific branches
pull_request:
branches: [ main, master, beta, dev, staging ]
branches: [ main, master, beta, dev, develop, staging ]
# Manual trigger from GitHub UI
workflow_dispatch:

Expand Down Expand Up @@ -54,6 +54,44 @@ jobs:
- name: Type check with mypy
run: mypy .

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
pip install -e .

# netra/instrumentation/triggers.py maps each instrumentation to the
# module whose import activates it. A wrong entry loses customer
# telemetry silently, and the tests that catch that
# (test_trigger_module_is_a_real_module_when_installed) can only check a
# trigger whose library is present -- otherwise they skip. These are the
# entries where the trigger name is not the obvious one: namespace
# packages that must name the real submodule, and distributions whose
# import name differs from their package name.
- name: Install trigger libraries for instrumentation coverage
run: |
pip install \
cerebras-cloud-sdk \
google-adk \
google-genai \
langchain-core \
langgraph \
livekit-agents \
openai

- name: Run tests
run: pytest tests -q

commit-check:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
Expand Down
41 changes: 40 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

## [Unreleased]

### Changed

- **`init_instrumentations()` no longer takes `base64_image_uploader`** - Netra hosts no image store, so the only call site had always passed `None` through four layers to reach traceloop, where the parameter is typed as required and mistyped besides (three arguments here, four in traceloop). It is now passed as `None` at the traceloop boundary and gone from the SDK's own signature. Internal helper; `Netra.init()` is unaffected.

- **A named instrumentation with no instrumentor now warns instead of logging at debug** - `Netra.init(instruments={InstrumentSet.PYRAMID})` is a no-op — no Pyramid instrumentor ships with the SDK — and said so only at `DEBUG`. Naming one explicitly now logs a warning. An `InstrumentSet.ALL` expansion still logs at debug, since it sweeps in six such members every time.

- **`netra.instrumentation.lazy` is now `netra.instrumentation.wiring.deferral`**, matching the noun-per-module naming of its siblings, which moved alongside it into `netra.instrumentation.wiring` (`selection`, `registry`, `activation`, `triggers`). Internal modules.

- **`netra.instrumentation` is now four subpackages rather than a flat directory** - the 25 per-library instrumentors moved to `netra.instrumentation.libraries.<library>`, `http_body` split into `netra.instrumentation.capture` (`bounded_capture`, `stream_formats`, `stream_utils`) and `netra.instrumentation.http` (`headers`, `body`), and `utils` became `span_utils` so it no longer reads as a sibling of `opentelemetry.instrumentation.utils` at import sites. `netra.instrumentation.instruments` is unchanged and remains the public path for `InstrumentSet`; the exported OpenTelemetry scope name of every instrumentor is unchanged too, now pinned in a `_TRACER_NAME` constant rather than derived from `__name__`, with a test that fails if one drifts. All internal modules.

- **`netra.utils.TRUNCATION_MARKER_KEY` now lives in `netra.instrumentation.capture.bounded_capture`**, next to the code that stamps it. Still importable from `netra.utils`, and the marker string itself is unchanged.

### Fixed

- **`requests` spans no longer lose their whole `output` attribute on an empty streaming response** - a `stream=True` response whose body carried no bytes left `requests` with nothing to replay, and reading the body back to record it raised `RuntimeError` instead of returning empty. That took the status code and headers down with the body, so an empty SSE stream or a bodiless chunked response produced a span with no `output` at all. The body state is now checked rather than the read attempted.

- **`httpx` and `requests` now agree on the shape of a bodiless stream** - `httpx` recorded `"body": ""` where `requests` omitted the key, for the same response. Both now omit it, matching the non-streaming path: a stream that yielded nothing is bodiless, not a body that happens to be empty.

- **`CustomInstruments`, `InstrumentSet` and `DEFAULT_INSTRUMENTS` are importable from `netra.instrumentation` again** - all three were reachable as `from netra.instrumentation import ...` before activation was split out of that module in 1.0.1b1, and the split dropped them without intending to. Re-exported. The supported public path remains `from netra import NetraInstruments`.

- **Two instrumentations were listed twice in the trigger table** - `ASYNCIO` and `SQLITE3` each had a duplicate row in `INSTRUMENT_TRIGGERS`. The duplicated values were identical so nothing was mistriggered, but the later row silently wins, and pyflakes' `F601` only fires when repeated keys have *different* values — so an edit to either copy would have been dropped without warning. Deduplicated, with a test that parses the source to catch a recurrence.

### Removed

- **`TRACELOOP_INSTRUMENTS_REPLACED_BY_NETRA`** - the set could never match anything: eight of its twelve names belong to `InstrumentSet` members tagged `_Origin.CUSTOM` (which never reach traceloop selection) and the other four name no member at all. The invariant it was meant to protect — that Netra's own instrumentations are never also delegated to traceloop — is enforced by `_Origin` and covered by `test_every_registered_instrumentor_belongs_to_the_custom_family`.

## [1.0.1b2] - 2026-08-27

### Fixed
Expand All @@ -26,7 +54,9 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver

- **Blocking one traceloop instrumentation no longer enables every other one** - `Netra.init(instruments={InstrumentSet.OPENAI}, block_instruments={InstrumentSet.ANTHROPIC})` previously enabled langchain, bedrock, vertexai and every other installed traceloop instrumentation. Selection inherited traceloop's "an empty instrument list means all of them" rule, and a request naming only Netra-backed instrumentations partitioned to an empty traceloop list — so adding a block list flipped the request into its opposite. A request now enables exactly what it names, minus what it blocks. This was also the only code path that imported `traceloop-sdk` during `Netra.init()`; selection is now free of it on every path.

- **Instrumentations gated on a module name rather than a distribution now apply** - `ASYNCIO`, `AWS_LAMBDA`, `LOGGING` and `SQLITE3` were gated on `asyncio`, `aws_lambda`, `logging` and `sqlite3`. Those are import names, not installed distributions, so the gate never matched and requesting one of these instrumentations was a silent no-op. They are now ungated, matching `THREADING` and `URLLIB`. Distribution gates are additionally matched per PEP 503, so a gate spelled with an underscore matches a distribution published with a hyphen — this revives `AIO_PIKA` (`aio_pika`) and `CEREBRAS` (`cerebras_cloud_sdk`), which had the same problem. None of these are in `DEFAULT_INSTRUMENTS`, so this only affects callers who asked for them explicitly or passed `InstrumentSet.ALL`.
- **Instrumentations gated on a module name rather than a distribution now apply** - `ASYNCIO`, `AWS_LAMBDA`, `LOGGING` and `SQLITE3` were gated on `asyncio`, `aws_lambda`, `logging` and `sqlite3`. Those are import names, not installed distributions, so the gate never matched and requesting one of these instrumentations was a silent no-op. They are now ungated, matching `THREADING` and `URLLIB`. Distribution gates are additionally matched per PEP 503, so a gate spelled with an underscore matches a distribution published with a hyphen — this revives `AIO_PIKA` (`aio_pika`) and `CEREBRAS` (`cerebras_cloud_sdk`), which had the same problem.

**`CEREBRAS` is in `DEFAULT_INSTRUMENTS`.** Every other instrumentation named above is opt-in, but Cerebras is enabled by default, and its gate has never matched — `cerebras-cloud-sdk` is published with hyphens and the old check compared lower-cased names only. Any process on default configuration with the Cerebras SDK installed will run `NetraCerebrasInstrumentor` for the first time on upgrade. The remaining instrumentations here affect only callers who asked for them explicitly or passed `InstrumentSet.ALL`.

- **`AIOHTTP` is now actually instrumented when requested** - the instrumentor existed but was never reachable from the dispatch chain, so enabling `InstrumentSet.AIOHTTP` did nothing. It is now registered against `AioHttpClientInstrumentor`. Not in `DEFAULT_INSTRUMENTS`.

Expand Down Expand Up @@ -106,16 +136,25 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver

- **Fix OpenAI streaming wrapper span lifecycle** - Made `_finalize_span()` idempotent with a `_span_ended` guard, added `close()` and `__del__()` to both sync and async wrappers so spans are properly finalized even on early exit or GC. `AsyncStreamingWrapper` now exposes `aclose()` per the async iterator protocol, with `close()` as an async alias for OpenAI SDK compatibility.

- **Add opt-in TTL caching for `get_prompt`** - `Netra.prompts.get_prompt` now accepts `use_cache` and `cache_ttl` parameters for in-memory caching. Default TTL is `PROMPT_CACHE_TTL_SECONDS` (60); override per call with `cache_ttl`. Use `Netra.prompts.clear_cache()` to invalidate cached entries.

- **Add opt-in TTL caching for `get_model_pricing`** - `Netra.models.get_model_pricing` now accepts `use_cache` and `cache_ttl` parameters for in-memory caching. Default TTL is `MODEL_PRICING_CACHE_TTL_SECONDS` (300); override per call with `cache_ttl`. Use `Netra.models.clear_cache()` to invalidate cached entries.

- **Add instrumentation for Hermes Agent** - New monkey-patching based instrumentation for the `hermes-agent` SDK (>= 0.17.0). Captures conversation runs, skill invocations (single, stacked, and bundle), tool executions, function calls, and approval gates as OpenTelemetry spans with full input/output attributes, token usage, and model metadata.

- **Fix span attributes in OpenAI instrumentation** - Assistant completions no longer emit empty entries when the model returns `content: null` alongside tool calls, request messages now correctly handle non-dictionary objects (such as Pydantic ChatCompletionMessage instances) by converting them with model_as_dict() instead of skipping them, and assistant `tool_calls` arrays as well as `tool_call_id` values on tool messages are now captured and serialized as indexed prompt and completion span attributes.

- **Fix set_root_output_stream handling** – `set_root_output_stream` now forces a commit of the inner stream to capture output when a stream exits early (for example, via `break` or `.close()`). It also correctly handles plain iterables by setting their output immediately with a warning recommending `Netra.set_root_output()`. Only true single-pass iterators are wrapped as streams.

- **Refactor stream wrapper architecture to use callback injection** - `stream_utils` is now a pure utility module with no Netra-internal imports. The commit logic (serialize and set attribute on root span) is injected as a callback from `SessionManager`, eliminating the circular dependency between `stream_utils` and `SessionManager`.

## [0.1.96] - 2026-07-23

- **Reparent children of blocked root instruments instead of dropping the subtree** - When an instrumentation is not allowed to emit root-level spans, its children are now re-parented onto the nearest valid ancestor rather than dropping the entire subtree, so downstream spans are preserved.

- **Add utility to explicitly record exceptions on a span** - New `Netra.record_exception(exception, attributes=...)` utility to attach a caught exception to the currently active span from within an `except` block. It adds a standard OpenTelemetry exception event (type, message, stacktrace), sets the span status to ERROR, and records the `netra.error_message` attribute.


## [0.1.95] - 2026-06-26

- **Added get_all_datasets with tag as optional param** - If tag is provided, we get details of all the datasets with that particular tag attached.
Expand Down
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
- 📈 **Session Management**: Track user sessions and custom attributes
- 🌐 **HTTP Client Instrumentation**: Automatic tracing for aiohttp and httpx
- 💾 **Vector Database Support**: Weaviate, Qdrant, and other vector DB instrumentation
- 📋 **Prompt Management**: Fetch managed prompts from Netra with optional in-memory TTL caching
- 💰 **Model Pricing**: Fetch model pricing from Netra with optional in-memory TTL caching

## 📦 Installation

Expand Down Expand Up @@ -319,6 +321,78 @@ Action tracking follows this schema:
]
```

## 📋 Prompt Management

Fetch managed prompt versions from Netra via `Netra.prompts`. Caching is opt-in and disabled by default. Default TTL is **60 seconds** (`PROMPT_CACHE_TTL_SECONDS`); override per call with `cache_ttl`.

```python
from netra import Netra
from netra.instrumentation.instruments import InstrumentSet

Netra.init(
app_name="My App",
instruments={InstrumentSet.OPENAI},
)

# Fetch a prompt (calls the API on every request by default)
prompt = Netra.prompts.get_prompt("my-prompt", label="production")

# Opt in to in-memory caching to reduce API calls (default TTL: 60s)
prompt = Netra.prompts.get_prompt("my-prompt", label="production", use_cache=True)

# Override TTL for a single call (seconds)
prompt = Netra.prompts.get_prompt("my-prompt", use_cache=True, cache_ttl=300)

# Clear cached entries after updating a prompt
Netra.prompts.clear_cache()
```

Caching notes:

- `use_cache` defaults to `False`; enable it per call when you want caching.
- Default TTL is the module constant `PROMPT_CACHE_TTL_SECONDS` (60); override with `cache_ttl`.
- Cache keys are scoped by prompt `name` and `label`.
- Empty or failed responses are not stored in the cache.
- The prompt cache is cleared automatically when `Netra.shutdown()` is called.

## 💰 Model Pricing

Fetch model details and pricing for your project via `Netra.models`. Caching is opt-in and disabled by default. Default TTL is **300 seconds** (`MODEL_PRICING_CACHE_TTL_SECONDS`); override per call with `cache_ttl`.

```python
from netra import Netra
from netra.instrumentation.instruments import InstrumentSet

Netra.init(
app_name="My App",
instruments={InstrumentSet.OPENAI},
)

# Fetch all model pricing (calls the API on every request by default)
models = Netra.models.get_model_pricing()

# Fetch pricing for a specific model
models = Netra.models.get_model_pricing(name="gpt-4o")

# Opt in to in-memory caching to reduce API calls (default TTL: 300s)
models = Netra.models.get_model_pricing(use_cache=True)

# Override TTL for a single call (seconds)
models = Netra.models.get_model_pricing(use_cache=True, cache_ttl=600)

# Clear cached entries after pricing updates
Netra.models.clear_cache()
```

Caching notes:

- `use_cache` defaults to `False`; enable it per call when you want caching.
- Default TTL is the module constant `MODEL_PRICING_CACHE_TTL_SECONDS` (300); override with `cache_ttl`.
- Cache keys are scoped by model `name` (or `"all"` when no name is passed).
- Empty or failed responses are not stored in the cache.
- When `use_cache` is `True`, do not mutate the returned list or nested dicts — the same objects may be served on later cache hits.
- The model pricing cache is cleared automatically when `Netra.shutdown()` is called.

## 🔧 Advanced Configuration

### Environment Variables
Expand Down
Loading