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
25 changes: 22 additions & 3 deletions basalt/observability/context_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,13 +725,32 @@ def _with_span_handle(

tokens.append(attach(set_value(FEATURE_SLUG_CONTEXT_KEY, feature_slug)))

# If this is a root span (no parent), store it in context
is_root = parent_span is None
# Determine if this should be treated as a Basalt root span
# A span is a Basalt root if:
# 1. There's no parent span at all (true root), OR
# 2. This is a start_observe (span_type="basalt_trace") AND the parent is NOT a Basalt span
# (e.g., parent is from FastAPI, httpx, or other instrumentation)
Comment on lines +728 to +732
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Root-span detection now treats start_observe spans as Basalt roots when there’s a non-Basalt parent span (e.g., FastAPI/httpx). There are existing context-manager tests, but none appear to cover the “external parent span present” case; add a unit test that sets a non-Basalt current span (with no ROOT_SPAN_CONTEXT_KEY) and asserts start_observe sets basalt.root and attaches ROOT_SPAN_CONTEXT_KEY.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

root_span_token = None

# Check if we're inside a basalt trace
# Check if we're already inside a basalt trace
in_basalt_trace = otel_context.get_value(ROOT_SPAN_CONTEXT_KEY) is not None

# Determine root status
if parent_span is None:
# No parent at all - this is a true root
is_root = True
elif span_type == "basalt_trace" and not in_basalt_trace:
# Parent exists but it's NOT a Basalt span (e.g., FastAPI HTTP span)
# AND this is a start_observe call (basalt_trace type)
# -> Treat as Basalt root (allows start_observe to work inside FastAPI handlers)
is_root = True
else:
# Parent exists and either:
# - We're already in a Basalt trace, OR
# - This is not a start_observe call
# -> Treat as nested span
is_root = False

# Make trace-level sampling decision
should_evaluate_token = None
if is_root:
Expand Down
8 changes: 8 additions & 0 deletions basalt/observability/instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,14 @@ def _instrument_providers(self, config: TelemetryConfig) -> None:

# Try to import the instrumentor
instrumentor_cls = _safe_import(module_name, class_name)
if not instrumentor_cls and provider_key == "google_generativeai":
# Fallback: some environments use the GenAI SDK under the same provider name.
instrumentor_cls = _safe_import(
"opentelemetry.instrumentation.google_genai",
"GoogleGenAiSdkInstrumentor",
)
if instrumentor_cls:
module_name = "opentelemetry.instrumentation.google_genai"
if not instrumentor_cls:
logger.debug(
f"Provider '{provider_key}' instrumentor not available. "
Expand Down
39 changes: 39 additions & 0 deletions examples/microservices/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python

# Virtual environments
venv/
env/
ENV/

# Hatch
.hatch/

# Environment files - NEVER commit actual secrets!
.envrc
.envrc.local
.env
.env.local

# IDEs
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Logs
*.log

# Testing
.pytest_cache/
.coverage
htmlcov/
36 changes: 36 additions & 0 deletions examples/microservices/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "basalt-microservices-example"
version = "0.1.0"
description = "Microservices example demonstrating Basalt observability across HTTP communication"
readme = "README.md"
Comment thread
CorentinGS marked this conversation as resolved.
requires-python = ">=3.10"
dependencies = []

[tool.hatch.envs.default]
detached = true
dependencies = [
# Web framework dependencies
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"httpx>=0.25.0",

Comment thread
CorentinGS marked this conversation as resolved.
# LLM SDKs
"openai==2.15.0",
"google-genai==1.60.0",

# LLM Auto-instrumentation (FastAPI instrumentation removed due to conflicts with Basalt)
"opentelemetry-instrumentation-openai~=0.51.0",
# "opentelemetry-instrumentation-google-genai~=0.5b0", # NEW Google GenAI SDK (from google import genai)
"opentelemetry-instrumentation-google-generativeai~=0.51.0",
"opentelemetry-instrumentation-fastapi",
# OTLP Exporter for local telemetry collection
"opentelemetry-exporter-otlp-proto-grpc~=1.39.1",
]

[tool.hatch.envs.default.scripts]
service-a = "uvicorn service_a.main:app --host 0.0.0.0 --port 8001"
service-b = "uvicorn service_b.main:app --host 0.0.0.0 --port 8002"
74 changes: 74 additions & 0 deletions examples/microservices/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
set -e

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Check if direnv is loaded
if [ -z "$BASALT_API_KEY" ]; then
echo -e "${RED}Error: Environment not loaded.${NC}"
echo "Please run: ${YELLOW}direnv allow${NC}"
echo "Then edit .envrc with your Basalt API key"
exit 1
fi

echo -e "${GREEN}Environment loaded successfully${NC}"
echo "BASALT_ENVIRONMENT: $BASALT_ENVIRONMENT"
echo "BASALT_SERVICE_NAME: $BASALT_SERVICE_NAME"
echo ""

# Change to the microservices directory
cd "$(dirname "$0")"

# Check if hatch environment exists
if ! hatch env show default &>/dev/null; then
echo -e "${YELLOW}Creating hatch environment...${NC}"
hatch env create
fi

# Install basalt-py from parent directory
echo -e "${YELLOW}Installing basalt-py from parent directory...${NC}"
hatch run pip install -q -e ../..

echo -e "${GREEN}Starting microservices...${NC}"
echo ""

# Start services in background
echo -e "${GREEN}Starting Service B on port 8002...${NC}"
hatch run service-b &
SERVICE_B_PID=$!

# Wait a moment for Service B to start
sleep 2

echo -e "${GREEN}Starting Service A on port 8001...${NC}"
hatch run service-a &
SERVICE_A_PID=$!

# Wait for services to fully start
sleep 2

# Trap to cleanup on exit
trap "echo -e '\n${YELLOW}Shutting down services...${NC}'; kill $SERVICE_A_PID $SERVICE_B_PID 2>/dev/null; exit 0" SIGINT SIGTERM EXIT

echo ""
echo -e "${GREEN}✓ Services started successfully!${NC}"
echo ""
echo "Service A: http://localhost:8001"
echo "Service B: http://localhost:8002"
echo ""
echo -e "${YELLOW}Test the microservices:${NC}"
echo " curl http://localhost:8001/process-request"
echo ""
echo -e "${YELLOW}Health checks:${NC}"
echo " curl http://localhost:8001/health"
echo " curl http://localhost:8002/health"
echo ""
echo -e "${RED}Press Ctrl+C to stop all services${NC}"
echo ""

# Wait for processes
wait
1 change: 1 addition & 0 deletions examples/microservices/service_a/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Service A - Primary service that orchestrates requests to Service B."""
Loading