mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Supportability metric is sent on first request.
This commit is contained in:
parent
870c7662e0
commit
68be09adab
1 changed files with 100 additions and 12 deletions
|
|
@ -19,6 +19,8 @@ Usage:
|
|||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
|
|
@ -29,6 +31,12 @@ from litellm.types.utils import ModelResponse, Message
|
|||
import newrelic.agent
|
||||
|
||||
|
||||
# Global state for supportability metric emission
|
||||
# Protected by _metric_lock to ensure thread-safe access
|
||||
_last_metric_emission_time: float = 0.0
|
||||
_metric_lock = threading.Lock()
|
||||
|
||||
|
||||
class NewRelicLogger(CustomLogger):
|
||||
"""
|
||||
New Relic logger for LiteLLM to send AI monitoring events.
|
||||
|
|
@ -58,7 +66,8 @@ class NewRelicLogger(CustomLogger):
|
|||
else:
|
||||
# Validate that newrelic package is available
|
||||
try:
|
||||
import newrelic.agent
|
||||
newrelic.agent.register_application()
|
||||
|
||||
self.enabled = True
|
||||
verbose_logger.info(
|
||||
f"New Relic AI Monitoring initialized for app: {self.app_name}, "
|
||||
|
|
@ -80,6 +89,80 @@ class NewRelicLogger(CustomLogger):
|
|||
# Spec requires value to be either true (bool) or 'true' (string)
|
||||
return value.lower() == "true"
|
||||
|
||||
def _get_litellm_version(self) -> str:
|
||||
"""
|
||||
Get litellm version for supportability metrics.
|
||||
|
||||
Returns:
|
||||
Version string (e.g., "1.80.0") or "unknown" if unable to determine
|
||||
"""
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
return version('litellm')
|
||||
except Exception as e:
|
||||
verbose_logger.warning(f"Unable to determine litellm version: {e}")
|
||||
return "unknown"
|
||||
|
||||
def _emit_supportability_metric(self):
|
||||
"""
|
||||
Emit New Relic supportability metric for LiteLLM usage.
|
||||
|
||||
Per spec, this metric should be emitted at least once every 27 hours
|
||||
to indicate the library is in use. Format:
|
||||
Supportability/Python/ML/LiteLLM/{version}
|
||||
|
||||
This method updates the global _last_metric_emission_time and should
|
||||
be called within a lock when checking periodic emission.
|
||||
"""
|
||||
global _last_metric_emission_time
|
||||
|
||||
try:
|
||||
litellm_version = self._get_litellm_version()
|
||||
metric_name = f"Supportability/Python/ML/LiteLLM/{litellm_version}"
|
||||
|
||||
# Record metric with value of 1 (will be aggregated by New Relic)
|
||||
app = newrelic.agent.application()
|
||||
|
||||
if app and app.enabled:
|
||||
app.record_custom_metric(metric_name, 1)
|
||||
|
||||
# Update last emission time
|
||||
_last_metric_emission_time = time.time()
|
||||
|
||||
verbose_logger.info(
|
||||
f"Emitted New Relic supportability metric: {metric_name}"
|
||||
)
|
||||
else:
|
||||
verbose_logger.info("New Relic application is not enabled; skipping metric recording.")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
verbose_logger.warning(f"Failed to emit supportability metric: {e}")
|
||||
|
||||
def _check_and_emit_periodic_metric(self):
|
||||
"""
|
||||
Check if 23 hours have passed since last metric emission and re-emit if needed.
|
||||
|
||||
Uses a mutex to ensure only one thread emits the metric even if multiple
|
||||
requests are being processed concurrently.
|
||||
"""
|
||||
global _last_metric_emission_time
|
||||
|
||||
# Quick check without lock to avoid unnecessary locking
|
||||
current_time = time.time()
|
||||
time_since_last_emission = current_time - _last_metric_emission_time
|
||||
|
||||
# 23 hours in seconds = 82800
|
||||
if time_since_last_emission >= 82800:
|
||||
# Acquire lock to ensure only one thread emits
|
||||
with _metric_lock:
|
||||
# Double-check inside lock in case another thread just emitted
|
||||
current_time = time.time()
|
||||
time_since_last_emission = current_time - _last_metric_emission_time
|
||||
|
||||
if time_since_last_emission >= 82800:
|
||||
self._emit_supportability_metric()
|
||||
|
||||
def _should_record_content(self) -> bool:
|
||||
"""Check if message content should be recorded."""
|
||||
return self.record_content
|
||||
|
|
@ -377,7 +460,13 @@ class NewRelicLogger(CustomLogger):
|
|||
if "max_tokens" in request_params:
|
||||
event_data["request.max_tokens"] = request_params["max_tokens"]
|
||||
|
||||
newrelic.agent.record_custom_event("LlmChatCompletionSummary", event_data)
|
||||
app = newrelic.agent.application()
|
||||
|
||||
if app and app.enabled:
|
||||
app.record_custom_event("LlmChatCompletionSummary", event_data)
|
||||
else:
|
||||
verbose_logger.info("New Relic application is not enabled; skipping event recording.")
|
||||
|
||||
import pprint
|
||||
verbose_logger.info(f"Recorded LlmChatCompletionSummary event: {pprint.pformat(event_data)}")
|
||||
|
||||
|
|
@ -403,7 +492,7 @@ class NewRelicLogger(CustomLogger):
|
|||
messages: List of message dicts to record
|
||||
"""
|
||||
try:
|
||||
import newrelic.agent
|
||||
app = newrelic.agent.application()
|
||||
|
||||
for message in messages:
|
||||
sequence = message["sequence"]
|
||||
|
|
@ -431,7 +520,11 @@ class NewRelicLogger(CustomLogger):
|
|||
if span_id:
|
||||
event_data["span_id"] = span_id
|
||||
|
||||
newrelic.agent.record_custom_event("LlmChatCompletionMessage", event_data)
|
||||
|
||||
if app and app.enabled:
|
||||
app.record_custom_event("LlmChatCompletionMessage", event_data)
|
||||
else:
|
||||
verbose_logger.info("New Relic application is not enabled; skipping event recording.")
|
||||
|
||||
import pprint
|
||||
verbose_logger.info(
|
||||
|
|
@ -469,6 +562,9 @@ class NewRelicLogger(CustomLogger):
|
|||
if not self.enabled:
|
||||
return
|
||||
|
||||
# Check and emit periodic supportability metric if 23 hours have passed
|
||||
self._check_and_emit_periodic_metric()
|
||||
|
||||
import pprint
|
||||
verbose_logger.info(f"newrelic._process_success called, kwargs=\n{pprint.pformat(kwargs)}, \nresponse_obj=\n{pprint.pformat(response_obj)}")
|
||||
|
||||
|
|
@ -532,8 +628,6 @@ class NewRelicLogger(CustomLogger):
|
|||
"""Unused per spec."""
|
||||
pass
|
||||
|
||||
@newrelic.agent.background_task()
|
||||
@newrelic.agent.function_trace(name="log_success_event")
|
||||
def log_success_event(self, kwargs, response_obj, start_time, end_time):
|
||||
"""
|
||||
Main success path for non-streaming requests.
|
||||
|
|
@ -547,8 +641,6 @@ class NewRelicLogger(CustomLogger):
|
|||
verbose_logger.warning(f"Error in New Relic log_success_event: {e}")
|
||||
self.handle_callback_failure("newrelic")
|
||||
|
||||
@newrelic.agent.background_task()
|
||||
@newrelic.agent.function_trace(name="async_log_success_event")
|
||||
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
|
||||
"""
|
||||
Main success path for async/streaming requests.
|
||||
|
|
@ -562,8 +654,6 @@ class NewRelicLogger(CustomLogger):
|
|||
verbose_logger.warning(f"Error in New Relic async_log_success_event: {e}")
|
||||
self.handle_callback_failure("newrelic")
|
||||
|
||||
@newrelic.agent.background_task()
|
||||
@newrelic.agent.function_trace(name="log_failure_event")
|
||||
def log_failure_event(self, kwargs, response_obj, start_time, end_time):
|
||||
"""
|
||||
Log error metric for failed LLM calls (sync).
|
||||
|
|
@ -580,8 +670,6 @@ class NewRelicLogger(CustomLogger):
|
|||
verbose_logger.warning(f"Error in New Relic log_failure_event: {e}")
|
||||
self.handle_callback_failure("newrelic")
|
||||
|
||||
@newrelic.agent.background_task()
|
||||
@newrelic.agent.function_trace(name="async_log_failure_event")
|
||||
async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time):
|
||||
"""
|
||||
Log error metric for failed LLM calls (async).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue