11use anyhow:: anyhow;
22use assert_matches:: assert_matches;
3+ use std:: sync:: Mutex ;
34use std:: { collections:: HashMap , env, net:: SocketAddr , sync:: Arc , time:: Duration } ;
45use temporal_client:: {
56 WorkflowClientTrait , WorkflowOptions , WorkflowService , REQUEST_LATENCY_HISTOGRAM_NAME ,
@@ -45,6 +46,7 @@ use temporal_sdk_core_test_utils::{
4546 get_integ_server_options, get_integ_telem_options, CoreWfStarter , NAMESPACE , OTEL_URL_ENV_VAR ,
4647} ;
4748use tokio:: { join, sync:: Barrier , task:: AbortHandle } ;
49+ use tracing_subscriber:: fmt:: MakeWriter ;
4850use url:: Url ;
4951
5052static ANY_PORT : & str = "127.0.0.1:0" ;
@@ -815,3 +817,124 @@ async fn evict_on_complete_does_not_count_as_forced_eviction() {
815817 // Metric shouldn't show up at all, since it's zero the whole time.
816818 assert ! ( !body. contains( "temporal_sticky_cache_total_forced_eviction" ) ) ;
817819}
820+
821+ #[ tokio:: test]
822+ async fn test_otel_errors_are_logged_as_errors ( ) {
823+ // Create a subscriber that captures logs
824+ let collector = tracing_subscriber:: fmt ( )
825+ . with_max_level ( tracing:: Level :: ERROR )
826+ . with_test_writer ( )
827+ . finish ( ) ;
828+
829+ // Set this subscriber as default
830+ let _guard = tracing:: subscriber:: set_default ( collector) ;
831+
832+ let opts = OtelCollectorOptionsBuilder :: default ( )
833+ // Intentionally invalid endpoint
834+ . url ( "http://localhost:9999/v1/metrics" . parse ( ) . unwrap ( ) )
835+ // .protocol(OtlpProtocol::Http)
836+ . build ( )
837+ . unwrap ( ) ;
838+
839+ // This exporter will fail every time it tries to export metrics
840+ let exporter = build_otlp_metric_exporter ( opts) . unwrap ( ) ;
841+
842+ }
843+
844+ // use std::sync::{Arc, Mutex};
845+ // use std::time::Duration;
846+ // use temporal_sdk_core::{
847+ // CoreRuntime, TelemetryOptionsBuilder, telemetry::{build_otlp_metric_exporter, OtlpProtocol, OtelCollectorOptionsBuilder, CoreMeter},
848+ // };
849+ // use tracing_subscriber::fmt::MakeWriter;
850+ // use tracing::Level;
851+ // use tokio::time::sleep;
852+
853+ // A writer that captures logs into a buffer so we can assert on them.
854+ struct CapturingWriter {
855+ buf : Arc < Mutex < Vec < u8 > > > ,
856+ }
857+
858+ impl MakeWriter < ' _ > for CapturingWriter {
859+ type Writer = CapturingHandle ;
860+
861+ fn make_writer ( & self ) -> Self :: Writer {
862+ CapturingHandle ( self . buf . clone ( ) )
863+ }
864+ }
865+
866+ struct CapturingHandle ( Arc < Mutex < Vec < u8 > > > ) ;
867+
868+ impl std:: io:: Write for CapturingHandle {
869+ fn write ( & mut self , buf : & [ u8 ] ) -> std:: io:: Result < usize > {
870+ let mut b = self . 0 . lock ( ) . unwrap ( ) ;
871+ b. extend_from_slice ( buf) ;
872+ Ok ( buf. len ( ) )
873+ }
874+ fn flush ( & mut self ) -> std:: io:: Result < ( ) > {
875+ Ok ( ( ) )
876+ }
877+ }
878+
879+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
880+ async fn test_otel_error_logged ( ) {
881+ // Set up tracing subscriber to capture ERROR logs
882+ let logs = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
883+ let writer = CapturingWriter { buf : logs. clone ( ) } ;
884+
885+ let subscriber = tracing_subscriber:: fmt ( )
886+ . with_max_level ( tracing:: Level :: TRACE )
887+ . with_writer ( writer)
888+ . finish ( ) ;
889+ let _guard = tracing:: subscriber:: set_default ( subscriber) ;
890+
891+ // Configure OTLP exporter with an invalid endpoint so it fails
892+ let opts = OtelCollectorOptionsBuilder :: default ( )
893+ . url ( "http://localhost:9999/v1/metrics" . parse ( ) . unwrap ( ) ) // Invalid endpoint
894+ // .protocol(OtlpProtocol::Http)
895+ . build ( )
896+ . unwrap ( ) ;
897+ let exporter = build_otlp_metric_exporter ( opts) . unwrap ( ) ;
898+
899+ let telemopts = TelemetryOptionsBuilder :: default ( )
900+ . metrics ( Arc :: new ( exporter) as Arc < dyn CoreMeter > )
901+ . build ( )
902+ . unwrap ( ) ;
903+
904+ let rt = CoreRuntime :: new_assume_tokio ( telemopts) . unwrap ( ) ;
905+
906+ let opts = get_integ_server_options ( ) ;
907+ let mut raw_client = opts
908+ . connect_no_namespace ( rt. telemetry ( ) . get_temporal_metric_meter ( ) )
909+ . await
910+ . unwrap ( ) ;
911+ assert ! ( raw_client. get_client( ) . capabilities( ) . is_some( ) ) ;
912+
913+ let _ = raw_client
914+ . list_namespaces ( ListNamespacesRequest :: default ( ) )
915+ . await
916+ . unwrap ( ) ;
917+
918+ // Trigger metric emission or just wait for exporter attempts
919+ // If you have a Temporal client to generate metrics, you can do so here.
920+ // For now, just wait to allow exporter to attempt sending metrics and fail.
921+ tokio:: time:: sleep ( Duration :: from_secs ( 5 ) ) . await ;
922+
923+
924+ // Check the captured logs
925+ let logs = logs. lock ( ) . unwrap ( ) ;
926+ let log_str = String :: from_utf8_lossy ( & logs) ;
927+
928+ // Assert that there is an error log
929+ assert ! (
930+ log_str. contains( "ERROR" ) ,
931+ "Expected ERROR log not found in logs: {}" ,
932+ log_str
933+ ) ;
934+ // Look for some substring that indicates OTLP export failed
935+ assert ! (
936+ log_str. contains( "failed" ) || log_str. contains( "error" ) ,
937+ "Expected an OTel exporter error message in logs: {}" ,
938+ log_str
939+ ) ;
940+ }
0 commit comments