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
19 changes: 19 additions & 0 deletions datadog_sync/model/metrics_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ async def create_resource(self, _id: str, resource: Dict) -> Tuple[str, Dict]:
async def update_resource(self, _id: str, resource: Dict) -> Tuple[str, Dict]:
destination_client = self.config.destination_client

# Distribution metrics cannot be written via PUT /api/v1/metrics/{name}
# — the destination rejects type=distribution with a 400 at the DB
# layer. Skip early so the run does not burn retries on a request
# that will never succeed. Skip fires before the destination probe
# below to avoid a wasted GET.
#
# Case-tolerant match: the destination canonicalizes to lowercase but
# normalize defensively so an upstream drift in the source-side casing
# cannot silently re-enable the 400 loop.
metric_type = (resource.get("type") or "").strip().lower()
if metric_type == "distribution":
log.debug(f"[metrics_metadata - {_id}] skipping: distribution type not writable via public PUT")
raise SkipResource(
_id,
self.resource_type,
"distribution type is rejected by the destination metrics_metadata endpoint; "
"skipping public PUT",
)

# metrics_metadata can only attach to a metric that already exists on
# destination. Legacy dogweb returns 400 {"errors":["error updating metric
# metadata"]} when the metric is missing; that failure adds ~25-30min of
Expand Down
46 changes: 45 additions & 1 deletion tests/unit/test_metrics_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# This product includes software developed at Datadog (https://www.datadoghq.com/).
# Copyright 2019 Datadog, Inc.

"""Unit tests for metrics_metadata destination-existence filter (HAMR-392 Jul8-T20)."""
"""Unit tests for metrics_metadata update_resource filters."""

import asyncio
from types import SimpleNamespace
Expand Down Expand Up @@ -82,6 +82,50 @@ def test_update_resource_get_403_propagates(metrics_metadata):
client.put.assert_not_awaited()


@pytest.mark.parametrize(
"type_value",
["distribution", "Distribution", "DISTRIBUTION", "distribution "],
ids=["lowercase", "capitalized", "uppercase", "trailing_space"],
)
def test_update_resource_distribution_type_raises_skip(metrics_metadata, type_value):
"""Distribution-typed metrics skip early; no destination HTTP call is made.
Case + whitespace variants must all skip so an upstream drift in the source
canonicalization cannot silently re-enable the 400 loop.
"""
client = metrics_metadata.config.destination_client
client.get = AsyncMock()
client.put = AsyncMock()

with pytest.raises(SkipResource) as exc_info:
asyncio.run(metrics_metadata.update_resource("some.dist.metric", {"type": type_value, "description": "x"}))

assert "some.dist.metric" in str(exc_info.value)
assert "distribution" in str(exc_info.value)
client.get.assert_not_awaited()
client.put.assert_not_awaited()


@pytest.mark.parametrize("type_value", ["count", "rate", "gauge", "histogram", None])
def test_update_resource_non_distribution_type_probes_destination(metrics_metadata, type_value):
"""Regression: non-distribution types (and missing type) still probe destination via GET before PUT.
The v1 probe endpoint returns a flat metric metadata dict; the return value is unused (only
non-404 status matters), so any non-None dict works as a mock.
"""
client = metrics_metadata.config.destination_client
client.get = AsyncMock(return_value={"type": type_value or "count", "description": "existing"})
client.put = AsyncMock(return_value={"description": "updated"})

resource = {"description": "updated"}
if type_value is not None:
resource["type"] = type_value

_id, _ = asyncio.run(metrics_metadata.update_resource("some.metric", resource))

assert _id == "some.metric"
client.get.assert_awaited_once()
client.put.assert_awaited_once()


def test_create_resource_delegates_to_update(metrics_metadata):
"""create_resource remains a thin wrapper around update_resource (behaviour unchanged)."""
client = metrics_metadata.config.destination_client
Expand Down
Loading