Files
o11y/tests/fixtures/alerts.py
zeekayandGitHub babbdec4b8 debrand: SigNoz -> O11y across the tree (+ otel-collector v0.144.7, schema cutover migration) (#28)
* debrand: signoz/SigNoz/SIGNOZ -> o11y/O11y/O11Y across the tree

Rename all SigNoz *branding* to O11y in file contents, file/dir names,
package names, env vars, config keys, comments, docs, SDK code, and the
ClickHouse schema identifiers the querier reads.

Collector dependency:
- Repoint github.com/hanzoai/signoz-otel-collector ->
  github.com/hanzoai/otel-collector and bump v0.144.6 -> v0.144.7
  (go.mod + go.sum reconciled via go mod tidy). Internal package paths
  also debranded upstream: signozschemamigrator -> o11yschemamigrator,
  signozlogspipelineprocessor -> o11ylogspipelineprocessor.
- Fix: v0.144.7 dropped otelconst.DistributedFieldKeysTable from the
  collector's public constants; pin FieldKeysTable = "distributed_field_keys"
  locally in pkg/telemetrymetadata/tables.go.

Package/dir renames (package decl + all importers):
  pkg/apiserver/signozapiserver -> o11yapiserver, plus signozalertmanager,
  signozauthzapi, signozglobal, signozquerier, signozruler, and pkg/signoz -> pkg/o11y.

Schema (read plane): signoz_traces/metrics/logs/metadata/analytics/meter and
signoz_index_v3/index_v2/error_index_v2/spans (+ distributed_*) -> o11y_*.
Data-preserving cutover migration added:
  deploy/clickhouse/migrations/0001_rename_signoz_to_o11y.sql
  (metadata-only RENAME DATABASE/TABLE, no drop/recreate).

Preserved (attribution / external / wire contracts):
- LICENSE + NOTICE verbatim (incl. "software from SigNoz", "Copyright ... SigNoz Inc.").
- Upstream github.com/SigNoz/* repo URLs + @SigNoz/* CODEOWNERS teams.
- Billing resource-attribute regex "signoz.workspace.*" (external wire contract).

go build ./pkg/... ./cmd/... = 0; gofmt clean; renamed+schema pkg tests pass.

* o11y: bump collector to v0.144.8 (o11y_* physical schema writer) + lockstep deploy plan

Collector v0.144.8's schema-migrator + exporters now CREATE/WRITE the same
o11y_* physical databases/tables the querier reads and the RENAME migration
(0001_rename_signoz_to_o11y.sql) targets.

Cross-checked byte-identical: writer DB set == migration target DB set (6);
writer prefixed-table set == migration target table set (8); o11y querier
reads and cloud reads are subsets of that set. Zero table-name mismatches
between writer, readers, and migration.

Collector go.mod unchanged between v0.144.7 and v0.144.8 (identical go.mod
hash) — pure source rename, no module-graph change.

Adds deploy/clickhouse/DEPLOY_signoz_to_o11y_cutover.md: the lockstep order
(collector v0.144.8 -> RENAME migration -> o11y+cloud readers) with a
fresh/scratch-ClickHouse pre-flight (create via migrator, emit trace+metric,
query back) and rollback. Ships together or telemetry blackholes.

go build ./pkg/... ./cmd/community = 0.
2026-07-09 13:08:55 -07:00

221 lines
8.4 KiB
Python

import base64
import json
import time
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from http import HTTPStatus
import pytest
import requests
from fixtures import types
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.fs import get_testdata_file_path
from fixtures.logger import setup_logger
from fixtures.logs import Logs
from fixtures.metrics import Metrics
from fixtures.traces import Traces
logger = setup_logger(__name__)
@pytest.fixture(name="create_alert_rule", scope="function")
def create_alert_rule(o11y: types.O11y, get_token: Callable[[str, str], str]) -> Callable[[dict], str]:
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
rule_ids = []
def _create_alert_rule(rule_data: dict) -> str:
response = requests.post(
o11y.self.host_configs["8080"].get("/api/v1/rules"),
json=rule_data,
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
assert response.status_code == HTTPStatus.OK, f"Failed to create rule, api returned {response.status_code} with response: {response.text}"
rule_id = response.json()["data"]["id"]
rule_ids.append(rule_id)
return rule_id
def _delete_alert_rule(rule_id: str):
logger.info("Deleting rule: %s", {"rule_id": rule_id})
response = requests.delete(
o11y.self.host_configs["8080"].get(f"/api/v1/rules/{rule_id}"),
headers={"Authorization": f"Bearer {admin_token}"},
timeout=5,
)
if response.status_code != HTTPStatus.OK:
raise Exception( # pylint: disable=broad-exception-raised
f"Failed to delete rule, api returned {response.status_code} with response: {response.text}"
)
yield _create_alert_rule
# delete the rule on cleanup
for rule_id in rule_ids:
try:
_delete_alert_rule(rule_id)
except Exception as e: # pylint: disable=broad-exception-caught
logger.error("Error deleting rule: %s", {"rule_id": rule_id, "error": e})
@pytest.fixture(name="insert_alert_data", scope="function")
def insert_alert_data(
insert_metrics: Callable[[list[Metrics]], None],
insert_traces: Callable[[list[Traces]], None],
insert_logs: Callable[[list[Logs]], None],
) -> Callable[[list[types.AlertData]], None]:
def _insert_alert_data(
alert_data_items: list[types.AlertData],
base_time: datetime = None,
) -> None:
metrics: list[Metrics] = []
traces: list[Traces] = []
logs: list[Logs] = []
now = base_time or datetime.now(tz=UTC).replace(second=0, microsecond=0)
for data_item in alert_data_items:
if data_item.type == "metrics":
_metrics = Metrics.load_from_file(
get_testdata_file_path(data_item.data_path),
base_time=now,
)
metrics.extend(_metrics)
elif data_item.type == "traces":
_traces = Traces.load_from_file(
get_testdata_file_path(data_item.data_path),
base_time=now,
)
traces.extend(_traces)
elif data_item.type == "logs":
_logs = Logs.load_from_file(
get_testdata_file_path(data_item.data_path),
base_time=now,
)
logs.extend(_logs)
# Add data to ClickHouse if any data is present
if len(metrics) > 0:
insert_metrics(metrics)
if len(traces) > 0:
insert_traces(traces)
if len(logs) > 0:
insert_logs(logs)
yield _insert_alert_data
def collect_webhook_firing_alerts(webhook_test_container: types.TestContainerDocker, notification_channel_name: str) -> list[types.FiringAlert]:
# Prepare the endpoint path for the channel name, for alerts tests we have
# used different paths for receiving alerts from each channel so that
# multiple rules can be tested in isolation.
rule_webhook_endpoint = f"/alert/{notification_channel_name}"
url = webhook_test_container.host_configs["8080"].get("__admin/requests/find")
req = {
"method": "POST",
"url": rule_webhook_endpoint,
}
res = requests.post(url, json=req, timeout=5)
assert res.status_code == HTTPStatus.OK, f"Failed to collect firing alerts for notification channel {notification_channel_name}, status code: {res.status_code}, response: {res.text}"
response = res.json()
alerts = []
for req in response["requests"]:
alert_body_base64 = req["bodyAsBase64"]
alert_body = base64.b64decode(alert_body_base64).decode("utf-8")
# remove newlines from the alert body
alert_body = alert_body.replace("\n", "")
alert_dict = json.loads(alert_body) # parse the alert body into a dictionary
for a in alert_dict["alerts"]:
alerts.append(types.FiringAlert(labels=a["labels"]))
return alerts
def _verify_alerts_labels(firing_alerts: list[dict[str, str]], expected_alerts: list[dict[str, str]]) -> tuple[int, list[dict[str, str]]]:
"""
Checks how many of the expected alerts have been fired.
Returns the count of expected alerts that have been fired.
"""
fired_count = 0
missing_alerts = []
for alert in expected_alerts:
is_alert_fired = False
for fired_alert in firing_alerts:
# Check if current expected alert is present in the fired alerts
if all(key in fired_alert and fired_alert[key] == value for key, value in alert.items()):
is_alert_fired = True
break
if is_alert_fired:
fired_count += 1
else:
missing_alerts.append(alert)
return (fired_count, missing_alerts)
def verify_webhook_alert_expectation(
test_alert_container: types.TestContainerDocker,
notification_channel_name: str,
alert_expectations: types.AlertExpectation,
) -> bool:
# time to wait till the expected alerts are fired
time_to_wait = datetime.now() + timedelta(seconds=alert_expectations.wait_time_seconds)
expected_alerts_labels = [alert.labels for alert in alert_expectations.expected_alerts]
while datetime.now() < time_to_wait:
firing_alerts = collect_webhook_firing_alerts(test_alert_container, notification_channel_name)
firing_alert_labels = [alert.labels for alert in firing_alerts]
if alert_expectations.should_alert:
# verify the number of alerts fired, currently we're only verifying the labels of the alerts
# but there could be verification of annotations and other fields in the FiringAlert
(verified_count, missing_alerts) = _verify_alerts_labels(firing_alert_labels, expected_alerts_labels)
if verified_count == len(alert_expectations.expected_alerts):
logger.info("Got expected number of alerts: %s", {"count": verified_count})
return True
# No alert is supposed to be fired if should_alert is False
elif len(firing_alerts) > 0:
break
# wait for some time before checking again
time.sleep(1)
# We've waited but we didn't get the expected number of alerts
# check if alert was expected to be fired or not, if not then we're good
if not alert_expectations.should_alert:
assert len(firing_alerts) == 0, (
"Expected no alerts to be fired, ",
f"got {len(firing_alerts)} alerts, firing alerts: {firing_alerts}",
)
logger.info("No alerts fired, as expected")
return True
# we've waited but we didn't get the expected number of alerts, raise an exception
assert verified_count == len(alert_expectations.expected_alerts), (
f"Expected {len(alert_expectations.expected_alerts)} alerts to be fired but got {verified_count} alerts, ",
f"missing alerts: {missing_alerts}, ",
f"firing alerts: {firing_alerts}",
)
return True # should not reach here
def update_rule_channel_name(rule_data: dict, channel_name: str):
"""
updates the channel name in the thresholds
so alert notification are sent to the given channel
"""
thresholds = rule_data["condition"]["thresholds"]
if "kind" in thresholds and thresholds["kind"] == "basic":
# loop over all the sepcs and update the channels
for spec in thresholds["spec"]:
spec["channels"] = [channel_name]