Skip to content
Open
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
1 change: 1 addition & 0 deletions app/agent/investigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"azure": ["azure", "azure_sql"],
"splunk": ["splunk"],
"signoz": ["signoz"],
"tempo": ["tempo"],
}

# Callback type: called with (event_kind, data_dict) during the agent loop.
Expand Down
52 changes: 52 additions & 0 deletions app/cli/wizard/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ def validate_splunk_integration(**kwargs):
return _validate(**kwargs)


def validate_tempo_integration(**kwargs):
from app.cli.wizard.integration_health import validate_tempo_integration as _validate

return _validate(**kwargs)


def get_sentry_auth_recommendations():
from app.integrations.sentry import get_sentry_auth_recommendations as _get

Expand Down Expand Up @@ -1681,6 +1687,45 @@ def _configure_telegram() -> tuple[str, str]:
_console.print(f"[{SECONDARY}]Try again or press Ctrl+C to cancel.[/]")


def _configure_tempo() -> tuple[str, str]:
_, credentials = _integration_defaults("tempo")
_console.print(
f"[{SECONDARY}]Tempo commonly runs without auth behind a gateway — a URL alone is enough.[/]"
)
while True:
url = _prompt_value(
"Tempo URL (e.g. http://localhost:3200)",
default=_string_value(credentials.get("url")),
)
api_key = _prompt_value(
"Tempo bearer token (optional, leave blank if none)",
default=_string_value(credentials.get("api_key")),
secret=True,
allow_empty=True,
)
org_id = _prompt_value(
"Tempo tenant / X-Scope-OrgID (optional, leave blank if single-tenant)",
default=_string_value(credentials.get("org_id")),
allow_empty=True,
)
with _console.status("Validating Tempo integration...", spinner="dots"):
result = validate_tempo_integration(url=url, api_key=api_key, org_id=org_id)
_render_integration_result("Tempo", result)
if result.ok:
creds: dict[str, str] = {"url": url}
if api_key:
creds["api_key"] = api_key
if org_id:
creds["org_id"] = org_id
upsert_integration("tempo", {"credentials": creds})
env_values: dict[str, str] = {"TEMPO_URL": url}
if org_id:
env_values["TEMPO_ORG_ID"] = org_id
env_path = sync_env_values(env_values)
return "Tempo", str(env_path)
_console.print(f"[{SECONDARY}]Try again or press Ctrl+C to cancel.[/]")


def _configure_splunk() -> tuple[str, str]:
_, credentials = _integration_defaults("splunk")
while True:
Expand Down Expand Up @@ -1931,6 +1976,11 @@ def _configure_selected_integrations() -> tuple[list[str], str | None]:
label="OpenSearch / Elasticsearch",
hint="Query logs and indices from OpenSearch or Elasticsearch clusters",
),
Choice(
value="tempo",
label="Grafana Tempo",
hint="Query distributed traces from a standalone Tempo backend",
),
Choice(
value="skip",
label="Skip for now",
Expand Down Expand Up @@ -1969,6 +2019,7 @@ def _configure_selected_integrations() -> tuple[list[str], str | None]:
"openclaw": _configure_openclaw,
"opensearch": _configure_opensearch,
"splunk": _configure_splunk,
"tempo": _configure_tempo,
}
_SERVICE_LABELS = {
"grafana_local": "grafana local",
Expand All @@ -1992,6 +2043,7 @@ def _configure_selected_integrations() -> tuple[list[str], str | None]:
"notion": "notion",
"openclaw": "openclaw",
"opensearch": "opensearch",
"tempo": "grafana tempo",
}

_step(f"Service · {_SERVICE_LABELS.get(selected_service, selected_service)}")
Expand Down
2 changes: 2 additions & 0 deletions app/cli/wizard/integration_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
validate_opsgenie_integration,
validate_sentry_integration,
validate_splunk_integration,
validate_tempo_integration,
validate_vercel_integration,
)
from app.cli.wizard.integration_validators.http_probe_validators import (
Expand Down Expand Up @@ -55,5 +56,6 @@
"validate_slack_webhook",
"validate_telegram_bot",
"validate_splunk_integration",
"validate_tempo_integration",
"validate_vercel_integration",
]
26 changes: 26 additions & 0 deletions app/cli/wizard/integration_validators/client_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
IncidentIoIntegrationConfig,
)
from app.integrations.sentry import build_sentry_config, validate_sentry_config
from app.integrations.tempo import build_tempo_config, validate_tempo_config
from app.services.alertmanager import make_alertmanager_client
from app.services.coralogix import CoralogixClient
from app.services.datadog import DatadogClient, DatadogConfig
Expand Down Expand Up @@ -480,6 +481,31 @@ def validate_splunk_integration(
)


def validate_tempo_integration(
*,
url: str,
api_key: str = "",
username: str = "",
password: str = "",
org_id: str = "",
) -> IntegrationHealthResult:
"""Validate Tempo connectivity via the tag-search endpoint."""
try:
config = build_tempo_config(
{
"url": url,
"api_key": api_key,
"username": username,
"password": password,
"org_id": org_id,
}
)
except Exception as err:
return IntegrationHealthResult(ok=False, detail=f"Tempo config invalid: {err}")
result = validate_tempo_config(config)
return IntegrationHealthResult(ok=result.ok, detail=result.detail)


