Everything you need to run the book's code, and to use the framework in your own project.
- Python 3.10 or newer
- No API key, no network, no GPU
- Optional: Docker, if you want telemetry to land in a real backend
git clone https://github.com/ai-observability-engineering/ai-observability-engineering.git
cd ai-observability-engineering
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install -e ".[dev]"Verify:
aiobs-sim --version
aiobs-sim run --chapter 1If aiobs-sim is not on your PATH, use python -m simulator.app instead. Everything below works with either form.
aiobs-sim run --allYou should see 17 chapters, 28 examples, and a green summary line. If anything is red, that is a bug in this repository, not in your setup. Open an issue with the output.
pytest # 347 tests
pytest tests/unit # framework internals only, fast
pytest tests/functional # every example, every chapter claim
make report # writes report.html and report.jsonFor each registered example it:
- Installs a fresh tracer with an in-memory exporter
- Runs the example
- Captures every span it emitted
- Checks each span against the instrumentation contract
- Records spans, tokens, simulated cost, wall time, and any violations
An example passes only if it raised nothing and violated nothing. Instrumentation that emits plausible-looking but non-conformant telemetry fails, which is the point.
aiobs-sim run --all
aiobs-sim run --chapter 10
aiobs-sim run --pillar responsibility
aiobs-sim run --example ch06.output_drift_psi --example ch06.retrieval_drift_incident
aiobs-sim list
aiobs-sim coverage
aiobs-sim scenariosaiobs-sim run --all # coloured terminal report
aiobs-sim run --all --no-color # for CI logs
aiobs-sim run --all --format json # machine readable, to stdout
aiobs-sim run --all --format quiet # one line
aiobs-sim run --all --html report.html # standalone file, no external assets
aiobs-sim run --all --verbose # include full tracebacks
aiobs-sim run --all --lenient # skip semconv and PII enforcementUse --lenient while you are mid-refactor and the attribute names are not settled yet. Do not use it in CI.
The examples emit real OpenTelemetry spans. The in-memory exporter is a test affordance, not a limitation.
pip install ".[otlp]"
docker compose up -d
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces aiobs-sim run --all
open http://localhost:16686Or in code:
from aiobs import configure
configure(service_name="my-service", exporter="otlp", endpoint="http://localhost:4318/v1/traces")Exporters: memory (default), console, otlp, none.
The collector config at config/otel-collector.yaml runs two pipelines on purpose:
traces/sampled— 10% head sampling, payload attributes stripped. Fine for latency work.traces/cost— unsampled, filtered to spans carryingaiobs.cost.usd.
That split is Chapter 2's argument as configuration. A sampled stream cannot be a system of record for spend: sample at 10% and your cost total is wrong by however much the sample happened to miss. Run aiobs-sim run --example ch02.collector_pipeline_sampling to see the error size.
pip install ai-observability-engineeringfrom aiobs import configure, llm_span
from aiobs.instrument import set_llm_attributes, set_eval_attributes, set_cost_attributes
configure(service_name="support-assistant", exporter="otlp")
with llm_span(provider="anthropic", model="claude-sonnet-4-5") as span:
reply = client.messages.create(...)
set_llm_attributes(
span,
provider="anthropic",
model=reply.model,
input_tokens=reply.usage.input_tokens,
output_tokens=reply.usage.output_tokens,
finish_reason=reply.stop_reason,
)
set_cost_attributes(span, cost_usd, tenant="acme", use_case="support")
set_eval_attributes(span, {"groundedness": 0.91}, evaluator="ragas-v2")from aiobs import Layer, Pillar, observe
@observe(pillar=Pillar.PERFORMANCE, layer=Layer.DATA_AND_RETRIEVAL)
def retrieve(query: str) -> list[str]:
return vector_store.search(query, k=5)
@observe(pillar=Pillar.ROI, layer=Layer.BUSINESS_AND_OUTCOMES)
def record_resolution(ticket_id: str, resolved: bool) -> None:
...Tagging the pillar and layer is what makes coverage reporting possible. Skip it and the framework still works; you just lose the ability to see which pillar nobody is instrumenting.
Implement the protocol and every example works against it unchanged:
from aiobs.providers import ChatResponse, LLMProvider
class AnthropicProvider:
name = "anthropic"
model = "claude-sonnet-4-5"
def chat(self, prompt: str, **kwargs) -> ChatResponse:
reply = client.messages.create(...)
return ChatResponse(
text=reply.content[0].text,
model=reply.model,
provider=self.name,
input_tokens=reply.usage.input_tokens,
output_tokens=reply.usage.output_tokens,
)
def price(self, input_tokens: int, output_tokens: int) -> float:
return input_tokens / 1000 * 0.003 + output_tokens / 1000 * 0.015
assert isinstance(AnthropicProvider(), LLMProvider)from aiobs.evals import Evaluator, register
class LLMJudgeGroundedness(Evaluator):
name = "groundedness"
def score(self, output: str, *, context: str | None = None, **kwargs) -> float:
return judge_model.rate(output, context)
register("llm_judge_groundedness", LLMJudgeGroundedness)Record the eval set version. A score with no version attached is not interpretable six months later, which is the practical form of Chapter 1's third pitfall.
The GenAI conventions are pre-1.0. Attribute names still move.
src/aiobs/semconv.py is the only file that knows the strings. When the spec changes, edit SEMCONV_VERSION and the affected constants there; every example and test picks it up. That indirection costs one import and saves a repository-wide find-and-replace.
from aiobs.semconv import SEMCONV_VERSION, GenAI
print(SEMCONV_VERSION) # 1.37.0
print(GenAI.USAGE_INPUT_TOKENS) # gen_ai.usage.input_tokensModuleNotFoundError: aiobs
The package is not installed in editable mode. Run pip install -e ".[dev]" from the repository root, or set PYTHONPATH=src:..
aiobs-sim: command not found
Use python -m simulator.app instead, or activate the virtualenv you installed into.
An example reports example emitted no spans
The tracer was reconfigured after the harness installed it. Call configure() once at process start, and never inside an example.
Violation: carries non-namespaced attributes ['llm.model']
This is the harness doing its job. llm.* was never an OpenTelemetry convention. Use gen_ai.request.model and friends from aiobs.semconv.GenAI, or move the attribute into a prefix you own.
Violation: attribute 'x' looks like PII
Something wrote a payload into telemetry. Record a reference, not the content: span.set_attribute("aiobs.risk.evidence_ref", "vault://prompts/1234").
Violation: records cost but leaves it unattributed
set_cost_attributes was called without a tenant. Unattributed spend is the ROI failure mode of Part III; the harness treats it as a defect rather than a default.
Tests pass locally, fail in CI
Almost always span leakage between tests. Make sure pytest_plugins = ["aiobs.testing.fixtures"] is in your conftest.py; the autouse fixture resets the tracer between tests.
Drift detector raises reference window has zero variance
The reference window is a constant. PSI cannot bin a degenerate distribution, and returning a confident zero would be worse than raising. Widen the window or use a metric that actually varies.