@@ -19,6 +19,7 @@ use anyhow::Context;
1919use opentelemetry:: trace:: TracerProvider ;
2020use opentelemetry:: { KeyValue , global} ;
2121use opentelemetry_appender_tracing:: layer:: OpenTelemetryTracingBridge ;
22+ use opentelemetry_otlp:: { Protocol as OtlpWireProtocol , WithExportConfig } ;
2223use opentelemetry_sdk:: logs:: SdkLoggerProvider ;
2324use opentelemetry_sdk:: propagation:: TraceContextPropagator ;
2425use opentelemetry_sdk:: trace:: { BatchConfigBuilder , SdkTracerProvider } ;
@@ -39,6 +40,54 @@ use tracing_subscriber::prelude::*;
3940use tracing_subscriber:: registry:: LookupSpan ;
4041
4142use crate :: QW_ENABLE_OPENTELEMETRY_OTLP_EXPORTER_ENV_KEY ;
43+
44+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
45+ enum OtlpProtocol {
46+ Grpc ,
47+ HttpProtobuf ,
48+ HttpJson ,
49+ }
50+
51+ fn parse_otlp_protocol ( protocol_str : & str ) -> anyhow:: Result < OtlpProtocol > {
52+ const OTLP_PROTOCOL_GRPC : & str = "grpc" ;
53+ const OTLP_PROTOCOL_HTTP_PROTOBUF : & str = "http/protobuf" ;
54+ const OTLP_PROTOCOL_HTTP_JSON : & str = "http/json" ;
55+
56+ match protocol_str {
57+ OTLP_PROTOCOL_GRPC => Ok ( OtlpProtocol :: Grpc ) ,
58+ OTLP_PROTOCOL_HTTP_PROTOBUF => Ok ( OtlpProtocol :: HttpProtobuf ) ,
59+ OTLP_PROTOCOL_HTTP_JSON => Ok ( OtlpProtocol :: HttpJson ) ,
60+ other => anyhow:: bail!(
61+ "unsupported OTLP protocol `{other}`, supported values are `{OTLP_PROTOCOL_GRPC}`, \
62+ `{OTLP_PROTOCOL_HTTP_PROTOBUF}` and `{OTLP_PROTOCOL_HTTP_JSON}`"
63+ ) ,
64+ }
65+ }
66+
67+ /// Resolves the OTLP protocol from candidates in priority order, defaulting to gRPC.
68+ fn resolve_otlp_protocol ( candidates : & [ Option < & str > ] ) -> anyhow:: Result < OtlpProtocol > {
69+ match candidates. iter ( ) . flatten ( ) . next ( ) {
70+ Some ( protocol_str) => parse_otlp_protocol ( protocol_str) ,
71+ None => Ok ( OtlpProtocol :: Grpc ) ,
72+ }
73+ }
74+
75+ macro_rules! build_otlp_exporter {
76+ ( $builder: expr, $protocol: expr) => {
77+ match $protocol {
78+ OtlpProtocol :: Grpc => $builder. with_tonic( ) . build( ) ,
79+ OtlpProtocol :: HttpProtobuf => $builder
80+ . with_http( )
81+ . with_protocol( OtlpWireProtocol :: HttpBinary )
82+ . build( ) ,
83+ OtlpProtocol :: HttpJson => $builder
84+ . with_http( )
85+ . with_protocol( OtlpWireProtocol :: HttpJson )
86+ . build( ) ,
87+ }
88+ } ;
89+ }
90+
4291#[ cfg( feature = "tokio-console" ) ]
4392use crate :: QW_ENABLE_TOKIO_CONSOLE_ENV_KEY ;
4493
@@ -98,10 +147,16 @@ pub fn setup_logging_and_tracing(
98147 // Note on disabling ANSI characters: setting the ansi boolean on event format is insufficient.
99148 // It is thus set on layers, see https://github.com/tokio-rs/tracing/issues/1817
100149 let provider_opt = if get_bool_from_env ( QW_ENABLE_OPENTELEMETRY_OTLP_EXPORTER_ENV_KEY , false ) {
101- let span_exporter = opentelemetry_otlp:: SpanExporter :: builder ( )
102- . with_tonic ( )
103- . build ( )
104- . context ( "failed to initialize OpenTelemetry OTLP exporter" ) ?;
150+ let global_protocol = env:: var ( "OTEL_EXPORTER_OTLP_PROTOCOL" ) . ok ( ) ;
151+ let traces_protocol = resolve_otlp_protocol ( & [
152+ env:: var ( "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" )
153+ . ok ( )
154+ . as_deref ( ) ,
155+ global_protocol. as_deref ( ) ,
156+ ] ) ?;
157+ let span_exporter =
158+ build_otlp_exporter ! ( opentelemetry_otlp:: SpanExporter :: builder( ) , traces_protocol)
159+ . context ( "failed to initialize OTLP traces exporter" ) ?;
105160 let span_processor = trace:: BatchSpanProcessor :: builder ( span_exporter)
106161 . with_batch_config (
107162 BatchConfigBuilder :: default ( )
@@ -117,10 +172,13 @@ pub fn setup_logging_and_tracing(
117172 . with_attribute ( KeyValue :: new ( "service.version" , build_info. version . clone ( ) ) )
118173 . build ( ) ;
119174
120- let logs_exporter = opentelemetry_otlp:: LogExporter :: builder ( )
121- . with_tonic ( )
122- . build ( )
123- . context ( "failed to initialize OpenTelemetry OTLP logs" ) ?;
175+ let logs_protocol = resolve_otlp_protocol ( & [
176+ env:: var ( "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL" ) . ok ( ) . as_deref ( ) ,
177+ global_protocol. as_deref ( ) ,
178+ ] ) ?;
179+ let logs_exporter =
180+ build_otlp_exporter ! ( opentelemetry_otlp:: LogExporter :: builder( ) , logs_protocol)
181+ . context ( "failed to initialize OTLP logs exporter" ) ?;
124182
125183 let logger_provider = SdkLoggerProvider :: builder ( )
126184 . with_resource ( resource. clone ( ) )
@@ -363,3 +421,44 @@ pub(super) mod jemalloc_profiled {
363421 ) )
364422 }
365423}
424+
425+ #[ cfg( test) ]
426+ mod tests {
427+ use super :: * ;
428+
429+ #[ test]
430+ fn test_resolve_otlp_protocol_defaults_to_grpc ( ) {
431+ let protocol = resolve_otlp_protocol ( & [ None , None ] ) . unwrap ( ) ;
432+ assert_eq ! ( protocol, OtlpProtocol :: Grpc ) ;
433+ }
434+
435+ #[ test]
436+ fn test_resolve_otlp_protocol_first_candidate_takes_priority ( ) {
437+ let protocol = resolve_otlp_protocol ( & [ Some ( "http/protobuf" ) , Some ( "grpc" ) ] ) . unwrap ( ) ;
438+ assert_eq ! ( protocol, OtlpProtocol :: HttpProtobuf ) ;
439+ }
440+
441+ #[ test]
442+ fn test_resolve_otlp_protocol_falls_back_to_later_candidate ( ) {
443+ let protocol = resolve_otlp_protocol ( & [ None , Some ( "http/protobuf" ) ] ) . unwrap ( ) ;
444+ assert_eq ! ( protocol, OtlpProtocol :: HttpProtobuf ) ;
445+ }
446+
447+ #[ test]
448+ fn test_resolve_otlp_protocol_grpc_explicit ( ) {
449+ let protocol = resolve_otlp_protocol ( & [ Some ( "grpc" ) ] ) . unwrap ( ) ;
450+ assert_eq ! ( protocol, OtlpProtocol :: Grpc ) ;
451+ }
452+
453+ #[ test]
454+ fn test_resolve_otlp_protocol_http_json_explicit ( ) {
455+ let protocol = resolve_otlp_protocol ( & [ Some ( "http/json" ) ] ) . unwrap ( ) ;
456+ assert_eq ! ( protocol, OtlpProtocol :: HttpJson ) ;
457+ }
458+
459+ #[ test]
460+ fn test_resolve_otlp_protocol_rejects_unsupported_value ( ) {
461+ let result = resolve_otlp_protocol ( & [ Some ( "http/xml" ) ] ) ;
462+ assert ! ( result. is_err( ) ) ;
463+ }
464+ }
0 commit comments