Skip to content

Commit 8872bf8

Browse files
authored
feat: OTLP Exporter builders to return specific Error type (#2790)
1 parent ddac8e1 commit 8872bf8

File tree

15 files changed

+169
-122
lines changed

15 files changed

+169
-122
lines changed

examples/tracing-jaeger/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ publish = false
88
[dependencies]
99
opentelemetry = { path = "../../opentelemetry" }
1010
opentelemetry_sdk = { path = "../../opentelemetry-sdk", features = ["rt-tokio"] }
11-
opentelemetry-otlp = { workspace = true, features = ["tonic"] }
11+
opentelemetry-otlp = { workspace = true, features = ["grpc-tonic"] }
1212
tokio = { workspace = true, features = ["full"] }

examples/tracing-jaeger/src/main.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ use opentelemetry::{
33
trace::{TraceContextExt, Tracer},
44
KeyValue,
55
};
6-
use opentelemetry_sdk::trace::{SdkTracerProvider, TraceError};
6+
use opentelemetry_otlp::ExporterBuildError;
7+
use opentelemetry_sdk::trace::SdkTracerProvider;
78
use opentelemetry_sdk::Resource;
89

910
use std::error::Error;
1011

11-
fn init_tracer_provider() -> Result<opentelemetry_sdk::trace::SdkTracerProvider, TraceError> {
12+
fn init_tracer_provider() -> Result<opentelemetry_sdk::trace::SdkTracerProvider, ExporterBuildError>
13+
{
1214
let exporter = opentelemetry_otlp::SpanExporter::builder()
1315
.with_tonic()
1416
.build()?;

opentelemetry-otlp/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
[#2770](https://github.com/open-telemetry/opentelemetry-rust/issues/2770)
99
partially to properly handle `shutdown()` when using `http`. (`tonic` still
1010
does not do proper shutdown)
11+
- *Breaking*
12+
ExporterBuilder's build() method now Result with `ExporterBuildError` being the
13+
Error variant. Previously it returned signal specific errors like `LogError`
14+
from the `opentelemetry_sdk`, which are no longer part of the sdk. No changes
15+
required if you were using unwrap/expect. If you were matching on the returning
16+
Error enum, replace with the enum `ExporterBuildError`. Unlike the previous
17+
`Error` which contained many variants unrelated to building an exporter, the
18+
new one returns specific variants applicable to building an exporter. Some
19+
variants might be applicable only on select features.
1120

1221
## 0.28.0
1322

opentelemetry-otlp/src/exporter/http/logs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ impl LogExporter for OtlpHttpClient {
1515

1616
let (body, content_type) = self
1717
.build_logs_export_body(batch)
18-
.map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?;
18+
.map_err(OTelSdkError::InternalFailure)?;
1919

2020
let mut request = http::Request::builder()
2121
.method(Method::POST)

opentelemetry-otlp/src/exporter/http/mod.rs

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{
2-
default_headers, default_protocol, parse_header_string,
2+
default_headers, default_protocol, parse_header_string, ExporterBuildError,
33
OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT,
44
};
55
use crate::{
@@ -108,7 +108,7 @@ impl HttpExporterBuilder {
108108
signal_endpoint_path: &str,
109109
signal_timeout_var: &str,
110110
signal_http_headers_var: &str,
111-
) -> Result<OtlpHttpClient, crate::Error> {
111+
) -> Result<OtlpHttpClient, ExporterBuildError> {
112112
let endpoint = resolve_http_endpoint(
113113
signal_endpoint_var,
114114
signal_endpoint_path,
@@ -168,12 +168,12 @@ impl HttpExporterBuilder {
168168
.unwrap_or_else(|_| reqwest::blocking::Client::new())
169169
})
170170
.join()
171-
.unwrap(), // Unwrap thread result
171+
.unwrap(), // TODO: Return ExporterBuildError::ThreadSpawnFailed
172172
) as Arc<dyn HttpClient>);
173173
}
174174
}
175175

176-
let http_client = http_client.ok_or(crate::Error::NoHttpClient)?;
176+
let http_client = http_client.ok_or(ExporterBuildError::NoHttpClient)?;
177177

178178
#[allow(clippy::mutable_key_type)] // http headers are not mutated
179179
let mut headers: HashMap<HeaderName, HeaderValue> = self
@@ -208,9 +208,7 @@ impl HttpExporterBuilder {
208208

209209
/// Create a log exporter with the current configuration
210210
#[cfg(feature = "trace")]
211-
pub fn build_span_exporter(
212-
mut self,
213-
) -> Result<crate::SpanExporter, opentelemetry_sdk::trace::TraceError> {
211+
pub fn build_span_exporter(mut self) -> Result<crate::SpanExporter, ExporterBuildError> {
214212
use crate::{
215213
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS,
216214
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT,
@@ -228,7 +226,7 @@ impl HttpExporterBuilder {
228226

229227
/// Create a log exporter with the current configuration
230228
#[cfg(feature = "logs")]
231-
pub fn build_log_exporter(mut self) -> opentelemetry_sdk::logs::LogResult<crate::LogExporter> {
229+
pub fn build_log_exporter(mut self) -> Result<crate::LogExporter, ExporterBuildError> {
232230
use crate::{
233231
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS,
234232
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
@@ -249,7 +247,7 @@ impl HttpExporterBuilder {
249247
pub fn build_metrics_exporter(
250248
mut self,
251249
temporality: opentelemetry_sdk::metrics::Temporality,
252-
) -> opentelemetry_sdk::metrics::MetricResult<crate::MetricExporter> {
250+
) -> Result<crate::MetricExporter, ExporterBuildError> {
253251
use crate::{
254252
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS,
255253
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT,
@@ -309,7 +307,7 @@ impl OtlpHttpClient {
309307
match self.protocol {
310308
#[cfg(feature = "http-json")]
311309
Protocol::HttpJson => match serde_json::to_string_pretty(&req) {
312-
Ok(json) => Ok((json.into(), "application/json")),
310+
Ok(json) => Ok((json.into_bytes(), "application/json")),
313311
Err(e) => Err(opentelemetry_sdk::trace::TraceError::from(e.to_string())),
314312
},
315313
_ => Ok((req.encode_to_vec(), "application/x-protobuf")),
@@ -320,7 +318,7 @@ impl OtlpHttpClient {
320318
fn build_logs_export_body(
321319
&self,
322320
logs: LogBatch<'_>,
323-
) -> opentelemetry_sdk::logs::LogResult<(Vec<u8>, &'static str)> {
321+
) -> Result<(Vec<u8>, &'static str), String> {
324322
use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
325323
let resource_logs = group_logs_by_resource_and_scope(logs, &self.resource);
326324
let req = ExportLogsServiceRequest { resource_logs };
@@ -329,7 +327,7 @@ impl OtlpHttpClient {
329327
#[cfg(feature = "http-json")]
330328
Protocol::HttpJson => match serde_json::to_string_pretty(&req) {
331329
Ok(json) => Ok((json.into(), "application/json")),
332-
Err(e) => Err(opentelemetry_sdk::logs::LogError::from(e.to_string())),
330+
Err(e) => Err(e.to_string()),
333331
},
334332
_ => Ok((req.encode_to_vec(), "application/x-protobuf")),
335333
}
@@ -357,21 +355,24 @@ impl OtlpHttpClient {
357355
}
358356
}
359357

360-
fn build_endpoint_uri(endpoint: &str, path: &str) -> Result<Uri, crate::Error> {
358+
fn build_endpoint_uri(endpoint: &str, path: &str) -> Result<Uri, ExporterBuildError> {
361359
let path = if endpoint.ends_with('/') && path.starts_with('/') {
362360
path.strip_prefix('/').unwrap()
363361
} else {
364362
path
365363
};
366-
format!("{endpoint}{path}").parse().map_err(From::from)
364+
let endpoint = format!("{endpoint}{path}");
365+
endpoint.parse().map_err(|er: http::uri::InvalidUri| {
366+
ExporterBuildError::InvalidUri(endpoint, er.to_string())
367+
})
367368
}
368369

369370
// see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#endpoint-urls-for-otlphttp
370371
fn resolve_http_endpoint(
371372
signal_endpoint_var: &str,
372373
signal_endpoint_path: &str,
373374
provided_endpoint: Option<String>,
374-
) -> Result<Uri, crate::Error> {
375+
) -> Result<Uri, ExporterBuildError> {
375376
// per signal env var is not modified
376377
if let Some(endpoint) = env::var(signal_endpoint_var)
377378
.ok()
@@ -388,14 +389,18 @@ fn resolve_http_endpoint(
388389
return Ok(endpoint);
389390
}
390391

391-
provided_endpoint
392-
.map(|e| e.parse().map_err(From::from))
393-
.unwrap_or_else(|| {
394-
build_endpoint_uri(
395-
OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT,
396-
signal_endpoint_path,
397-
)
398-
})
392+
if let Some(provider_endpoint) = provided_endpoint {
393+
provider_endpoint
394+
.parse()
395+
.map_err(|er: http::uri::InvalidUri| {
396+
ExporterBuildError::InvalidUri(provider_endpoint, er.to_string())
397+
})
398+
} else {
399+
build_endpoint_uri(
400+
OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT,
401+
signal_endpoint_path,
402+
)
403+
}
399404
}
400405

401406
#[allow(clippy::mutable_key_type)] // http headers are not mutated

opentelemetry-otlp/src/exporter/mod.rs

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
use crate::exporter::http::HttpExporterBuilder;
77
#[cfg(feature = "grpc-tonic")]
88
use crate::exporter::tonic::TonicExporterBuilder;
9-
use crate::{Error, Protocol};
9+
use crate::Protocol;
1010
#[cfg(feature = "serialize")]
1111
use serde::{Deserialize, Serialize};
1212
use std::fmt::{Display, Formatter};
1313
use std::str::FromStr;
1414
use std::time::Duration;
15+
use thiserror::Error;
1516

1617
/// Target to which the exporter is going to send signals, defaults to https://localhost:4317.
1718
/// Learn about the relationship between this constant and metrics/spans/logs at
@@ -92,6 +93,44 @@ impl Default for ExportConfig {
9293
}
9394
}
9495

96+
#[derive(Error, Debug)]
97+
/// Errors that can occur while building an exporter.
98+
// TODO: Refine and polish this.
99+
// Non-exhaustive to allow for future expansion without breaking changes.
100+
// This could be refined after polishing and finalizing the errors.
101+
#[non_exhaustive]
102+
pub enum ExporterBuildError {
103+
/// Spawning a new thread failed.
104+
#[error("Spawning a new thread failed. Unable to create Reqwest-Blocking client.")]
105+
ThreadSpawnFailed,
106+
107+
/// Feature required to use the specified compression algorithm.
108+
#[cfg(any(not(feature = "gzip-tonic"), not(feature = "zstd-tonic")))]
109+
#[error("feature '{0}' is required to use the compression algorithm '{1}'")]
110+
FeatureRequiredForCompressionAlgorithm(&'static str, Compression),
111+
112+
/// No Http client specified.
113+
#[error("no http client specified")]
114+
NoHttpClient,
115+
116+
/// Unsupported compression algorithm.
117+
#[error("unsupported compression algorithm '{0}'")]
118+
UnsupportedCompressionAlgorithm(String),
119+
120+
/// Invalid URI.
121+
#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))]
122+
#[error("invalid URI {0}. Reason {1}")]
123+
InvalidUri(String, String),
124+
125+
/// Failed due to an internal error.
126+
/// The error message is intended for logging purposes only and should not
127+
/// be used to make programmatic decisions. It is implementation-specific
128+
/// and subject to change without notice. Consumers of this error should not
129+
/// rely on its content beyond logging.
130+
#[error("Reason: {0}")]
131+
InternalFailure(String),
132+
}
133+
95134
/// The compression algorithm to use when sending data.
96135
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
97136
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -112,13 +151,15 @@ impl Display for Compression {
112151
}
113152

114153
impl FromStr for Compression {
115-
type Err = Error;
154+
type Err = ExporterBuildError;
116155

117156
fn from_str(s: &str) -> Result<Self, Self::Err> {
118157
match s {
119158
"gzip" => Ok(Compression::Gzip),
120159
"zstd" => Ok(Compression::Zstd),
121-
_ => Err(Error::UnsupportedCompressionAlgorithm(s.to_string())),
160+
_ => Err(ExporterBuildError::UnsupportedCompressionAlgorithm(
161+
s.to_string(),
162+
)),
122163
}
123164
}
124165
}
@@ -298,6 +339,55 @@ mod tests {
298339
assert_eq!(exporter_builder.exporter_config.endpoint, None);
299340
}
300341

342+
#[cfg(feature = "logs")]
343+
#[cfg(any(feature = "http-proto", feature = "http-json"))]
344+
#[test]
345+
fn export_builder_error_invalid_http_endpoint() {
346+
use std::time::Duration;
347+
348+
use crate::{ExportConfig, LogExporter, Protocol, WithExportConfig};
349+
350+
let ex_config = ExportConfig {
351+
endpoint: Some("invalid_uri/something".to_string()),
352+
protocol: Protocol::HttpBinary,
353+
timeout: Duration::from_secs(10),
354+
};
355+
356+
let exporter_result = LogExporter::builder()
357+
.with_http()
358+
.with_export_config(ex_config)
359+
.build();
360+
361+
assert!(matches!(
362+
exporter_result,
363+
Err(crate::exporter::ExporterBuildError::InvalidUri(_, _))
364+
));
365+
}
366+
367+
#[cfg(feature = "grpc-tonic")]
368+
#[test]
369+
fn export_builder_error_invalid_grpc_endpoint() {
370+
use std::time::Duration;
371+
372+
use crate::{ExportConfig, LogExporter, Protocol, WithExportConfig};
373+
374+
let ex_config = ExportConfig {
375+
endpoint: Some("invalid_uri/something".to_string()),
376+
protocol: Protocol::Grpc,
377+
timeout: Duration::from_secs(10),
378+
};
379+
380+
let exporter_result = LogExporter::builder()
381+
.with_tonic()
382+
.with_export_config(ex_config)
383+
.build();
384+
385+
assert!(matches!(
386+
exporter_result,
387+
Err(crate::exporter::ExporterBuildError::InvalidUri(_, _))
388+
));
389+
}
390+
301391
#[cfg(feature = "grpc-tonic")]
302392
#[test]
303393
fn test_default_tonic_endpoint() {

0 commit comments

Comments
 (0)