mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
587 lines
20 KiB
Python
587 lines
20 KiB
Python
from collections.abc import Mapping
|
|
|
|
import pytest
|
|
|
|
from sentry.spans.grouping.strategy.base import (
|
|
Span,
|
|
SpanGroupingStrategy,
|
|
loose_normalized_db_span_in_condition_strategy,
|
|
normalized_db_span_in_condition_strategy,
|
|
parametrize_db_span_strategy,
|
|
raw_description_strategy,
|
|
remove_http_client_query_string_strategy,
|
|
remove_redis_command_arguments_strategy,
|
|
)
|
|
from sentry.spans.grouping.strategy.config import (
|
|
CONFIGURATIONS,
|
|
SpanGroupingConfig,
|
|
register_configuration,
|
|
)
|
|
from sentry.spans.grouping.utils import hash_values
|
|
from sentry.testutils.performance_issues.span_builder import SpanBuilder
|
|
|
|
|
|
def test_register_duplicate_confiig() -> None:
|
|
config_id = "test-configuration"
|
|
register_configuration(config_id, [])
|
|
with pytest.raises(ValueError, match=f"Duplicate configuration id: {config_id}"):
|
|
register_configuration(config_id, [])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"span,fingerprint",
|
|
[
|
|
(SpanBuilder().build(), [""]),
|
|
(SpanBuilder().with_description("").build(), [""]),
|
|
(SpanBuilder().with_description("test description").build(), ["test description"]),
|
|
],
|
|
)
|
|
def test_raw_description_strategy(span: Span, fingerprint: list[str] | None) -> None:
|
|
assert raw_description_strategy(span) == fingerprint
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"span,fingerprint",
|
|
[
|
|
(SpanBuilder().build(), None),
|
|
# description does not have an IN condition
|
|
(SpanBuilder().with_op("db").build(), None),
|
|
# op is not db and description does have an IN condition
|
|
(SpanBuilder().with_description("SELECT count() FROM table WHERE id = %s").build(), None),
|
|
# description does not have an IN condition
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db")
|
|
.with_description("SELECT count() FROM table WHERE id = %s")
|
|
.build(),
|
|
None,
|
|
),
|
|
# op is not db
|
|
(
|
|
SpanBuilder().with_description("SELECT count() FROM table WHERE id IN (%s)").build(),
|
|
None,
|
|
),
|
|
# op is db
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db")
|
|
.with_description("SELECT count() FROM table WHERE id IN (%s)")
|
|
.build(),
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db")
|
|
.with_description("SELECT count() FROM table WHERE id IN (%s, %s)")
|
|
.build(),
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
# op is a db query
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db.sql.query")
|
|
.with_description("SELECT count() FROM table WHERE id IN (%s, %s)")
|
|
.build(),
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
# the number of %s is relevant
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db")
|
|
.with_description("SELECT count() FROM table WHERE id IN (%s, %s, %s, %s, %s)")
|
|
.build(),
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
# the number of spaces around the commas is irrelevant
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db")
|
|
.with_description("SELECT count() FROM table WHERE id IN (%s,%s , %s , %s , %s)")
|
|
.build(),
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
],
|
|
)
|
|
def test_normalized_db_span_in_condition_strategy(
|
|
span: Span, fingerprint: list[str] | None
|
|
) -> None:
|
|
assert normalized_db_span_in_condition_strategy(span) == fingerprint
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"span,fingerprint",
|
|
[
|
|
# description has multiple IN statements
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db.sql.query")
|
|
.with_description(
|
|
"SELECT count() FROM table WHERE id IN (%s, %s) AND id IN (%s, %s, %s)"
|
|
)
|
|
.build(),
|
|
["SELECT count() FROM table WHERE id IN (%s) AND id IN (%s)"],
|
|
),
|
|
# supports unparametrized queries
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db.sql.query")
|
|
.with_description("SELECT count() FROM table WHERE id IN (100, 101, 102)")
|
|
.build(),
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
# supports lowercase IN
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db.sql.query")
|
|
.with_description("select count() from table where id in (100, 101, 102)")
|
|
.build(),
|
|
["select count() from table where id IN (%s)"],
|
|
),
|
|
# op is an ActiveRecord query
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db.sql.active_record")
|
|
.with_description("SELECT count() FROM table WHERE id IN ($1, $2, $3)")
|
|
.build(),
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
# op is a Laravel query
|
|
(
|
|
SpanBuilder()
|
|
.with_op("db.sql.query")
|
|
.with_description("SELECT count() FROM table WHERE id IN (?, ?, ?)")
|
|
.build(),
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
],
|
|
)
|
|
def test_loose_normalized_db_span_in_condition_strategy(
|
|
span: Span, fingerprint: list[str] | None
|
|
) -> None:
|
|
assert loose_normalized_db_span_in_condition_strategy(span) == fingerprint
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"query,fingerprint",
|
|
[
|
|
# parametrizes numbers
|
|
("SELECT * FROM table WHERE id = 1", ["SELECT * FROM table WHERE id = %s"]),
|
|
("SELECT * FROM table LIMIT 1", ["SELECT * FROM table LIMIT %s"]),
|
|
(
|
|
"SELECT * FROM table WHERE temperature > -100",
|
|
["SELECT * FROM table WHERE temperature > %s"],
|
|
),
|
|
("SELECT * FROM table WHERE salary > 1e7", ["SELECT * FROM table WHERE salary > %s"]),
|
|
("SELECT * FROM table123 WHERE id = %s", None),
|
|
("SELECT * FROM ta123ble WHERE id = %s", None),
|
|
("SELECT * FROM `123table` WHERE id = %s", None),
|
|
# parametrizes single-quoted strings
|
|
("SELECT * FROM table WHERE sku = 'foo'", ["SELECT * FROM table WHERE sku = %s"]),
|
|
(
|
|
"SELECT * FROM table WHERE quote = 'it\\'s a string",
|
|
["SELECT * FROM table WHERE quote = %s"],
|
|
),
|
|
# leaves double-quoted strings (used for string literals in MySQL but identifiers in PostgreSQL)
|
|
('SELECT * from "table" WHERE sku = %s', None), # PG
|
|
('SELECT * from table WHERE sku = "foo"', None), # MySQL
|
|
# parametrizes booleans
|
|
("SELECT * FROM table WHERE deleted = true", ["SELECT * FROM table WHERE deleted = %s"]),
|
|
("SELECT * FROM table WHERE deleted = false", ["SELECT * FROM table WHERE deleted = %s"]),
|
|
("SELECT * FROM table_true WHERE deleted = %s", None),
|
|
("SELECT * FROM true_table WHERE deleted = %s", None),
|
|
("SELECT * FROM tatrueble WHERE deleted = %s", None),
|
|
# leaves nulls alone
|
|
("SELECT * FROM table WHERE deleted_at IS NULL", None),
|
|
# supports all the cases loose_normalized_db_span_in_condition_strategy does
|
|
(
|
|
"SELECT count() FROM table WHERE id IN (%s, %s) AND id IN (%s, %s, %s)",
|
|
["SELECT count() FROM table WHERE id IN (%s) AND id IN (%s)"],
|
|
),
|
|
(
|
|
"SELECT count() FROM table WHERE id IN (100, 101, 102)",
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
(
|
|
"select count() from table where id in (100, 101, 102)",
|
|
["select count() from table where id IN (%s)"],
|
|
),
|
|
(
|
|
"SELECT count() FROM table WHERE id IN ($1, $2, $3)",
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
(
|
|
"SELECT count() FROM table WHERE id IN (?, ?, ?)",
|
|
["SELECT count() FROM table WHERE id IN (%s)"],
|
|
),
|
|
# supports SAVEPOINTS with unquoted, backtick-quoted (MySQL), or
|
|
# double-quoted (PostgreSQL) identifiers
|
|
("SAVEPOINT unquoted_identifier", ["SAVEPOINT %s"]),
|
|
("SAVEPOINT unquoted_identifier;", ["SAVEPOINT %s;"]),
|
|
("savepoint unquoted_identifier", ["SAVEPOINT %s"]),
|
|
(
|
|
'SAVEPOINT "pg_quoted_identifier"',
|
|
["SAVEPOINT %s"],
|
|
),
|
|
(
|
|
"SAVEPOINT `mysql_quoted_identifier`",
|
|
["SAVEPOINT %s"],
|
|
),
|
|
],
|
|
)
|
|
def test_parametrize_db_span_strategy(query: str, fingerprint: list[str] | None) -> None:
|
|
span = SpanBuilder().with_op("db.sql.query").with_description(query).build()
|
|
assert parametrize_db_span_strategy(span) == fingerprint
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"span,fingerprint",
|
|
[
|
|
(SpanBuilder().build(), None),
|
|
# description is not of form `<HTTP METHOD> <URL>`
|
|
(SpanBuilder().with_op("http.client").build(), None),
|
|
# op is not http.client
|
|
(
|
|
SpanBuilder()
|
|
.with_description(
|
|
"GET https://sentry.io/api/0/organization/sentry/projects/?all_projects=1"
|
|
)
|
|
.build(),
|
|
None,
|
|
),
|
|
# description is not of form `<HTTP METHOD> <URL>`
|
|
(
|
|
SpanBuilder().with_op("http.client").with_description("making an http request").build(),
|
|
None,
|
|
),
|
|
# best effort when description is not a valid url
|
|
(
|
|
SpanBuilder()
|
|
.with_op("http.client")
|
|
.with_description("GET https://[this-is-not-a-valid-url?query")
|
|
.build(),
|
|
["GET", "https", "[this-is-not-a-valid-url", ""],
|
|
),
|
|
pytest.param(
|
|
SpanBuilder()
|
|
.with_op("http.client")
|
|
.with_description("GET https://[Filtered]@3x.png")
|
|
.build(),
|
|
["GET", "https", "[Filtered]@3x.png", ""],
|
|
),
|
|
(
|
|
SpanBuilder()
|
|
.with_op("http.client")
|
|
.with_description(
|
|
"GET https://sentry.io/api/0/organization/sentry/projects/?all_projects=1"
|
|
)
|
|
.build(),
|
|
["GET", "https", "sentry.io", "/api/0/organization/sentry/projects/"],
|
|
),
|
|
],
|
|
)
|
|
def test_remove_http_client_query_string_strategy(
|
|
span: Span, fingerprint: list[str] | None
|
|
) -> None:
|
|
assert remove_http_client_query_string_strategy(span) == fingerprint
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"span,fingerprint",
|
|
[
|
|
(SpanBuilder().build(), None),
|
|
# op is not `redis`
|
|
(SpanBuilder().with_description("INCRBY 'key' 1").build(), None),
|
|
(SpanBuilder().with_op("redis").with_description("INCRBY 'key' 1").build(), ["INCRBY"]),
|
|
(SpanBuilder().with_op("db.redis").with_description("INCRBY 'key' 1").build(), ["INCRBY"]),
|
|
],
|
|
)
|
|
def test_remove_redis_command_arguments_strategy(span: Span, fingerprint: list[str] | None) -> None:
|
|
assert remove_redis_command_arguments_strategy(span) == fingerprint
|
|
|
|
|
|
def test_reuse_existing_grouping_results() -> None:
|
|
config_id = "test-configuration"
|
|
strategy = SpanGroupingStrategy(config_id, [])
|
|
config = SpanGroupingConfig(config_id, strategy)
|
|
event = {
|
|
"transaction": "transaction name",
|
|
"contexts": {
|
|
"trace": {
|
|
"span_id": "a" * 16,
|
|
"hash": "a" * 16,
|
|
},
|
|
},
|
|
"spans": [
|
|
SpanBuilder().with_span_id("b" * 16).with_hash("b" * 16).build(),
|
|
],
|
|
"span_grouping_config": {
|
|
"id": config_id,
|
|
},
|
|
}
|
|
assert config.execute_strategy(event).results == {
|
|
"a" * 16: "a" * 16,
|
|
"b" * 16: "b" * 16,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"spans,expected",
|
|
[
|
|
([], {}),
|
|
([SpanBuilder().with_span_id("b" * 16).build()], {"b" * 16: ""}),
|
|
# group the ones with the same raw descriptions together
|
|
(
|
|
[
|
|
SpanBuilder().with_span_id("b" * 16).with_description("hi").build(),
|
|
SpanBuilder().with_span_id("c" * 16).with_description("hi").build(),
|
|
SpanBuilder().with_span_id("d" * 16).with_description("bye").build(),
|
|
],
|
|
{"b" * 16: "hi", "c" * 16: "hi", "d" * 16: "bye"},
|
|
),
|
|
# fingerprints take precedence over the description
|
|
(
|
|
[
|
|
SpanBuilder()
|
|
.with_span_id("b" * 16)
|
|
.with_description("hi")
|
|
.with_fingerprint(["a"])
|
|
.build(),
|
|
SpanBuilder().with_span_id("c" * 16).with_description("hi").build(),
|
|
SpanBuilder()
|
|
.with_span_id("d" * 16)
|
|
.with_description("bye")
|
|
.with_fingerprint(["a"])
|
|
.build(),
|
|
],
|
|
{"b" * 16: "a", "c" * 16: "hi", "d" * 16: "a"},
|
|
),
|
|
],
|
|
)
|
|
def test_basic_span_grouping_strategy(spans: list[Span], expected: Mapping[str, list[str]]) -> None:
|
|
event = {
|
|
"transaction": "transaction name",
|
|
"contexts": {
|
|
"trace": {
|
|
"span_id": "a" * 16,
|
|
},
|
|
},
|
|
"spans": spans,
|
|
}
|
|
strategy = SpanGroupingStrategy(name="basic-strategy", strategies=[])
|
|
assert strategy.execute(event) == {
|
|
key: hash_values(values)
|
|
for key, values in {**expected, "a" * 16: ["transaction name"]}.items()
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"spans,expected",
|
|
[
|
|
([], {}),
|
|
(
|
|
[
|
|
SpanBuilder()
|
|
.with_span_id("b" * 16)
|
|
.with_op("http.client")
|
|
.with_description(
|
|
"GET https://sentry.io/api/0/organization/sentry/projects/?all_projects=0"
|
|
)
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("c" * 16)
|
|
.with_op("http.client")
|
|
.with_description(
|
|
"GET https://sentry.io/api/0/organization/sentry/projects/?all_projects=1"
|
|
)
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("d" * 16)
|
|
.with_op("http.client")
|
|
.with_description(
|
|
"POST https://sentry.io/api/0/organization/sentry/projects/?all_projects=0"
|
|
)
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("e" * 16)
|
|
.with_description(
|
|
"GET https://sentry.io/api/0/organization/sentry/projects/?all_projects=0"
|
|
)
|
|
.build(),
|
|
SpanBuilder().with_span_id("f" * 16).with_op("http.client").build(),
|
|
],
|
|
{
|
|
"b" * 16: ["GET", "https", "sentry.io", "/api/0/organization/sentry/projects/"],
|
|
"c" * 16: ["GET", "https", "sentry.io", "/api/0/organization/sentry/projects/"],
|
|
"d" * 16: ["POST", "https", "sentry.io", "/api/0/organization/sentry/projects/"],
|
|
"e"
|
|
* 16: ["GET https://sentry.io/api/0/organization/sentry/projects/?all_projects=0"],
|
|
"f" * 16: [""],
|
|
},
|
|
),
|
|
(
|
|
[
|
|
SpanBuilder()
|
|
.with_span_id("b" * 16)
|
|
.with_op("db")
|
|
.with_description("SELECT count() FROM table WHERE id IN (%s)")
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("c" * 16)
|
|
.with_op("db")
|
|
.with_description("SELECT count() FROM table WHERE id IN (%s, %s)")
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("d" * 16)
|
|
.with_op("db")
|
|
.with_description("SELECT count() FROM table WHERE id = %s")
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("e" * 16)
|
|
.with_description("SELECT count() FROM table WHERE id IN (%s, %s)")
|
|
.build(),
|
|
SpanBuilder().with_span_id("f" * 16).with_op("db").build(),
|
|
],
|
|
{
|
|
"b" * 16: ["SELECT count() FROM table WHERE id IN (%s)"],
|
|
"c" * 16: ["SELECT count() FROM table WHERE id IN (%s)"],
|
|
"d" * 16: ["SELECT count() FROM table WHERE id = %s"],
|
|
"e" * 16: ["SELECT count() FROM table WHERE id IN (%s, %s)"],
|
|
"f" * 16: [""],
|
|
},
|
|
),
|
|
(
|
|
[
|
|
SpanBuilder()
|
|
.with_span_id("b" * 16)
|
|
.with_op("redis")
|
|
.with_description("INCRBY 'key' 1")
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("c" * 16)
|
|
.with_op("redis")
|
|
.with_description("INCRBY 'key' 2")
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("d" * 16)
|
|
.with_op("redis")
|
|
.with_description("EXPIRE 'key' 1")
|
|
.build(),
|
|
],
|
|
{
|
|
"b" * 16: ["INCRBY"],
|
|
"c" * 16: ["INCRBY"],
|
|
"d" * 16: ["EXPIRE"],
|
|
},
|
|
),
|
|
],
|
|
)
|
|
def test_default_2021_08_25_strategy(spans: list[Span], expected: Mapping[str, list[str]]) -> None:
|
|
event = {
|
|
"transaction": "transaction name",
|
|
"contexts": {
|
|
"trace": {
|
|
"span_id": "a" * 16,
|
|
},
|
|
},
|
|
"spans": spans,
|
|
}
|
|
configuration = CONFIGURATIONS["default:2021-08-25"]
|
|
assert configuration.execute_strategy(event).results == {
|
|
key: hash_values(values)
|
|
for key, values in {**expected, "a" * 16: ["transaction name"]}.items()
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"spans,expected",
|
|
[
|
|
([], {}),
|
|
(
|
|
[
|
|
SpanBuilder()
|
|
.with_span_id("b" * 16)
|
|
.with_op("db.sql.query")
|
|
.with_description("SELECT * FROM table WHERE id IN (1, 2, 3)")
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("c" * 16)
|
|
.with_op("db.sql.query")
|
|
.with_description("SELECT * FROM table WHERE id IN (4, 5, 6)")
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("d" * 16)
|
|
.with_op("db.sql.query")
|
|
.with_description("SELECT * FROM table WHERE id IN (7, 8, 9)")
|
|
.build(),
|
|
],
|
|
{
|
|
"b" * 16: ["SELECT * FROM table WHERE id IN (%s)"],
|
|
"c" * 16: ["SELECT * FROM table WHERE id IN (%s)"],
|
|
"d" * 16: ["SELECT * FROM table WHERE id IN (%s)"],
|
|
},
|
|
),
|
|
],
|
|
)
|
|
def test_default_2022_10_04_strategy(spans: list[Span], expected: Mapping[str, list[str]]) -> None:
|
|
event = {
|
|
"transaction": "transaction name",
|
|
"contexts": {
|
|
"trace": {
|
|
"span_id": "a" * 16,
|
|
},
|
|
},
|
|
"spans": spans,
|
|
}
|
|
configuration = CONFIGURATIONS["default:2022-10-04"]
|
|
assert configuration.execute_strategy(event).results == {
|
|
key: hash_values(values)
|
|
for key, values in {**expected, "a" * 16: ["transaction name"]}.items()
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"spans,expected",
|
|
[
|
|
([], {}),
|
|
(
|
|
[
|
|
SpanBuilder()
|
|
.with_span_id("b" * 16)
|
|
.with_op("db.sql.query")
|
|
.with_description("SELECT * FROM table WHERE id = 1")
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("c" * 16)
|
|
.with_op("db.sql.query")
|
|
.with_description("SELECT * FROM table WHERE id IN (4, 5, 6)")
|
|
.build(),
|
|
SpanBuilder()
|
|
.with_span_id("d" * 16)
|
|
.with_op("db.sql.query")
|
|
.with_description("SELECT * FROM table WHERE id = 'string'")
|
|
.build(),
|
|
],
|
|
{
|
|
"b" * 16: ["SELECT * FROM table WHERE id = %s"],
|
|
"c" * 16: ["SELECT * FROM table WHERE id IN (%s)"],
|
|
"d" * 16: ["SELECT * FROM table WHERE id = %s"],
|
|
},
|
|
),
|
|
],
|
|
)
|
|
def test_default_2022_10_27_strategy(spans: list[Span], expected: Mapping[str, list[str]]) -> None:
|
|
event = {
|
|
"transaction": "transaction name",
|
|
"contexts": {
|
|
"trace": {
|
|
"span_id": "a" * 16,
|
|
},
|
|
},
|
|
"spans": spans,
|
|
}
|
|
configuration = CONFIGURATIONS["default:2022-10-27"]
|
|
assert configuration.execute_strategy(event).results == {
|
|
key: hash_values(values)
|
|
for key, values in {**expected, "a" * 16: ["transaction name"]}.items()
|
|
}
|