Merge pull request #22103 from Harshit28j/litellm_feat_datadog_metrics

feat: ability to trace metrics datadog
This commit is contained in:
Harshit Jain
2026-02-28 17:25:23 +05:30
committed by GitHub
10 changed files with 704 additions and 7 deletions
@@ -7,6 +7,7 @@ import TabItem from '@theme/TabItem';
LiteLLM Supports logging to the following Datdog Integrations:
- `datadog` [Datadog Logs](https://docs.datadoghq.com/logs/)
- `datadog_llm_observability` [Datadog LLM Observability](https://www.datadoghq.com/product/llm-observability/)
- `datadog_metrics` [Datadog Custom Metrics](#datadog-custom-metrics)
- `datadog_cost_management` [Datadog Cloud Cost Management](#datadog-cloud-cost-management)
- `ddtrace-run` [Datadog Tracing](#datadog-tracing)
@@ -168,6 +169,65 @@ On the Datadog LLM Observability page, you should see that both input messages a
<Image img={require('../../img/dd_llm_obs.png')} />
## Datadog Custom Metrics
| Feature | Details |
|---------|---------|
| **What is logged** | Latency metrics, request counts by status code |
| **Events** | Success + Failure |
| **Product Link** | [Datadog Metrics](https://docs.datadoghq.com/metrics/) |
Publishes the following metrics to Datadog via the `/api/v2/series` endpoint:
| Metric | Type | Description |
|--------|------|-------------|
| `litellm.request.total_latency` | Gauge | End-to-end request latency (seconds) |
| `litellm.llm_api.latency` | Gauge | Time spent waiting for the LLM provider response (seconds) |
| `litellm.llm_api.request_count` | Count | Request count, tagged with status code |
Using `total_latency` and `llm_api.latency`, you can derive **internal latency** = `total_latency - llm_api.latency`.
All metrics include the following tags: `env`, `service`, `version`, `HOSTNAME`, `POD_NAME`, `provider`, `model_name`, `model_group`, `team`, `status_code`.
**Step 1**: Create a `config.yaml` file
```yaml
model_list:
- model_name: gpt-3.5-turbo
litellm_params:
model: gpt-3.5-turbo
litellm_settings:
success_callback: ["datadog_metrics"]
failure_callback: ["datadog_metrics"]
```
**Step 2**: Set required env variables
```shell
DD_API_KEY="your-api-key"
DD_SITE="us5.datadoghq.com" # your datadog site
```
**Step 3**: Start the proxy and make a test request
```shell
litellm --config config.yaml
```
```shell
curl --location 'http://0.0.0.0:4000/chat/completions' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer sk-1234' \
--data '{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "hello"}]
}'
```
**Step 4**: View metrics in Datadog Metrics Explorer
Navigate to **Metrics > Explorer** in Datadog and search for `litellm.request.total_latency`, `litellm.llm_api.latency`, or `litellm.llm_api.request_count`.
## Datadog Cloud Cost Management
| Feature | Details |
+1
View File
@@ -105,6 +105,7 @@ _custom_logger_compatible_callbacks_literal = Literal[
"prometheus",
"otel",
"datadog",
"datadog_metrics",
"datadog_llm_observability",
"galileo",
"braintrust",
+22 -1
View File
@@ -83,6 +83,27 @@
},
"description": "Datadog Logging Integration"
},
{
"id": "datadog_metrics",
"displayName": "Datadog Metrics",
"logo": "datadog.png",
"supports_key_team_logging": false,
"dynamic_params": {
"dd_api_key": {
"type": "password",
"ui_name": "API Key",
"description": "Datadog API key for authentication",
"required": true
},
"dd_site": {
"type": "text",
"ui_name": "Site",
"description": "Datadog site URL (e.g., us5.datadoghq.com)",
"required": true
}
},
"description": "Datadog Custom Metrics Integration"
},
{
"id": "datadog_cost_management",
"displayName": "Datadog Cost Management",
@@ -434,4 +455,4 @@
},
"description": "SQS Queue (AWS) Logging Integration"
}
]
]
@@ -0,0 +1,286 @@
import asyncio
import gzip
import os
import time
from datetime import datetime
from typing import List, Optional, Union
from litellm._logging import verbose_logger
from litellm.integrations.custom_batch_logger import CustomBatchLogger
from litellm.integrations.datadog.datadog_handler import (
get_datadog_env,
get_datadog_hostname,
get_datadog_pod_name,
get_datadog_service,
)
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client,
httpxSpecialProvider,
)
from litellm.types.integrations.base_health_check import IntegrationHealthCheckStatus
from litellm.types.integrations.datadog_metrics import (
DatadogMetricPoint,
DatadogMetricSeries,
DatadogMetricsPayload,
)
from litellm.types.utils import StandardLoggingPayload
class DatadogMetricsLogger(CustomBatchLogger):
def __init__(self, start_periodic_flush: bool = True, **kwargs):
self.dd_api_key = os.getenv("DD_API_KEY")
self.dd_app_key = os.getenv("DD_APP_KEY")
self.dd_site = os.getenv("DD_SITE", "datadoghq.com")
if not self.dd_api_key:
verbose_logger.warning(
"Datadog Metrics: DD_API_KEY is required. Integration will not work."
)
self.upload_url = f"https://api.{self.dd_site}/api/v2/series"
self.async_client = get_async_httpx_client(
llm_provider=httpxSpecialProvider.LoggingCallback
)
# Initialize lock
self.flush_lock = asyncio.Lock()
# Only set flush_lock if not already provided by caller
if "flush_lock" not in kwargs:
kwargs["flush_lock"] = self.flush_lock
# Send metrics more quickly to datadog (every 5 seconds)
if "flush_interval" not in kwargs:
kwargs["flush_interval"] = 5
super().__init__(**kwargs)
# Start periodic flush task only if instructed
if start_periodic_flush:
asyncio.create_task(self.periodic_flush())
def _extract_tags(
self,
log: StandardLoggingPayload,
status_code: Optional[Union[str, int]] = None,
) -> List[str]:
"""
Builds the list of tags for a Datadog metric point
"""
# Base tags
tags = [
f"env:{get_datadog_env()}",
f"service:{get_datadog_service()}",
f"version:{os.getenv('DD_VERSION', 'unknown')}",
f"HOSTNAME:{get_datadog_hostname()}",
f"POD_NAME:{get_datadog_pod_name()}",
]
# Add metric-specific tags
if provider := log.get("custom_llm_provider"):
tags.append(f"provider:{provider}")
if model := log.get("model"):
tags.append(f"model_name:{model}")
if model_group := log.get("model_group"):
tags.append(f"model_group:{model_group}")
if status_code is not None:
tags.append(f"status_code:{status_code}")
# Extract team tag
metadata = log.get("metadata", {}) or {}
team_tag = (
metadata.get("user_api_key_team_alias")
or metadata.get("team_alias") # type: ignore
or metadata.get("user_api_key_team_id")
or metadata.get("team_id") # type: ignore
)
if team_tag:
tags.append(f"team:{team_tag}")
return tags
def _add_metrics_from_log(
self,
log: StandardLoggingPayload,
kwargs: dict,
status_code: Union[str, int] = "200",
):
"""
Extracts latencies and appends Datadog metric series to the queue
"""
tags = self._extract_tags(log, status_code=status_code)
# We record metrics with the end_time as the timestamp for the point
end_time_dt = kwargs.get("end_time") or datetime.now()
timestamp = int(end_time_dt.timestamp())
# 1. Total Request Latency Metric (End to End)
start_time_dt = kwargs.get("start_time")
if start_time_dt and end_time_dt:
total_duration = (end_time_dt - start_time_dt).total_seconds()
series_total_latency: DatadogMetricSeries = {
"metric": "litellm.request.total_latency",
"type": 3, # gauge
"points": [{"timestamp": timestamp, "value": total_duration}],
"tags": tags,
}
self.log_queue.append(series_total_latency)
# 2. LLM API Latency Metric (Provider alone)
api_call_start_time = kwargs.get("api_call_start_time")
if api_call_start_time and end_time_dt:
llm_api_duration = (end_time_dt - api_call_start_time).total_seconds()
series_llm_latency: DatadogMetricSeries = {
"metric": "litellm.llm_api.latency",
"type": 3, # gauge
"points": [{"timestamp": timestamp, "value": llm_api_duration}],
"tags": tags,
}
self.log_queue.append(series_llm_latency)
# 3. Request Count / Status Code
series_count: DatadogMetricSeries = {
"metric": "litellm.llm_api.request_count",
"type": 1, # count
"points": [{"timestamp": timestamp, "value": 1.0}],
"tags": tags,
"interval": self.flush_interval,
}
self.log_queue.append(series_count)
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
try:
standard_logging_object: Optional[StandardLoggingPayload] = kwargs.get(
"standard_logging_object", None
)
if standard_logging_object is None:
return
self._add_metrics_from_log(
log=standard_logging_object, kwargs=kwargs, status_code="200"
)
if len(self.log_queue) >= self.batch_size:
await self.flush_queue()
except Exception as e:
verbose_logger.exception(
f"Datadog Metrics: Error in async_log_success_event: {str(e)}"
)
async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time):
try:
standard_logging_object: Optional[StandardLoggingPayload] = kwargs.get(
"standard_logging_object", None
)
if standard_logging_object is None:
return
# Extract status code from error information
status_code = "500" # default
error_information = (
standard_logging_object.get("error_information", {}) or {}
)
error_code = error_information.get("error_code") # type: ignore
if error_code is not None:
status_code = str(error_code)
self._add_metrics_from_log(
log=standard_logging_object, kwargs=kwargs, status_code=status_code
)
if len(self.log_queue) >= self.batch_size:
await self.flush_queue()
except Exception as e:
verbose_logger.exception(
f"Datadog Metrics: Error in async_log_failure_event: {str(e)}"
)
async def async_send_batch(self):
if not self.log_queue:
return
batch = self.log_queue.copy()
payload_data: DatadogMetricsPayload = {"series": batch}
try:
await self._upload_to_datadog(payload_data)
except Exception as e:
verbose_logger.exception(
f"Datadog Metrics: Error in async_send_batch: {str(e)}"
)
raise
async def _upload_to_datadog(self, payload: DatadogMetricsPayload):
if not self.dd_api_key:
return
headers = {
"Content-Type": "application/json",
"DD-API-KEY": self.dd_api_key,
}
if self.dd_app_key:
headers["DD-APPLICATION-KEY"] = self.dd_app_key
json_data = safe_dumps(payload)
compressed_data = gzip.compress(json_data.encode("utf-8"))
headers["Content-Encoding"] = "gzip"
response = await self.async_client.post(
self.upload_url, content=compressed_data, headers=headers # type: ignore
)
response.raise_for_status()
verbose_logger.debug(
f"Datadog Metrics: Uploaded {len(payload['series'])} metric points. Status: {response.status_code}"
)
async def async_health_check(self) -> IntegrationHealthCheckStatus:
"""
Check if the service is healthy
"""
try:
# Send a test metric point to Datadog
test_metric_point: DatadogMetricPoint = {
"timestamp": int(time.time()),
"value": 1.0,
}
test_metric_series: DatadogMetricSeries = {
"metric": "litellm.health_check",
"type": 3, # Gauge
"points": [test_metric_point],
"tags": ["env:health_check"],
}
payload_data: DatadogMetricsPayload = {"series": [test_metric_series]}
await self._upload_to_datadog(payload_data)
return IntegrationHealthCheckStatus(
status="healthy",
error_message=None,
)
except Exception as e:
return IntegrationHealthCheckStatus(
status="unhealthy",
error_message=str(e),
)
async def get_request_response_payload(
self,
request_id: str,
start_time_utc: Optional[datetime],
end_time_utc: Optional[datetime],
) -> Optional[dict]:
pass
@@ -20,6 +20,7 @@ from litellm.integrations.braintrust_logging import BraintrustLogger
from litellm.integrations.cloudzero.cloudzero import CloudZeroLogger
from litellm.integrations.datadog.datadog import DataDogLogger
from litellm.integrations.datadog.datadog_llm_obs import DataDogLLMObsLogger
from litellm.integrations.datadog.datadog_metrics import DatadogMetricsLogger
from litellm.integrations.deepeval import DeepEvalLogger
from litellm.integrations.dotprompt import DotpromptManager
from litellm.integrations.focus.focus_logger import FocusLogger
@@ -66,6 +67,7 @@ class CustomLoggerRegistry:
"prometheus": PrometheusLogger,
"datadog": DataDogLogger,
"datadog_llm_observability": DataDogLLMObsLogger,
"datadog_metrics": DatadogMetricsLogger,
"gcs_bucket": GCSBucketLogger,
"opik": OpikLogger,
"argilla": ArgillaLogger,
@@ -133,6 +133,7 @@ from ..integrations.azure_sentinel.azure_sentinel import AzureSentinelLogger
from ..integrations.azure_storage.azure_storage import AzureBlobStorageLogger
from ..integrations.custom_prompt_management import CustomPromptManagement
from ..integrations.datadog.datadog import DataDogLogger
from ..integrations.datadog.datadog_metrics import DatadogMetricsLogger
from ..integrations.datadog.datadog_llm_obs import DataDogLLMObsLogger
from ..integrations.dotprompt import DotpromptManager
from ..integrations.dynamodb import DyanmoDBLogger
@@ -3661,6 +3662,14 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915
_datadog_logger = DataDogLogger()
_in_memory_loggers.append(_datadog_logger)
return _datadog_logger # type: ignore
elif logging_integration == "datadog_metrics":
for callback in _in_memory_loggers:
if isinstance(callback, DatadogMetricsLogger):
return callback # type: ignore
_datadog_metrics_logger = DatadogMetricsLogger()
_in_memory_loggers.append(_datadog_metrics_logger)
return _datadog_metrics_logger # type: ignore
elif logging_integration == "datadog_llm_observability":
_datadog_llm_obs_logger = DataDogLLMObsLogger()
_in_memory_loggers.append(_datadog_llm_obs_logger)
@@ -4268,6 +4277,10 @@ def get_custom_logger_compatible_class( # noqa: PLR0915
for callback in _in_memory_loggers:
if isinstance(callback, DataDogLogger):
return callback
elif logging_integration == "datadog_metrics":
for callback in _in_memory_loggers:
if isinstance(callback, DatadogMetricsLogger):
return callback
elif logging_integration == "datadog_llm_observability":
for callback in _in_memory_loggers:
if isinstance(callback, DataDogLLMObsLogger):
+2 -6
View File
@@ -390,9 +390,7 @@ def get_logging_caching_headers(request_data: Dict) -> Optional[Dict]:
)
if "applied_policies" in _metadata:
headers["x-litellm-applied-policies"] = ",".join(
_metadata["applied_policies"]
)
headers["x-litellm-applied-policies"] = ",".join(_metadata["applied_policies"])
if "policy_sources" in _metadata:
sources = _metadata["policy_sources"]
@@ -449,9 +447,7 @@ def add_policy_to_applied_policies_header(
request_data["metadata"] = _metadata
def add_policy_sources_to_metadata(
request_data: Dict, policy_sources: Dict[str, str]
):
def add_policy_sources_to_metadata(request_data: Dict, policy_sources: Dict[str, str]):
"""
Store policy match reasons in metadata for x-litellm-policy-sources header.
@@ -230,6 +230,7 @@ async def health_services_endpoint( # noqa: PLR0915
"custom_callback_api",
"langsmith",
"datadog",
"datadog_metrics",
"datadog_llm_observability",
"generic_api",
"arize",
@@ -284,6 +285,30 @@ async def health_services_endpoint( # noqa: PLR0915
else "Datadog is healthy"
),
}
elif service == "datadog_metrics":
from litellm.integrations.datadog.datadog_metrics import (
DatadogMetricsLogger,
)
from litellm.litellm_core_utils.litellm_logging import (
get_custom_logger_compatible_class,
)
datadog_metrics_logger = get_custom_logger_compatible_class(
"datadog_metrics"
)
if datadog_metrics_logger is None:
datadog_metrics_logger = DatadogMetricsLogger(
start_periodic_flush=False
)
response = await datadog_metrics_logger.async_health_check()
return {
"status": response["status"],
"message": (
response["error_message"]
if response["status"] == "unhealthy"
else "Datadog Metrics is healthy"
),
}
elif service == "arize":
from litellm.integrations.arize.arize import ArizeLogger
@@ -0,0 +1,20 @@
from typing import List, Optional
from typing_extensions import TypedDict
class DatadogMetricPoint(TypedDict):
timestamp: int # Unix epoch seconds
value: float # The metric value
class DatadogMetricSeries(TypedDict, total=False):
metric: str
type: int # 0=unspecified, 1=count, 2=rate, 3=gauge
points: List[DatadogMetricPoint]
tags: List[str]
interval: Optional[int] # Required for count (type=1) and rate (type=2) metrics
class DatadogMetricsPayload(TypedDict):
series: List[DatadogMetricSeries]
@@ -0,0 +1,273 @@
import os
import time
from datetime import datetime, timedelta
from unittest.mock import AsyncMock
import pytest
from httpx import Request, Response
from litellm.integrations.datadog.datadog_metrics import DatadogMetricsLogger
from litellm.types.utils import StandardLoggingPayload
@pytest.fixture
def clean_env():
"""Set test env vars and restore originals after test."""
keys = ["DD_API_KEY", "DD_APP_KEY", "DD_SITE", "DD_ENV", "DD_SERVICE", "DD_VERSION"]
originals = {k: os.environ.get(k) for k in keys}
os.environ["DD_API_KEY"] = "test_api_key"
os.environ["DD_APP_KEY"] = "test_app_key"
os.environ["DD_SITE"] = "test.datadoghq.com"
os.environ["DD_ENV"] = "test-env"
os.environ["DD_SERVICE"] = "test-service"
os.environ["DD_VERSION"] = "1.0.0"
yield
for k, v in originals.items():
if v is not None:
os.environ[k] = v
elif k in os.environ:
del os.environ[k]
@pytest.mark.asyncio
async def test_init(clean_env):
"""Test initialization sets up clients and url correctly."""
logger = DatadogMetricsLogger(start_periodic_flush=False)
assert logger.upload_url == "https://api.test.datadoghq.com/api/v2/series"
@pytest.mark.asyncio
async def test_extract_tags(clean_env):
"""Test tag extraction from a StandardLoggingPayload."""
logger = DatadogMetricsLogger(start_periodic_flush=False)
payload = StandardLoggingPayload(
custom_llm_provider="openai",
model="gpt-4o",
model_group="gpt-4",
metadata={"user_api_key_team_alias": "test-team"},
)
tags = logger._extract_tags(log=payload, status_code="200")
assert "env:test-env" in tags
assert "service:test-service" in tags
assert "version:1.0.0" in tags
assert "provider:openai" in tags
assert "model_name:gpt-4o" in tags
assert "model_group:gpt-4" in tags
assert "status_code:200" in tags
assert "team:test-team" in tags
@pytest.mark.asyncio
async def test_extract_tags_no_team(clean_env):
"""Test tag extraction when no team info is present."""
logger = DatadogMetricsLogger(start_periodic_flush=False)
payload = StandardLoggingPayload(
custom_llm_provider="anthropic",
model="claude-3-sonnet",
)
tags = logger._extract_tags(log=payload, status_code="500")
assert "provider:anthropic" in tags
assert "model_name:claude-3-sonnet" in tags
assert "status_code:500" in tags
assert not any(tag.startswith("team:") for tag in tags)
@pytest.mark.asyncio
async def test_add_metrics_from_log(clean_env):
"""Test that _add_metrics_from_log appends the correct metric series to the queue."""
logger = DatadogMetricsLogger(batch_size=100, start_periodic_flush=False)
now = datetime.now()
start_time = now - timedelta(seconds=2)
api_call_start_time = now - timedelta(seconds=1)
payload = StandardLoggingPayload(
custom_llm_provider="openai",
model="gpt-4o",
)
kwargs = {
"start_time": start_time,
"api_call_start_time": api_call_start_time,
"end_time": now,
}
logger._add_metrics_from_log(log=payload, kwargs=kwargs, status_code="200")
# Should have 3 series: total_latency, llm_api_latency, request_count
assert len(logger.log_queue) == 3
metrics = {s["metric"]: s for s in logger.log_queue}
# Total latency ~2s
total = metrics["litellm.request.total_latency"]
assert total["type"] == 3 # gauge
assert abs(total["points"][0]["value"] - 2.0) < 0.1
# LLM API latency ~1s
llm = metrics["litellm.llm_api.latency"]
assert llm["type"] == 3 # gauge
assert abs(llm["points"][0]["value"] - 1.0) < 0.1
# Request count
count = metrics["litellm.llm_api.request_count"]
assert count["type"] == 1 # count
assert count["points"][0]["value"] == 1.0
assert "status_code:200" in count["tags"]
@pytest.mark.asyncio
async def test_async_log_success_event(clean_env):
"""Test that success events are added to the queue."""
logger = DatadogMetricsLogger(batch_size=100, start_periodic_flush=False)
now = datetime.now()
start_time = now - timedelta(seconds=1)
await logger.async_log_success_event(
kwargs={
"standard_logging_object": StandardLoggingPayload(
custom_llm_provider="openai",
model="gpt-4o",
),
"start_time": start_time,
"end_time": now,
},
response_obj=None,
start_time=start_time,
end_time=now,
)
# At least request_count and total_latency
assert len(logger.log_queue) >= 2
@pytest.mark.asyncio
async def test_async_log_success_event_no_standard_logging_object(clean_env):
"""Test that events without standard_logging_object are skipped."""
logger = DatadogMetricsLogger(batch_size=100, start_periodic_flush=False)
await logger.async_log_success_event(
kwargs={},
response_obj=None,
start_time=datetime.now(),
end_time=datetime.now(),
)
assert len(logger.log_queue) == 0
@pytest.mark.asyncio
async def test_async_log_failure_event_extracts_status_code(clean_env):
"""Test that failure events extract the error status code."""
logger = DatadogMetricsLogger(batch_size=100, start_periodic_flush=False)
now = datetime.now()
start_time = now - timedelta(seconds=1)
await logger.async_log_failure_event(
kwargs={
"standard_logging_object": StandardLoggingPayload(
custom_llm_provider="openai",
model="gpt-4o",
error_information={"error_code": "429"},
),
"start_time": start_time,
"end_time": now,
},
response_obj=None,
start_time=start_time,
end_time=now,
)
count_series = next(
(s for s in logger.log_queue if s["metric"] == "litellm.llm_api.request_count"),
None,
)
assert count_series is not None
assert "status_code:429" in count_series["tags"]
@pytest.mark.asyncio
async def test_async_log_failure_event_default_status_code(clean_env):
"""Test that failure events default to 500 when no error_code is present."""
logger = DatadogMetricsLogger(batch_size=100, start_periodic_flush=False)
now = datetime.now()
await logger.async_log_failure_event(
kwargs={
"standard_logging_object": StandardLoggingPayload(
custom_llm_provider="openai",
model="gpt-4o",
),
"start_time": now,
"end_time": now,
},
response_obj=None,
start_time=now,
end_time=now,
)
count_series = next(
(s for s in logger.log_queue if s["metric"] == "litellm.llm_api.request_count"),
None,
)
assert count_series is not None
assert "status_code:500" in count_series["tags"]
@pytest.mark.asyncio
async def test_async_send_batch(clean_env):
"""Test that async_send_batch uploads metrics to Datadog."""
logger = DatadogMetricsLogger(start_periodic_flush=False)
logger.async_client = AsyncMock()
mock_request = Request("POST", "https://api.test.datadoghq.com/api/v2/series")
logger.async_client.post.return_value = Response(
202, json={"status": "ok"}, request=mock_request
)
# Manually add a metric series to the queue
logger.log_queue = [
{
"metric": "litellm.request.total_latency",
"type": 3,
"points": [{"timestamp": int(time.time()), "value": 1.5}],
"tags": ["env:test"],
}
]
await logger.async_send_batch()
assert logger.async_client.post.called
call_args = logger.async_client.post.call_args
assert call_args[0][0] == "https://api.test.datadoghq.com/api/v2/series"
# Verify gzip + JSON payload
import gzip
import json
compressed = call_args[1]["content"]
payload = json.loads(gzip.decompress(compressed).decode("utf-8"))
assert len(payload["series"]) == 1
assert payload["series"][0]["metric"] == "litellm.request.total_latency"
@pytest.mark.asyncio
async def test_async_send_batch_empty_queue(clean_env):
"""Test that async_send_batch does nothing when queue is empty."""
logger = DatadogMetricsLogger(start_periodic_flush=False)
logger.async_client = AsyncMock()
await logger.async_send_batch()
assert not logger.async_client.post.called