def validate_opensearch_integration(
*,
url: str,
Expand Down
31 changes: 31 additions & 0 deletions app/integrations/_catalog_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
from app.integrations.signoz import build_signoz_config, signoz_config_from_env
from app.integrations.store import _STRUCTURAL_RECORD_FIELDS, load_integrations
from app.integrations.supabase import build_supabase_config
from app.integrations.tempo import build_tempo_config, tempo_config_from_env
from app.llm_credentials import resolve_env_credential
from app.services.vercel import VercelConfig
from app.utils.coercion import safe_int
Expand Down Expand Up @@ -950,6 +951,24 @@ def _classify_service_instance(
return signoz_config.model_dump(), "signoz"
return None, None

if key == "tempo":
try:
tempo_config = build_tempo_config(
{
"url": credentials.get("url", ""),
"api_key": credentials.get("api_key", ""),
"username": credentials.get("username", ""),
"password": credentials.get("password", ""),
"org_id": credentials.get("org_id", ""),
"integration_id": record_id,
}
)
except Exception:
return None, None
if tempo_config.is_configured:
return tempo_config.model_dump(), "tempo"
return None, None

# Fallback for unknown services: pass through credentials + record id.
return {"credentials": credentials, "integration_id": record_id}, key

Expand Down Expand Up @@ -1871,6 +1890,18 @@ def load_env_integrations() -> list[dict[str, Any]]:
except Exception:
logger.debug("Failed to load SigNoz config from env", exc_info=True)

try:
tempo_config = tempo_config_from_env()
if tempo_config is not None and tempo_config.is_configured:
integrations.append(
_active_env_record(
"tempo",
tempo_config.model_dump(exclude={"integration_id"}),
)
)
except Exception:
logger.debug("Failed to load Tempo config from env", exc_info=True)

return integrations


Expand Down
7 changes: 7 additions & 0 deletions app/integrations/_verification_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from app.integrations.sentry import build_sentry_config, validate_sentry_config
from app.integrations.signoz import build_signoz_config, validate_signoz_config
from app.integrations.supabase import build_supabase_config, validate_supabase_config
from app.integrations.tempo import build_tempo_config, validate_tempo_config
from app.services.alertmanager import AlertmanagerClient, AlertmanagerConfig
from app.services.argocd import ArgoCDClient, ArgoCDConfig
from app.services.coralogix import CoralogixClient
Expand Down Expand Up @@ -543,6 +544,11 @@ def _verify_opensearch(source: str, config: dict[str, Any]) -> dict[str, str]:
build_config=build_signoz_config,
validate_config=validate_signoz_config,
)
_verify_tempo = build_validation_verifier(
"tempo",
build_config=build_tempo_config,
validate_config=validate_tempo_config,
)


def _build_kafka_config(raw: dict[str, Any]) -> Any:
Expand Down Expand Up @@ -705,6 +711,7 @@ def _verify_supabase(service: str, config: dict[str, Any]) -> dict[str, str]:
"_verify_rabbitmq",
"_verify_sentry",
"_verify_signoz",
"_verify_tempo",
"_verify_slack",
"_verify_slack_without_test",
"_verify_snowflake",
Expand Down
18 changes: 18 additions & 0 deletions app/integrations/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,23 @@ def _setup_signoz() -> None:
)


def _setup_tempo() -> None:
url = _p("Tempo URL (e.g. http://localhost:3200 for local Docker)")
if not url:
_die("Tempo URL is required.")

api_key = _p("Tempo bearer token (optional, leave blank if none)", secret=True)
org_id = _p("Tempo tenant / X-Scope-OrgID (optional, leave blank if single-tenant)")

credentials: dict[str, str] = {"url": url}
if api_key:
credentials["api_key"] = api_key
if org_id:
credentials["org_id"] = org_id

upsert_integration("tempo", {"credentials": credentials})


_HANDLERS: dict[str, Any] = {
"alertmanager": _setup_alertmanager,
"aws": _setup_aws,
Expand Down Expand Up @@ -857,6 +874,7 @@ def _setup_signoz() -> None:
"postgresql": _setup_postgresql,
"mysql": _setup_mysql,
"signoz": _setup_signoz,
"tempo": _setup_tempo,
}


Expand Down
1 change: 1 addition & 0 deletions app/integrations/effective_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ class EffectiveIntegrations(StrictConfigModel):
victoria_logs: EffectiveIntegrationEntry | None = None
alicloud: EffectiveIntegrationEntry | None = None
signoz: EffectiveIntegrationEntry | None = None
tempo: EffectiveIntegrationEntry | None = None
8 changes: 8 additions & 0 deletions app/integrations/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
_verify_splunk,
_verify_supabase,
_verify_telegram,
_verify_tempo,
_verify_tracer,
_verify_twilio,
_verify_vercel,
Expand Down Expand Up @@ -358,6 +359,13 @@ class IntegrationSpec:
setup_order=23,
verify_order=35,
),
IntegrationSpec(
service="tempo",
verifier=_verify_tempo,
direct_effective=True,
setup_order=24,
verify_order=36,
),
)

INTEGRATION_SPECS_BY_SERVICE = {spec.service: spec for spec in INTEGRATION_SPECS}
Expand Down
Loading