1818import enum
1919import os
2020import queue
21- import sys
2221import threading
2322from typing import (
2423 Any ,
133132# rather than inherit AuthorizedSession's 120s default.
134133_TELEMETRY_API_CHECK_TIMEOUT_SECONDS = 5.0
135134
135+ _DEFAULT_TELEMETRY_LOGS_ENDPOINT = "https://telemetry.googleapis.com/v1/logs"
136+
137+ _GCP_LOG_NAME = "gcp.log_name"
138+ _EVENT_NAME = "event.name"
139+ _GCP_RESOURCE_TYPE = "gcp.resource_type"
140+ _LOCATION = "location"
141+ _REASONING_ENGINE_ID = "reasoning_engine_id"
142+
136143
137144class _MtlsEndpoint (enum .Enum ):
138145 """Enum for the mTLS endpoint setting."""
@@ -357,8 +364,8 @@ def _warn_missing_dependency(
357364 )
358365 MISSING_LOGGING_IMPORT_ERROR_MESSAGE = (
359366 "proceeding with logging disabled because not all packages (i.e."
360- " `google-cloud-logging `, `opentelemetry-sdk`, "
361- " `opentelemetry-exporter-gcp-logging`) for tracing have been installed"
367+ " `opentelemetry-sdk `, `opentelemetry-exporter-otlp-proto-http`) "
368+ " for logging have been installed"
362369 )
363370
364371 if needed_for_tracing and enable_tracing :
@@ -367,15 +374,6 @@ def _warn_missing_dependency(
367374 _warn (MISSING_LOGGING_IMPORT_ERROR_MESSAGE )
368375 return None
369376
370- def _detect_cloud_resource_id (project_id : str ) -> Optional [str ]:
371- location = os .getenv ("GOOGLE_CLOUD_AGENT_ENGINE_LOCATION" , "" ) or os .getenv (
372- "GOOGLE_CLOUD_LOCATION" , ""
373- )
374- agent_engine_id = os .getenv ("GOOGLE_CLOUD_AGENT_ENGINE_ID" )
375- if all (v is not None for v in (location , agent_engine_id )):
376- return f"//aiplatform.googleapis.com/projects/{ project_id } /locations/{ location } /reasoningEngines/{ agent_engine_id } "
377- return None
378-
379377 try :
380378 import opentelemetry
381379 import opentelemetry .trace
@@ -396,30 +394,7 @@ def _detect_cloud_resource_id(project_id: str) -> Optional[str]:
396394 "opentelemetry-sdk" , needed_for_tracing = True , needed_for_logging = True
397395 )
398396
399- import uuid
400-
401- # Provide a set of resource attributes but allow to override them with env
402- # variables like OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME.
403- cloud_resource_id = _detect_cloud_resource_id (project_id )
404- resource = opentelemetry .sdk .resources .Resource .create (
405- attributes = {
406- "gcp.project_id" : project_id ,
407- "cloud.account.id" : project_id ,
408- "cloud.provider" : "gcp" ,
409- "cloud.platform" : "gcp.agent_engine" ,
410- "service.name" : os .getenv ("GOOGLE_CLOUD_AGENT_ENGINE_ID" , "" ),
411- "service.instance.id" : f"{ uuid .uuid4 ().hex } -{ os .getpid ()} " ,
412- "cloud.region" : (
413- os .getenv ("GOOGLE_CLOUD_AGENT_ENGINE_LOCATION" , "" )
414- or os .getenv ("GOOGLE_CLOUD_LOCATION" , "" )
415- ),
416- }
417- | (
418- {"cloud.resource_id" : cloud_resource_id }
419- if cloud_resource_id is not None
420- else {}
421- )
422- ).merge (opentelemetry .sdk .resources .OTELResourceDetector ().detect ())
397+ resource = _create_otel_resource (project_id )
423398
424399 if enable_tracing :
425400 try :
@@ -435,12 +410,7 @@ def _detect_cloud_resource_id(project_id: str) -> Optional[str]:
435410 import google .auth
436411
437412 credentials , _ = google .auth .default ()
438- vertex_sdk_version = aip_version .__version__
439- otlp_http_version = opentelemetry .exporter .otlp .proto .http .version .__version__
440- user_agent = (
441- f"Vertex-Agent-Engine/{ vertex_sdk_version } "
442- f" OTel-OTLP-Exporter-Python/{ otlp_http_version } "
443- )
413+ user_agent = _get_user_agent ()
444414
445415 session = requests_auth .AuthorizedSession (credentials = credentials )
446416
@@ -500,54 +470,47 @@ def _detect_cloud_resource_id(project_id: str) -> Optional[str]:
500470
501471 if enable_logging :
502472 try :
503- import opentelemetry .exporter .cloud_logging
473+ import opentelemetry .exporter .otlp .proto .http ._log_exporter
474+ import google .auth .transport .requests
504475 except (ImportError , AttributeError ):
505476 return _warn_missing_dependency (
506- "opentelemetry-exporter-gcp-logging " , needed_for_logging = True
477+ "opentelemetry-exporter-otlp-proto-http " , needed_for_logging = True
507478 )
508479
509- class _SimpleLogRecordProcessor (
510- opentelemetry .sdk ._logs .export .SimpleLogRecordProcessor
511- ):
480+ import google .auth
512481
513- def force_flush (
514- self , timeout_millis : int = 30000
515- ) -> bool : # pylint: disable=no-self-use
516- sys .stdout .flush ()
517- sys .stderr .flush ()
518- return True
519-
520- logger_provider = opentelemetry .sdk ._logs .LoggerProvider (resource = resource )
521- # Use the legacy log processor when experimental semconv is enabled.
522- # Exporting JSON logs to stdout is bugged; Agent Engine fails to
523- # correctly parse the `gen_ai.client.inference.operation.details`
524- # messages.
525- # TODO: b/480102541 - Unify both branches once the regression is fixed.
526- if "gen_ai_latest_experimental" in os .getenv (
527- "OTEL_SEMCONV_STABILITY_OPT_IN" , ""
528- ).split ("," ):
529- logger_provider .add_log_record_processor (
530- opentelemetry .sdk ._logs .export .BatchLogRecordProcessor (
531- opentelemetry .exporter .cloud_logging .CloudLoggingExporter (
532- project_id = project_id ,
533- default_log_name = os .getenv (
534- "GCP_DEFAULT_LOG_NAME" , "adk-on-agent-engine"
535- ),
536- ),
537- )
482+ credentials , _ = google .auth .default ()
483+ session = requests_auth .AuthorizedSession (credentials = credentials )
484+
485+ if _use_client_cert_effective ():
486+ client_cert_source = (
487+ mtls .default_client_cert_source ()
488+ if mtls .has_default_client_cert_source ()
489+ else None
538490 )
491+ session .configure_mtls_channel ()
492+ endpoint = _get_logs_api_endpoint (client_cert_source )
539493 else :
540- logger_provider .add_log_record_processor (
541- _SimpleLogRecordProcessor (
542- opentelemetry .exporter .cloud_logging .CloudLoggingExporter (
543- project_id = project_id ,
544- default_log_name = os .getenv (
545- "GCP_DEFAULT_LOG_NAME" , "adk-on-agent-engine"
546- ),
547- structured_json_file = sys .stdout ,
548- ),
549- )
494+ endpoint = _DEFAULT_TELEMETRY_LOGS_ENDPOINT
495+
496+ # One processor serves stable and experimental semconv. The stdout
497+ # branch experimental records used to need is gone along with the
498+ # Cloud Logging exporter (b/480102541).
499+ logger_provider = opentelemetry .sdk ._logs .LoggerProvider (
500+ resource = _create_otel_resource (project_id , "logs" )
501+ )
502+ logger_provider .add_log_record_processor (
503+ _named_batch_log_record_processor (
504+ opentelemetry .exporter .otlp .proto .http ._log_exporter .OTLPLogExporter (
505+ session = session ,
506+ endpoint = endpoint ,
507+ headers = {"User-Agent" : _get_user_agent ()},
508+ ),
509+ default_log_name = os .getenv (
510+ "GCP_DEFAULT_LOG_NAME" , "adk-on-agent-engine"
511+ ),
550512 )
513+ )
551514
552515 opentelemetry ._logs .set_logger_provider (logger_provider = logger_provider )
553516
@@ -637,6 +600,109 @@ def _warn_if_telemetry_api_disabled():
637600 _warn (_TELEMETRY_API_DISABLED_WARNING % (project , project ))
638601
639602
603+ def _get_user_agent () -> str :
604+ """Returns the User-Agent to send on OTLP exports."""
605+ from google .cloud .aiplatform import version as aip_version
606+
607+ user_agent = f"Vertex-Agent-Engine/{ aip_version .__version__ } "
608+ try :
609+ import opentelemetry .exporter .otlp .proto .http .version
610+
611+ user_agent += (
612+ " OTel-OTLP-Exporter-Python/"
613+ f"{ opentelemetry .exporter .otlp .proto .http .version .__version__ } "
614+ )
615+ except (ImportError , AttributeError ):
616+ pass
617+ return user_agent
618+
619+
620+ def _get_logs_api_endpoint (client_cert_source : bytes | None = None ) -> str :
621+ """Returns the logs endpoint matching _get_api_endpoint's mTLS decision.
622+
623+ Args:
624+ client_cert_source (bytes | None): The client certificate source.
625+
626+ Returns:
627+ str: The logs API endpoint to be used.
628+ """
629+ return _get_api_endpoint (client_cert_source ).replace ("/v1/traces" , "/v1/logs" )
630+
631+
632+ def _create_otel_resource (project_id : str , for_signal : str = "unspecified" ):
633+ """Returns the OTel resource describing the Agent Engine deployment.
634+
635+ Args:
636+ project_id: Project to which to send telemetry.
637+ for_signal: The signal the resource is for. `logs` adds the
638+ MonitoredResource hints Cloud Logging needs, which must not be set
639+ on the resource traces and metrics share.
640+
641+ Returns:
642+ The resource to set on the provider for `for_signal`.
643+ """
644+ import os
645+ import uuid
646+
647+ import opentelemetry .sdk .resources
648+
649+ location = os .getenv ("GOOGLE_CLOUD_AGENT_ENGINE_LOCATION" , "" ) or os .getenv (
650+ "GOOGLE_CLOUD_LOCATION" , ""
651+ )
652+ agent_engine_id = os .getenv ("GOOGLE_CLOUD_AGENT_ENGINE_ID" , "" )
653+ attributes = {
654+ "gcp.project_id" : project_id ,
655+ "cloud.account.id" : project_id ,
656+ "cloud.provider" : "gcp" ,
657+ "cloud.platform" : "gcp.agent_engine" ,
658+ "service.name" : agent_engine_id ,
659+ "service.instance.id" : f"{ uuid .uuid4 ().hex } -{ os .getpid ()} " ,
660+ "cloud.region" : location ,
661+ }
662+ if location and agent_engine_id :
663+ attributes ["cloud.resource_id" ] = (
664+ f"//aiplatform.googleapis.com/projects/{ project_id } "
665+ f"/locations/{ location } /reasoningEngines/{ agent_engine_id } "
666+ )
667+ if for_signal == "logs" :
668+ # Cloud Logging otherwise detects resource as `generic_task`
669+ attributes [_GCP_RESOURCE_TYPE ] = "aiplatform.googleapis.com/ReasoningEngine"
670+ attributes [_LOCATION ] = location
671+ attributes [_REASONING_ENGINE_ID ] = agent_engine_id
672+
673+ # Provide a set of resource attributes but allow to override them with env
674+ # variables like OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME.
675+ return opentelemetry .sdk .resources .Resource .create (attributes = attributes ).merge (
676+ opentelemetry .sdk .resources .OTELResourceDetector ().detect ()
677+ )
678+
679+
680+ def _named_batch_log_record_processor (exporter , * , default_log_name : str ):
681+ """Returns a batch processor that keeps log names and labels stable.
682+
683+ Args:
684+ exporter: The OTLP log exporter to wrap.
685+ default_log_name (str): Log name for records that carry none.
686+
687+ Returns:
688+ The configured log record processor.
689+ """
690+ import opentelemetry .sdk ._logs .export
691+
692+ class _Processor (opentelemetry .sdk ._logs .export .BatchLogRecordProcessor ):
693+ def on_emit (self , log_record ) -> None :
694+ record = log_record .log_record
695+ attributes = dict (record .attributes or {})
696+ if record .event_name :
697+ attributes .setdefault (_EVENT_NAME , record .event_name )
698+ elif _GCP_LOG_NAME not in attributes :
699+ attributes [_GCP_LOG_NAME ] = default_log_name
700+ record .attributes = attributes
701+ super ().on_emit (log_record )
702+
703+ return _Processor (exporter )
704+
705+
640706def _get_api_endpoint (client_cert_source : bytes | None = None ) -> str :
641707 """Returns API endpoint based on mTLS configuration and cert availability.
642708
0 commit comments