mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
Co-authored-by: yassin <yassin@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
333 lines
14 KiB
Python
333 lines
14 KiB
Python
"""Tests for litellm/integrations/otel/model/db_endpoint.py
|
|
|
|
Prisma talks to PostgreSQL through a loopback query engine, so a DB span with no
|
|
``server.address`` gets attributed to ``localhost`` by the backend. These cover
|
|
the endpoint derivation that names the real server, for the local engine and for
|
|
remote and read-replica deployments, and pin the rule that no credential is ever
|
|
exported.
|
|
"""
|
|
|
|
import os
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from litellm.integrations.otel.model.db_endpoint import (
|
|
DatabaseEndpoint,
|
|
db_span_attributes,
|
|
parse_database_endpoint,
|
|
postgres_endpoint,
|
|
)
|
|
|
|
LOCAL_DSN = "postgresql://llmproxy:dbpassword9090@localhost:5432/litellm"
|
|
REMOTE_DSN = "postgresql://llmproxy:s3cr3t@litellm-prod.abc123.us-east-1.rds.amazonaws.com:6432/litellm?schema=reporting&sslmode=require"
|
|
REPLICA_DSN = "postgresql://reader:r3ad0nly@litellm-prod-ro.abc123.us-east-1.rds.amazonaws.com/litellm_replica"
|
|
|
|
|
|
def _resolve(service, call_type=None, database_url=None, read_replica_url=None):
|
|
"""Resolve attributes with the two DB env vars set, as the proxy sets them."""
|
|
env = {k: v for k, v in (("DATABASE_URL", database_url), ("DATABASE_URL_READ_REPLICA", read_replica_url)) if v}
|
|
with patch.dict(os.environ, env, clear=False):
|
|
for absent in {"DATABASE_URL", "DATABASE_URL_READ_REPLICA"} - set(env):
|
|
os.environ.pop(absent, None)
|
|
return dict(db_span_attributes(service, call_type))
|
|
|
|
|
|
def test_local_prisma_engine_endpoint_is_the_postgres_server_not_the_engine():
|
|
assert parse_database_endpoint(LOCAL_DSN) == DatabaseEndpoint(
|
|
address="localhost", port=5432, namespace="litellm"
|
|
)
|
|
|
|
|
|
def test_remote_endpoint_keeps_host_port_and_schema_qualified_namespace():
|
|
assert parse_database_endpoint(REMOTE_DSN) == DatabaseEndpoint(
|
|
address="litellm-prod.abc123.us-east-1.rds.amazonaws.com",
|
|
port=6432,
|
|
namespace="litellm|reporting",
|
|
)
|
|
|
|
|
|
def test_read_replica_dsn_parses_to_the_replica_host_and_database():
|
|
assert parse_database_endpoint(REPLICA_DSN) == DatabaseEndpoint(
|
|
address="litellm-prod-ro.abc123.us-east-1.rds.amazonaws.com",
|
|
port=5432,
|
|
namespace="litellm_replica",
|
|
)
|
|
|
|
|
|
def test_default_schema_is_not_spelled_out_in_the_namespace():
|
|
"""``?schema=public`` and no schema at all are the same deployment, so they
|
|
must not split a group-by on db.namespace."""
|
|
assert parse_database_endpoint("postgresql://u:p@db.internal/litellm?schema=public") == parse_database_endpoint(
|
|
"postgresql://u:p@db.internal/litellm"
|
|
)
|
|
|
|
|
|
def test_unix_socket_host_parameter_wins_over_the_netloc():
|
|
"""libpq and the Cloud SQL connector both put the real target in ``host=``
|
|
behind a localhost netloc, which is the attribution this module removes."""
|
|
assert parse_database_endpoint(
|
|
"postgresql://u:p@localhost:5432/litellm?host=/cloudsql/proj:us-east1:inst"
|
|
) == DatabaseEndpoint(address="/cloudsql/proj:us-east1:inst", port=5432, namespace="litellm")
|
|
|
|
|
|
def test_socket_only_dsn_without_a_netloc_host_still_resolves():
|
|
assert parse_database_endpoint("postgresql:///litellm?host=/var/run/postgresql") == DatabaseEndpoint(
|
|
address="/var/run/postgresql", port=5432, namespace="litellm"
|
|
)
|
|
|
|
|
|
def test_percent_encoded_database_name_is_decoded():
|
|
endpoint = parse_database_endpoint("postgresql://u:p@db.internal/litellm%20prod")
|
|
assert endpoint is not None and endpoint.namespace == "litellm prod"
|
|
|
|
|
|
MISPARSED_AUTHORITY_DSNS = (
|
|
("postgresql://litellm:/kJ8xQz+9wT@db.internal:5432/litellm", "kJ8xQz+9wT"),
|
|
("postgresql://litellm:12345/aBcD@db.internal:5432/litellm", "aBcD"),
|
|
# '#' sends the tail to the fragment and '?' to the query, so the path is
|
|
# empty and only the stranded userinfo '@' reveals the mis-split.
|
|
("postgresql://litellm:12345#aBcD@db.internal/litellm", "aBcD"),
|
|
("postgresql://litellm:12345?aBcD@db.internal/litellm", "aBcD"),
|
|
# A '?'-stranded tail that happens to parse as parameters, including one
|
|
# that hijacks the host= parameter into server.address.
|
|
("postgresql://litellm:12345?a=aBcD@db.internal/litellm", "aBcD"),
|
|
("postgresql://litellm:12345?host=aBcD@db.internal/litellm", "aBcD"),
|
|
# Both '/' and '?key=value' together: the slash leaves a clean path holding
|
|
# the password remainder and the query still parses, so only the stranded
|
|
# at-sign gives it away.
|
|
("postgresql://litellm:12345/aBcD?x=1@db.internal/litellm", "aBcD"),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(("dsn", "secret"), MISPARSED_AUTHORITY_DSNS)
|
|
def test_unencoded_slash_in_password_never_yields_an_endpoint(dsn, secret):
|
|
"""An unencoded '/' truncates the authority, so urlparse reports the username
|
|
as the host and the password tail as the database. Postgres drivers reject
|
|
such a DSN outright, so the only safe reading is no endpoint at all."""
|
|
assert parse_database_endpoint(dsn) is None
|
|
|
|
|
|
@pytest.mark.parametrize(("dsn", "secret"), MISPARSED_AUTHORITY_DSNS)
|
|
def test_unencoded_slash_in_password_never_reaches_a_span(dsn, secret):
|
|
attrs = _resolve("postgres", "get_data", database_url=dsn)
|
|
exported = " ".join(str(value) for value in attrs.values())
|
|
assert secret not in exported
|
|
assert "db.namespace" not in attrs
|
|
assert "server.address" not in attrs
|
|
|
|
|
|
def test_extra_path_segment_yields_no_endpoint():
|
|
"""A database name cannot hold an unencoded '/', so a second path segment
|
|
means the authority was mis-split even when no '@' survived into the path."""
|
|
assert parse_database_endpoint("postgresql://db.internal:5432/litellm/extra") is None
|
|
|
|
|
|
@pytest.mark.parametrize("dsn", [d for d, _ in MISPARSED_AUTHORITY_DSNS])
|
|
def test_a_mis_split_authority_never_exports_the_database_username(dsn):
|
|
"""The username lands in ``parsed.hostname`` when the authority truncates, so
|
|
a span would name the DB user as the server."""
|
|
attrs = _resolve("postgres", "get_data", database_url=dsn)
|
|
assert "server.address" not in attrs
|
|
assert "litellm" not in " ".join(str(v) for v in attrs.values())
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"dsn",
|
|
[
|
|
"postgresql://db.internal:5432/litellm?application_name=svc@prod",
|
|
"postgresql://db.internal:5432/litellm?user=admin@company.com",
|
|
],
|
|
)
|
|
def test_an_unencoded_at_sign_in_a_query_forfeits_the_endpoint(dsn):
|
|
"""This shape is byte-for-byte indistinguishable from a mis-split password,
|
|
so it resolves to no endpoint rather than risking a credential fragment.
|
|
Percent-encoding the at-sign restores the attributes."""
|
|
assert parse_database_endpoint(dsn) is None
|
|
assert parse_database_endpoint(dsn.replace("@", "%40")) is not None
|
|
|
|
|
|
def test_host_and_port_query_parameters_are_honoured_together():
|
|
assert parse_database_endpoint("postgresql://ignored/litellm?host=real.internal&port=6543") == DatabaseEndpoint(
|
|
address="real.internal", port=6543, namespace="litellm"
|
|
)
|
|
|
|
|
|
def test_percent_encoded_password_still_resolves_the_endpoint():
|
|
"""The encoded spelling is the one a driver accepts, so it must keep working."""
|
|
assert parse_database_endpoint("postgresql://litellm:pa%2Fssw0rd@db.internal:5432/litellm") == DatabaseEndpoint(
|
|
address="db.internal", port=5432, namespace="litellm"
|
|
)
|
|
|
|
|
|
def test_hostless_socket_dsn_still_names_the_database():
|
|
"""``postgresql:///litellm`` is a valid local-socket DSN that Prisma accepts,
|
|
so the database is knowable even though no server address is."""
|
|
assert parse_database_endpoint("postgresql:///litellm") == DatabaseEndpoint(
|
|
address=None, port=None, namespace="litellm"
|
|
)
|
|
|
|
|
|
def test_hostless_socket_dsn_emits_namespace_without_a_server():
|
|
attrs = _resolve("postgres", "get_data", database_url="postgresql:///litellm")
|
|
assert attrs["db.namespace"] == "litellm"
|
|
assert "server.address" not in attrs
|
|
assert "server.port" not in attrs
|
|
|
|
|
|
def test_dsn_with_neither_host_nor_database_yields_no_endpoint():
|
|
assert parse_database_endpoint("postgresql://") is None
|
|
|
|
|
|
def test_prisma_default_schema_is_left_implicit():
|
|
endpoint = parse_database_endpoint("postgresql://u:p@db.internal/litellm?schema=public")
|
|
assert endpoint is not None and endpoint.namespace == "litellm"
|
|
|
|
|
|
@pytest.mark.parametrize("spelling", ["PUBLIC", "Public", "reporting"])
|
|
def test_a_non_default_schema_stays_in_the_namespace(spelling):
|
|
"""Prisma quotes the schema name, so ``?schema=PUBLIC`` provisions a second
|
|
schema alongside ``public`` with its own tables. Case-folding them into one
|
|
namespace would report two different schemas as the same database."""
|
|
endpoint = parse_database_endpoint(f"postgresql://u:p@db.internal/litellm?schema={spelling}")
|
|
assert endpoint is not None and endpoint.namespace == f"litellm|{spelling}"
|
|
|
|
|
|
def test_postgres_scheme_alias_is_accepted():
|
|
assert parse_database_endpoint("postgres://u:p@db.internal/litellm") == DatabaseEndpoint(
|
|
address="db.internal", port=5432, namespace="litellm"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"dsn",
|
|
[
|
|
None,
|
|
"",
|
|
"mysql://u:p@db.internal:3306/litellm",
|
|
"postgresql://u:p@db.internal:not-a-port/litellm",
|
|
"not a url at all",
|
|
],
|
|
)
|
|
def test_unusable_dsn_degrades_to_no_endpoint(dsn):
|
|
assert parse_database_endpoint(dsn) is None
|
|
|
|
|
|
def test_database_without_name_or_schema_has_no_namespace():
|
|
assert parse_database_endpoint("postgresql://u:p@db.internal:5432/") == DatabaseEndpoint(
|
|
address="db.internal", port=5432, namespace=None
|
|
)
|
|
|
|
|
|
def test_postgres_service_span_carries_system_operation_and_endpoint():
|
|
assert _resolve("postgres", "get_data", database_url=REMOTE_DSN) == {
|
|
"db.system.name": "postgresql",
|
|
"db.system": "postgresql",
|
|
"db.operation.name": "get_data",
|
|
"server.address": "litellm-prod.abc123.us-east-1.rds.amazonaws.com",
|
|
"server.port": 6432,
|
|
"db.namespace": "litellm|reporting",
|
|
}
|
|
|
|
|
|
def test_legacy_db_system_is_dual_emitted_for_datadog():
|
|
"""Datadog's OTLP intake infers the database span type from ``db.system``,
|
|
not from the semconv-current ``db.system.name``."""
|
|
assert _resolve("postgres", "get_data", database_url=LOCAL_DSN)["db.system"] == "postgresql"
|
|
assert _resolve("redis", "set")["db.system"] == "redis"
|
|
|
|
|
|
def test_batch_write_service_is_also_attributed_to_postgres():
|
|
attrs = _resolve("batch_write_to_db", "_PROXY_track_cost_callback", database_url=REMOTE_DSN)
|
|
assert attrs["db.system.name"] == "postgresql"
|
|
assert attrs["server.address"] == "litellm-prod.abc123.us-east-1.rds.amazonaws.com"
|
|
|
|
|
|
def test_resolved_prisma_operation_puts_verb_table_and_summary_on_the_db_keys():
|
|
from litellm.integrations.otel.model.spans import PostgresOperation
|
|
|
|
with patch.dict(os.environ, {"DATABASE_URL": LOCAL_DSN}, clear=False):
|
|
os.environ.pop("DATABASE_URL_READ_REPLICA", None)
|
|
attrs = dict(db_span_attributes("postgres", "update_data", PostgresOperation("update", "LiteLLM_TeamTable")))
|
|
bare = dict(db_span_attributes("postgres", "update_data", PostgresOperation("update", None)))
|
|
assert attrs == {
|
|
"db.system.name": "postgresql",
|
|
"db.system": "postgresql",
|
|
"db.operation.name": "update",
|
|
"db.collection.name": "LiteLLM_TeamTable",
|
|
"db.query.summary": "UPDATE LiteLLM_TeamTable",
|
|
"server.address": "localhost",
|
|
"server.port": 5432,
|
|
"db.namespace": "litellm",
|
|
}
|
|
assert bare["db.operation.name"] == "update"
|
|
assert {"db.collection.name", "db.query.summary"}.isdisjoint(bare)
|
|
|
|
|
|
def test_redis_service_never_borrows_the_postgres_endpoint():
|
|
assert _resolve("redis", "set", database_url=REMOTE_DSN) == {
|
|
"db.system.name": "redis",
|
|
"db.system": "redis",
|
|
"db.operation.name": "set",
|
|
}
|
|
|
|
|
|
def test_non_datastore_service_gets_no_db_attributes():
|
|
assert _resolve("reset_budget_job", "reset_budget", database_url=REMOTE_DSN) == {}
|
|
|
|
|
|
def test_configured_read_replica_suppresses_the_endpoint_rather_than_naming_the_primary():
|
|
"""Reads are routed to the replica per Prisma call, underneath the span, so
|
|
naming the writer would pin replica latency onto the primary."""
|
|
attrs = _resolve("postgres", "get_data", database_url=REMOTE_DSN, read_replica_url=REPLICA_DSN)
|
|
assert attrs == {
|
|
"db.system.name": "postgresql",
|
|
"db.system": "postgresql",
|
|
"db.operation.name": "get_data",
|
|
}
|
|
|
|
|
|
def test_endpoint_attributes_are_omitted_when_database_url_is_unset():
|
|
assert _resolve("postgres", "get_data") == {
|
|
"db.system.name": "postgresql",
|
|
"db.system": "postgresql",
|
|
"db.operation.name": "get_data",
|
|
}
|
|
|
|
|
|
def test_blank_call_type_does_not_emit_an_empty_operation_attribute():
|
|
assert "db.operation.name" not in _resolve("postgres", "")
|
|
assert "db.operation.name" not in _resolve("postgres", None)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("dsn", "secrets"),
|
|
[
|
|
(LOCAL_DSN, ("dbpassword9090", "llmproxy")),
|
|
(REMOTE_DSN, ("s3cr3t", "llmproxy", "sslmode")),
|
|
(REPLICA_DSN, ("r3ad0nly", "reader")),
|
|
],
|
|
)
|
|
def test_no_credential_reaches_any_exported_attribute(dsn, secrets):
|
|
attrs = _resolve("postgres", "get_data", database_url=dsn)
|
|
assert attrs["server.address"]
|
|
exported = " ".join(str(value) for value in attrs.values())
|
|
for secret in secrets:
|
|
assert secret not in exported
|
|
|
|
|
|
def test_a_runtime_endpoint_change_is_reflected_on_the_next_span():
|
|
"""The RDS IAM refresh, the reconnect path and the DB-backed
|
|
environment_variables overlay can all rewrite DATABASE_URL after startup, so
|
|
a value cached for the process lifetime would report a server the process no
|
|
longer talks to."""
|
|
first = _resolve("postgres", "get_data", database_url=LOCAL_DSN)
|
|
assert first["server.address"] == "localhost"
|
|
moved = _resolve("postgres", "get_data", database_url=REMOTE_DSN)
|
|
assert moved["server.address"] == "litellm-prod.abc123.us-east-1.rds.amazonaws.com"
|
|
|
|
|
|
def test_a_replica_configured_after_the_first_span_suppresses_the_endpoint():
|
|
assert _resolve("postgres", "get_data", database_url=REMOTE_DSN)["server.address"]
|
|
later = _resolve("postgres", "get_data", database_url=REMOTE_DSN, read_replica_url=REPLICA_DSN)
|
|
assert "server.address" not in later
|