@@ -227,8 +227,9 @@ def _init_metrics(self, meter_provider):
227227                PeriodicExportingMetricReader ,
228228            )
229229
230+             normalized_endpoint  =  self ._normalize_otel_endpoint (self .config .endpoint , 'metrics' )
230231            _metric_exporter  =  OTLPMetricExporter (
231-                 endpoint = self . config . endpoint ,
232+                 endpoint = normalized_endpoint ,
232233                headers = OpenTelemetry ._get_headers_dictionary (self .config .headers ),
233234                preferred_temporality = {Histogram : AggregationTemporality .DELTA },
234235            )
@@ -268,22 +269,20 @@ def _init_logs(self, logger_provider):
268269            return 
269270
270271        from  opentelemetry ._logs  import  set_logger_provider 
271-         from  opentelemetry .exporter .otlp .proto .grpc ._log_exporter  import  OTLPLogExporter 
272272        from  opentelemetry .sdk ._logs  import  LoggerProvider  as  OTLoggerProvider 
273273        from  opentelemetry .sdk ._logs .export  import  BatchLogRecordProcessor 
274274
275275        # set up log pipeline 
276276        if  logger_provider  is  None :
277-             logger_provider  =  OTLoggerProvider ()
277+             litellm_resource  =  _get_litellm_resource ()
278+             logger_provider  =  OTLoggerProvider (resource = litellm_resource )
278279            # Only add OTLP exporter if we created the logger provider ourselves 
279-             logger_provider .add_log_record_processor (
280-                 BatchLogRecordProcessor (
281-                     OTLPLogExporter (
282-                         endpoint = self .config .endpoint ,
283-                         headers = self ._get_headers_dictionary (self .config .headers ),
284-                     )
280+             log_exporter  =  self ._get_log_exporter ()
281+             if  log_exporter :
282+                 logger_provider .add_log_record_processor (
283+                     BatchLogRecordProcessor (log_exporter )  # type: ignore[arg-type] 
285284                )
286-             ) 
285+ 
287286        set_logger_provider (logger_provider )
288287
289288    def  log_success_event (self , kwargs , response_obj , start_time , end_time ):
@@ -658,10 +657,15 @@ def _emit_semantic_logs(self, kwargs, response_obj, span: Span):
658657        if  not  self .config .enable_events :
659658            return 
660659
661-         from  opentelemetry ._logs  import  LogRecord , get_logger 
660+         from  opentelemetry ._logs  import  SeverityNumber , get_logger , get_logger_provider 
661+         from  opentelemetry .sdk ._logs  import  LogRecord  as  SdkLogRecord 
662662
663663        otel_logger  =  get_logger (LITELLM_LOGGER_NAME )
664664
665+         # Get the resource from the logger provider 
666+         logger_provider  =  get_logger_provider ()
667+         resource  =  getattr (logger_provider , '_resource' , None ) or  _get_litellm_resource ()
668+ 
665669        parent_ctx  =  span .get_span_context ()
666670        provider  =  (kwargs .get ("litellm_params" ) or  {}).get (
667671            "custom_llm_provider" , "Unknown" 
@@ -676,15 +680,18 @@ def _emit_semantic_logs(self, kwargs, response_obj, span: Span):
676680            if  self .message_logging  and  msg .get ("content" ):
677681                attrs ["gen_ai.prompt" ] =  msg ["content" ]
678682
679-             otel_logger .emit (
680-                 LogRecord (
681-                     attributes = attrs ,
682-                     body = msg .copy (),
683-                     trace_id = parent_ctx .trace_id ,
684-                     span_id = parent_ctx .span_id ,
685-                     trace_flags = parent_ctx .trace_flags ,
686-                 )
683+             log_record  =  SdkLogRecord (
684+                 timestamp = self ._to_ns (datetime .now ()),
685+                 trace_id = parent_ctx .trace_id ,
686+                 span_id = parent_ctx .span_id ,
687+                 trace_flags = parent_ctx .trace_flags ,
688+                 severity_number = SeverityNumber .INFO ,
689+                 severity_text = "INFO" ,
690+                 body = msg .copy (),
691+                 resource = resource ,
692+                 attributes = attrs ,
687693            )
694+             otel_logger .emit (log_record )
688695
689696        # per-choice events 
690697        for  idx , choice  in  enumerate (response_obj .get ("choices" , [])):
@@ -705,15 +712,18 @@ def _emit_semantic_logs(self, kwargs, response_obj, span: Span):
705712            if  self .message_logging  and  body_msg .get ("content" ):
706713                body ["message" ]["content" ] =  body_msg ["content" ]
707714
708-             otel_logger .emit (
709-                 LogRecord (
710-                     attributes = attrs ,
711-                     body = body ,
712-                     trace_id = parent_ctx .trace_id ,
713-                     span_id = parent_ctx .span_id ,
714-                     trace_flags = parent_ctx .trace_flags ,
715-                 )
715+             log_record  =  SdkLogRecord (
716+                 timestamp = self ._to_ns (datetime .now ()),
717+                 trace_id = parent_ctx .trace_id ,
718+                 span_id = parent_ctx .span_id ,
719+                 trace_flags = parent_ctx .trace_flags ,
720+                 severity_number = SeverityNumber .INFO ,
721+                 severity_text = "INFO" ,
722+                 body = body ,
723+                 resource = resource ,
724+                 attributes = attrs ,
716725            )
726+             otel_logger .emit (log_record )
717727
718728    def  _create_guardrail_span (
719729        self , kwargs : Optional [dict ], context : Optional [Context ]
@@ -1292,19 +1302,21 @@ def _get_span_processor(self, dynamic_headers: Optional[dict] = None):
12921302                "OpenTelemetry: intiializing http exporter. Value of OTEL_EXPORTER: %s" ,
12931303                self .OTEL_EXPORTER ,
12941304            )
1305+             normalized_endpoint  =  self ._normalize_otel_endpoint (self .OTEL_ENDPOINT , 'traces' )
12951306            return  BatchSpanProcessor (
12961307                OTLPSpanExporterHTTP (
1297-                     endpoint = self . OTEL_ENDPOINT , headers = _split_otel_headers 
1308+                     endpoint = normalized_endpoint , headers = _split_otel_headers 
12981309                ),
12991310            )
13001311        elif  self .OTEL_EXPORTER  ==  "otlp_grpc"  or  self .OTEL_EXPORTER  ==  "grpc" :
13011312            verbose_logger .debug (
13021313                "OpenTelemetry: intiializing grpc exporter. Value of OTEL_EXPORTER: %s" ,
13031314                self .OTEL_EXPORTER ,
13041315            )
1316+             normalized_endpoint  =  self ._normalize_otel_endpoint (self .OTEL_ENDPOINT , 'traces' )
13051317            return  BatchSpanProcessor (
13061318                OTLPSpanExporterGRPC (
1307-                     endpoint = self . OTEL_ENDPOINT , headers = _split_otel_headers 
1319+                     endpoint = normalized_endpoint , headers = _split_otel_headers 
13081320                ),
13091321            )
13101322        else :
@@ -1314,6 +1326,145 @@ def _get_span_processor(self, dynamic_headers: Optional[dict] = None):
13141326            )
13151327            return  BatchSpanProcessor (ConsoleSpanExporter ())
13161328
1329+     def  _get_log_exporter (self ):
1330+         """ 
1331+         Get the appropriate log exporter based on the configuration. 
1332+         """ 
1333+         verbose_logger .debug (
1334+             "OpenTelemetry Logger, initializing log exporter \n self.OTEL_EXPORTER: %s\n self.OTEL_ENDPOINT: %s\n self.OTEL_HEADERS: %s" ,
1335+             self .OTEL_EXPORTER ,
1336+             self .OTEL_ENDPOINT ,
1337+             self .OTEL_HEADERS ,
1338+         )
1339+ 
1340+         _split_otel_headers  =  OpenTelemetry ._get_headers_dictionary (self .OTEL_HEADERS )
1341+ 
1342+         # Normalize endpoint for logs - ensure it points to /v1/logs instead of /v1/traces 
1343+         normalized_endpoint  =  self ._normalize_otel_endpoint (self .OTEL_ENDPOINT , 'logs' )
1344+ 
1345+         verbose_logger .debug (
1346+             "OpenTelemetry: Log endpoint normalized from %s to %s" ,
1347+             self .OTEL_ENDPOINT ,
1348+             normalized_endpoint ,
1349+         )
1350+ 
1351+         if  hasattr (self .OTEL_EXPORTER , "export" ):
1352+             # Custom exporter provided 
1353+             verbose_logger .debug (
1354+                 "OpenTelemetry: Using custom log exporter. Value of OTEL_EXPORTER: %s" ,
1355+                 self .OTEL_EXPORTER ,
1356+             )
1357+             return  self .OTEL_EXPORTER 
1358+ 
1359+         if  self .OTEL_EXPORTER  ==  "console" :
1360+             from  opentelemetry .sdk ._logs .export  import  ConsoleLogExporter 
1361+             verbose_logger .debug (
1362+                 "OpenTelemetry: Using console log exporter. Value of OTEL_EXPORTER: %s" ,
1363+                 self .OTEL_EXPORTER ,
1364+             )
1365+             return  ConsoleLogExporter ()
1366+         elif  (
1367+             self .OTEL_EXPORTER  ==  "otlp_http" 
1368+             or  self .OTEL_EXPORTER  ==  "http/protobuf" 
1369+             or  self .OTEL_EXPORTER  ==  "http/json" 
1370+         ):
1371+             from  opentelemetry .exporter .otlp .proto .http ._log_exporter  import  OTLPLogExporter 
1372+             verbose_logger .debug (
1373+                 "OpenTelemetry: Using HTTP log exporter. Value of OTEL_EXPORTER: %s, endpoint: %s" ,
1374+                 self .OTEL_EXPORTER ,
1375+                 normalized_endpoint ,
1376+             )
1377+             return  OTLPLogExporter (
1378+                 endpoint = normalized_endpoint , headers = _split_otel_headers 
1379+             )
1380+         elif  self .OTEL_EXPORTER  ==  "otlp_grpc"  or  self .OTEL_EXPORTER  ==  "grpc" :
1381+             from  opentelemetry .exporter .otlp .proto .grpc ._log_exporter  import  OTLPLogExporter 
1382+             verbose_logger .debug (
1383+                 "OpenTelemetry: Using gRPC log exporter. Value of OTEL_EXPORTER: %s, endpoint: %s" ,
1384+                 self .OTEL_EXPORTER ,
1385+                 normalized_endpoint ,
1386+             )
1387+             return  OTLPLogExporter (
1388+                 endpoint = normalized_endpoint , headers = _split_otel_headers 
1389+             )
1390+         else :
1391+             verbose_logger .warning (
1392+                 "OpenTelemetry: Unknown log exporter '%s', defaulting to console. Supported: console, otlp_http, otlp_grpc" ,
1393+                 self .OTEL_EXPORTER ,
1394+             )
1395+             from  opentelemetry .sdk ._logs .export  import  ConsoleLogExporter 
1396+             return  ConsoleLogExporter ()
1397+ 
1398+     def  _normalize_otel_endpoint (
1399+         self ,
1400+         endpoint : Optional [str ],
1401+         signal_type : str 
1402+     ) ->  Optional [str ]:
1403+         """ 
1404+         Normalize the endpoint URL for a specific OpenTelemetry signal type. 
1405+ 
1406+         The OTLP exporters expect endpoints to use signal-specific paths: 
1407+         - traces: /v1/traces 
1408+         - metrics: /v1/metrics 
1409+         - logs: /v1/logs 
1410+ 
1411+         This method ensures the endpoint has the correct path for the given signal type. 
1412+ 
1413+         Args: 
1414+             endpoint: The endpoint URL to normalize 
1415+             signal_type: The telemetry signal type ('traces', 'metrics', or 'logs') 
1416+ 
1417+         Returns: 
1418+             Normalized endpoint URL with the correct signal path 
1419+ 
1420+         Examples: 
1421+             _normalize_otel_endpoint("http://collector:4318/v1/traces", "logs") 
1422+             -> "http://collector:4318/v1/logs" 
1423+ 
1424+             _normalize_otel_endpoint("http://collector:4318", "traces") 
1425+             -> "http://collector:4318/v1/traces" 
1426+ 
1427+             _normalize_otel_endpoint("http://collector:4318/v1/logs", "metrics") 
1428+             -> "http://collector:4318/v1/metrics" 
1429+         """ 
1430+         if  not  endpoint :
1431+             return  endpoint 
1432+ 
1433+         # Validate signal_type 
1434+         valid_signals  =  {'traces' , 'metrics' , 'logs' }
1435+         if  signal_type  not  in   valid_signals :
1436+             verbose_logger .warning (
1437+                 "Invalid signal_type '%s' provided to _normalize_otel_endpoint. " 
1438+                 "Valid values: %s. Returning endpoint unchanged." ,
1439+                 signal_type ,
1440+                 valid_signals 
1441+             )
1442+             return  endpoint 
1443+ 
1444+         # Remove trailing slash 
1445+         endpoint  =  endpoint .rstrip ('/' )
1446+ 
1447+         # Check if endpoint already ends with the correct signal path 
1448+         target_path  =  f'/v1/{ signal_type }  ' 
1449+         if  endpoint .endswith (target_path ):
1450+             return  endpoint 
1451+ 
1452+         # Replace existing signal path with the target signal path 
1453+         other_signals  =  valid_signals  -  {signal_type }
1454+         for  other_signal  in  other_signals :
1455+             other_path  =  f'/v1/{ other_signal }  ' 
1456+             if  endpoint .endswith (other_path ):
1457+                 endpoint  =  endpoint .rsplit ('/' , 1 )[0 ] +  f'/{ signal_type }  ' 
1458+                 return  endpoint 
1459+ 
1460+         # No existing signal path found, append the target path 
1461+         if  not  endpoint .endswith ('/v1' ):
1462+             endpoint  =  endpoint  +  target_path 
1463+         else :
1464+             endpoint  =  endpoint  +  f'/{ signal_type }  ' 
1465+ 
1466+         return  endpoint 
1467+ 
13171468    @staticmethod  
13181469    def  _get_headers_dictionary (headers : Optional [Union [str , dict ]]) ->  Dict [str , str ]:
13191470        """ 
0 commit comments