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
51 changes: 51 additions & 0 deletions .env.test.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# ============================================================================
# .env.test — Environment variables for live test execution / cassette recording
# ============================================================================
# Copy this file to .env.test and fill in real values.
# cp .env.test.example .env.test
# source .env.test
#
# IMPORTANT: .env.test is gitignored — never commit real API keys.
#
# For cassette REPLAY (CI, day-to-day dev): no keys needed. Tests use dummy values.
# For cassette RECORDING: you need real API keys for the providers below.
# ============================================================================

# --- LLM Provider API Keys (for cassette recording) ---
# OpenAI: https://platform.openai.com/api-keys
OPENAI_API_KEY=sk-replace-with-real-key

# Anthropic: https://console.anthropic.com/settings/keys
ANTHROPIC_API_KEY=sk-ant-replace-with-real-key

# Google AI Studio (Gemini): https://aistudio.google.com/apikey
GOOGLE_API_KEY=replace-with-real-key

# Groq: https://console.groq.com/keys
GROQ_API_KEY=gsk_replace-with-real-key

# Mistral: https://console.mistral.ai/api-keys/
MISTRAL_API_KEY=replace-with-real-key

# Cohere: https://dashboard.cohere.com/api-keys
COHERE_API_KEY=replace-with-real-key

# Together: https://api.together.xyz/settings/api-keys
TOGETHER_API_KEY=replace-with-real-key

# Writer: https://dev.writer.com/api-keys
WRITER_API_KEY=replace-with-real-key

# Replicate: https://replicate.com/account/api-tokens
REPLICATE_API_TOKEN=replace-with-real-key

# OpenRouter (budget-friendly proxy): https://openrouter.ai/keys
OPENROUTER_API_KEY=sk-or-replace-with-real-key

# --- AWS (for Bedrock / SageMaker cassette recording) ---
# AWS_ACCESS_KEY_ID=replace-with-real-key
# AWS_SECRET_ACCESS_KEY=replace-with-real-key
# AWS_DEFAULT_REGION=us-east-1

# --- GCP (for VertexAI cassette recording) ---
# GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
45 changes: 41 additions & 4 deletions .github/workflows/fr-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- name: Check out code
Expand All @@ -31,9 +31,9 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: |
packages/**/pyproject.toml

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Run FR-marked test suite
env:
Expand All @@ -47,3 +47,40 @@ jobs:
name: fr-test-reports-py${{ matrix.python-version }}
path: reports/test-run/
if-no-files-found: ignore

non-fr-tests:
name: Non-FR Tests (py${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Run non-FR test suite (upstream UT + VCR)
env:
HAYSTACK_TELEMETRY_ENABLED: "False"
run: bash ./scripts/run-tests.sh -- -m "not fr"

- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: non-fr-test-reports-py${{ matrix.python-version }}
path: reports/test-run/
if-no-files-found: ignore
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ celerybeat.pid

# Environments
.env
.env.test
.venv
env/
venv/
Expand Down Expand Up @@ -175,8 +176,10 @@ cython_debug/
# NX
.nx

# Test artifcats
# Test artifacts
chroma.sqlite3
*.bin
milvus.db

# Claude
.claude
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@
pytestmark = pytest.mark.fr


@pytest.fixture(autouse=True)
def _cleanup_safety_handlers():
"""Ensure safety handlers are cleared after each test in this file.

Tests here register global safety stream factories via
register_completion_safety_stream_factory(). Without cleanup, these
persist and corrupt subsequent tests (e.g. test_thinking.py streaming).
"""
yield
clear_safety_handlers()


def test_apply_prompt_safety_masks_prompt_system_and_messages(monkeypatch):
monkeypatch.setattr(safety, "run_prompt_safety", lambda **kwargs: SafetyResult(text=f"masked:{kwargs['text']}", overall_action="MASK"))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,15 @@ def assert_guardrails(reader):
]
assert data_point.value > 0

assert (
metric.data.data_points[0].attributes[GenAIAttributes.GEN_AI_SYSTEM]
== "bedrock"
)
if metric.name in (
GuardrailMeters.LLM_BEDROCK_GUARDRAIL_ACTIVATION,
GuardrailMeters.LLM_BEDROCK_GUARDRAIL_LATENCY,
GuardrailMeters.LLM_BEDROCK_GUARDRAIL_COVERAGE,
):
assert (
metric.data.data_points[0].attributes[GenAIAttributes.GEN_AI_SYSTEM]
== "bedrock"
)

assert found_activations is True
assert found_latency is True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ def test_invoke_model_metrics(test_context, brt):
data_point.sum > 0 for data_point in metric.data.data_points
)

assert (
metric.data.data_points[0].attributes[GenAIAttributes.GEN_AI_SYSTEM]
== "bedrock"
)
if metric.name in (Meters.LLM_TOKEN_USAGE, Meters.LLM_OPERATION_DURATION):
assert (
metric.data.data_points[0].attributes[GenAIAttributes.GEN_AI_SYSTEM]
== "bedrock"
)

assert found_token_metric is True
assert found_duration_metric is True
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ def test_anthropic_3_completion_streaming_with_events_with_content(
choice_event = {
"index": 0,
"finish_reason": "unknown",
"message": {"content": response.get("body")._accumulating_body.get("content")},
"message": {"content": response.get("body")._self_accumulating_body.get("content")},
}
assert_message_in_logs(logs[1], "gen_ai.choice", choice_event)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dev = [
]
test = [
"opentelemetry-sdk>=1.38.0,<2",
"pytest-asyncio>=0.23.7,<0.24.0",
"pytest>=8.2.2,<9",
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dependencies = [
Repository = "https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-google-generativeai"

[project.optional-dependencies]
instruments = ["google-genai"]
instruments = ["google-genai>=1.58.0,<1.59.0"]

[project.entry-points."opentelemetry_instrumentor"]
google_generativeai = "opentelemetry.instrumentation.google_generativeai:GoogleGenerativeAiInstrumentor"
Expand All @@ -34,8 +34,9 @@ dev = [
"ruff>=0.4.0",
]
test = [
"google-genai>=1.0.0,<2",
"google-genai>=1.58.0,<1.59.0",
"opentelemetry-sdk>=1.38.0,<2",
"pytest-asyncio>=0.23.7,<0.24.0",
"pytest-recording>=0.13.1,<0.14.0",
"pytest-sugar==1.0.0",
"pytest>=8.2.2,<9",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,37 @@


@pytest.fixture(scope="session")
def exporter(metrics_test_context):
def _session_instrumentor(metrics_test_context):
"""Session-scoped instrumentor that stays active for the entire run."""
meter_provider, _ = metrics_test_context
instrumentor = GoogleGenerativeAiInstrumentor()
instrumentor.instrument(meter_provider=meter_provider)
return instrumentor


@pytest.fixture(autouse=True)
def _ensure_instrumented(_session_instrumentor):
"""Re-instrument after each test.

Function-scoped fixtures (instrument_legacy, instrument_with_content, etc.)
call uninstrument() on teardown, which globally unwraps the monkeypatched
methods. This fixture re-applies instrumentation so that later tests
(especially test_generate_metrics) still see metrics being emitted.
"""
yield
if not _session_instrumentor.is_instrumented_by_opentelemetry:
_session_instrumentor.instrument()


@pytest.fixture(scope="session")
def exporter(_session_instrumentor):
exporter = InMemorySpanExporter()
processor = SimpleSpanProcessor(exporter)

provider = TracerProvider()
provider.add_span_processor(processor)
set_tracer_provider(provider)

meter_provider, _ = metrics_test_context
GoogleGenerativeAiInstrumentor().instrument(meter_provider=meter_provider)

return exporter


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,8 @@ def test_generate_metrics(metrics_test_context, genai_client):

metrics = {m.name: m for m in scope_metrics.metrics}

# ---- Required metrics (semantic conventions) ----
required_metrics = {
Meters.LLM_OPERATION_DURATION,
Meters.LLM_TOKEN_USAGE,
}
assert required_metrics.issubset(metrics.keys())
# ---- Duration metric (always emitted) ----
assert Meters.LLM_OPERATION_DURATION in metrics, "Duration metric not emitted"

duration_metric = metrics[Meters.LLM_OPERATION_DURATION]

Expand All @@ -102,23 +98,25 @@ def test_generate_metrics(metrics_test_context, genai_client):
assert GenAIAttributes.GEN_AI_PROVIDER_NAME in duration_dp.attributes
assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in duration_dp.attributes

token_metric = metrics[Meters.LLM_TOKEN_USAGE]
# ---- Token metric (only emitted when response includes usage_metadata) ----
if Meters.LLM_TOKEN_USAGE in metrics:
token_metric = metrics[Meters.LLM_TOKEN_USAGE]

assert token_metric.unit == "token"
assert token_metric.data.data_points
assert token_metric.unit == "token"
assert token_metric.data.data_points

token_points_by_type = {
dp.attributes.get(GenAIAttributes.GEN_AI_TOKEN_TYPE): dp
for dp in token_metric.data.data_points
}
token_points_by_type = {
dp.attributes.get(GenAIAttributes.GEN_AI_TOKEN_TYPE): dp
for dp in token_metric.data.data_points
}

# Both input & output tokens must exist
assert {"input", "output"}.issubset(token_points_by_type.keys())
# Both input & output tokens must exist
assert {"input", "output"}.issubset(token_points_by_type.keys())

for token_type, dp in token_points_by_type.items():
assert dp.count >= 1
assert dp.sum >= 0
for token_type, dp in token_points_by_type.items():
assert dp.count >= 1
assert dp.sum >= 0

# Required semantic attributes
assert GenAIAttributes.GEN_AI_PROVIDER_NAME in dp.attributes
assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in dp.attributes
# Required semantic attributes
assert GenAIAttributes.GEN_AI_PROVIDER_NAME in dp.attributes
assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in dp.attributes
1 change: 1 addition & 0 deletions packages/opentelemetry-instrumentation-lancedb/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data/
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ dev = [
"ruff>=0.4.0",
]
test = [
"lancedb>=0.26.0",
"lancedb>=0.20.0,<0.26",
"numpy>=1.26.4,<2",
"opentelemetry-sdk>=1.38.0,<2",
"pandas>=2.2.2,<3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

@pytest.fixture
def collection():
# Drop stale table from prior runs (create_table doesn't overwrite).
try:
db.drop_table("my_table")
except Exception:
pass
data = [
{"vector": [1.3, 1.4], "item": "fizz", "price": 100.0},
{"vector": [9.5, 56.2], "item": "buzz", "price": 200.0},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dependencies = [
Repository = "https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-llamaindex"

[project.optional-dependencies]
instruments = ["llama-index"]
instruments = ["llama-index>=0.14.12,<0.14.13"]
llamaparse = ["llama-parse"]

[project.entry-points."opentelemetry_instrumentor"]
Expand All @@ -41,7 +41,8 @@ test = [
"llama-index-llms-openai>=0.6.0,<0.7.0",
"llama-index-postprocessor-cohere-rerank>=0.5.0,<0.6.0",
"llama-index-vector-stores-chroma>=0.5.0,<0.6.0",
"llama-index>=0.14.12,<0.15.0",
"llama-index>=0.14.12,<0.14.13",
"llama-index-core>=0.14.12,<0.14.13",
"llama-parse>=0.6.0,<0.7.0",
"onnxruntime<1.20.0",
"openai>=1.52.2,<2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -781,8 +781,8 @@ def test_chat_reasoning(instrument_legacy, span_exporter,
assert len(spans) >= 1
span = spans[-1]

assert span.attributes["gen_ai.request.reasoning_effort"] == "low"
assert span.attributes["gen_ai.usage.reasoning_tokens"] > 0
assert span.attributes[SpanAttributes.LLM_REQUEST_REASONING_EFFORT] == "low"
assert span.attributes[SpanAttributes.LLM_USAGE_REASONING_TOKENS] > 0


def assert_message_in_logs(log: ReadableLogRecord, event_name: str, expected_content: dict):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1493,8 +1493,8 @@ def test_chat_reasoning(instrument_legacy, span_exporter,
assert len(spans) >= 1
span = spans[-1]

assert span.attributes["gen_ai.request.reasoning_effort"] == "low"
assert span.attributes["gen_ai.usage.reasoning_tokens"] > 0
assert span.attributes[SpanAttributes.LLM_REQUEST_REASONING_EFFORT] == "low"
assert span.attributes[SpanAttributes.LLM_USAGE_REASONING_TOKENS] > 0


@pytest.mark.vcr
Expand Down
Loading
Loading