Skip to content

Commit aceb106

Browse files
authored
fix: use semconv in tower layer (#435)
1 parent 381c894 commit aceb106

File tree

3 files changed

+310
-10
lines changed

3 files changed

+310
-10
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Changelog
2+
3+
## vNext
4+
5+
### Changed
6+
7+
* Migrate to use `opentelemetry-semantic-conventions` package for metric names and attribute keys instead of hardcoded strings
8+
* Add dependency on otel semantic conventions crate and use constants from it instead of hardcoded attribute names. The values are unchanged
9+
- `HTTP_SERVER_ACTIVE_REQUESTS_METRIC` now uses `semconv::metric::HTTP_SERVER_ACTIVE_REQUESTS`
10+
- `HTTP_SERVER_REQUEST_BODY_SIZE_METRIC` now uses `semconv::metric::HTTP_SERVER_REQUEST_BODY_SIZE`
11+
- `HTTP_SERVER_RESPONSE_BODY_SIZE_METRIC` now uses `semconv::metric::HTTP_SERVER_RESPONSE_BODY_SIZE`
12+
- `HTTP_SERVER_DURATION_METRIC` now uses `semconv::metric::HTTP_SERVER_REQUEST_DURATION`
13+
* Update attribute keys to use semantic conventions constants:
14+
- `NETWORK_PROTOCOL_NAME_LABEL` now uses `semconv::attribute::NETWORK_PROTOCOL_NAME`
15+
- `HTTP_REQUEST_METHOD_LABEL` now uses `semconv::attribute::HTTP_REQUEST_METHOD`
16+
- `HTTP_ROUTE_LABEL` now uses `semconv::attribute::HTTP_ROUTE`
17+
- `HTTP_RESPONSE_STATUS_CODE_LABEL` now uses `semconv::attribute::HTTP_RESPONSE_STATUS_CODE`
18+
19+
### Added
20+
21+
* Add comprehensive test coverage for all HTTP server metrics with attribute validation
22+
23+
## v0.16.0
24+
25+
Initial release of OpenTelemetry Tower instrumentation middleware for HTTP metrics collection.
26+
27+
### Added
28+
29+
* HTTP server metrics middleware for Tower-compatible services
30+
* Support for Axum framework via `axum` feature flag
31+
* Metrics collection for:
32+
- `http.server.request.duration` - Request duration histogram
33+
- `http.server.active_requests` - Active requests counter
34+
- `http.server.request.body.size` - Request body size histogram
35+
- `http.server.response.body.size` - Response body size histogram
36+
* Configurable request duration histogram boundaries
37+
* Custom request and response attribute extractors
38+
* Automatic protocol version, HTTP method, URL scheme, and status code labeling
39+
* Route extraction for Axum applications

opentelemetry-instrumentation-tower/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ futures-util = { version = "0.3", default-features = false }
2222
http = { version = "1", features = ["std"], default-features = false }
2323
http-body = { version = "1", default-features = false }
2424
opentelemetry = { workspace = true, features = ["futures", "metrics"]}
25+
opentelemetry-semantic-conventions = { workspace = true, features = ["semconv_experimental"] }
2526
pin-project-lite = { version = "0.2", default-features = false }
2627
tower-service = { version = "0.3", default-features = false }
2728
tower-layer = { version = "0.3", default-features = false }
2829

2930
[dev-dependencies]
31+
opentelemetry_sdk = { workspace = true, features = ["metrics", "testing"] }
32+
tokio = { version = "1.0", features = ["macros", "rt"] }
33+
tower = { version = "0.5", features = ["util"] }
3034

3135
[lints]
32-
workspace = true
36+
workspace = true

opentelemetry-instrumentation-tower/src/lib.rs

Lines changed: 266 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ use axum::extract::MatchedPath;
1313
use futures_util::ready;
1414
use opentelemetry::metrics::{Histogram, Meter, UpDownCounter};
1515
use opentelemetry::KeyValue;
16+
use opentelemetry_semantic_conventions as semconv;
1617
use pin_project_lite::pin_project;
1718
use tower_layer::Layer;
1819
use tower_service::Service;
1920

20-
const HTTP_SERVER_DURATION_METRIC: &str = "http.server.request.duration";
21+
const HTTP_SERVER_DURATION_METRIC: &str = semconv::metric::HTTP_SERVER_REQUEST_DURATION;
2122
const HTTP_SERVER_DURATION_UNIT: &str = "s";
2223

2324
const _OTEL_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES: [f64; 14] = [
@@ -31,23 +32,23 @@ const _OTEL_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES: [f64; 14] = [
3132
const LIBRARY_DEFAULT_HTTP_SERVER_DURATION_BOUNDARIES: [f64; 14] = [
3233
0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0,
3334
];
34-
const HTTP_SERVER_ACTIVE_REQUESTS_METRIC: &str = "http.server.active_requests";
35+
const HTTP_SERVER_ACTIVE_REQUESTS_METRIC: &str = semconv::metric::HTTP_SERVER_ACTIVE_REQUESTS;
3536
const HTTP_SERVER_ACTIVE_REQUESTS_UNIT: &str = "{request}";
3637

37-
const HTTP_SERVER_REQUEST_BODY_SIZE_METRIC: &str = "http.server.request.body.size";
38+
const HTTP_SERVER_REQUEST_BODY_SIZE_METRIC: &str = semconv::metric::HTTP_SERVER_REQUEST_BODY_SIZE;
3839
const HTTP_SERVER_REQUEST_BODY_SIZE_UNIT: &str = "By";
3940

40-
const HTTP_SERVER_RESPONSE_BODY_SIZE_METRIC: &str = "http.server.response.body.size";
41+
const HTTP_SERVER_RESPONSE_BODY_SIZE_METRIC: &str = semconv::metric::HTTP_SERVER_RESPONSE_BODY_SIZE;
4142
const HTTP_SERVER_RESPONSE_BODY_SIZE_UNIT: &str = "By";
4243

43-
const NETWORK_PROTOCOL_NAME_LABEL: &str = "network.protocol.name";
44+
const NETWORK_PROTOCOL_NAME_LABEL: &str = semconv::attribute::NETWORK_PROTOCOL_NAME;
4445
const NETWORK_PROTOCOL_VERSION_LABEL: &str = "network.protocol.version";
4546
const URL_SCHEME_LABEL: &str = "url.scheme";
4647

47-
const HTTP_REQUEST_METHOD_LABEL: &str = "http.request.method";
48-
#[allow(dead_code)] // cargo check is not smart
49-
const HTTP_ROUTE_LABEL: &str = "http.route";
50-
const HTTP_RESPONSE_STATUS_CODE_LABEL: &str = "http.response.status_code";
48+
const HTTP_REQUEST_METHOD_LABEL: &str = semconv::attribute::HTTP_REQUEST_METHOD;
49+
#[cfg(feature = "axum")]
50+
const HTTP_ROUTE_LABEL: &str = semconv::attribute::HTTP_ROUTE;
51+
const HTTP_RESPONSE_STATUS_CODE_LABEL: &str = semconv::attribute::HTTP_RESPONSE_STATUS_CODE;
5152

5253
/// Trait for extracting custom attributes from HTTP requests
5354
pub trait RequestAttributeExtractor<B>: Clone + Send + Sync + 'static {
@@ -505,3 +506,259 @@ fn split_and_format_protocol_version(http_version: http::Version) -> (String, St
505506
};
506507
(String::from("http"), String::from(version_str))
507508
}
509+
510+
#[cfg(test)]
511+
mod tests {
512+
use super::*;
513+
use http::{Request, Response, StatusCode};
514+
use opentelemetry::metrics::MeterProvider;
515+
use opentelemetry_sdk::metrics::{
516+
data::{AggregatedMetrics, MetricData},
517+
InMemoryMetricExporter, PeriodicReader, SdkMeterProvider,
518+
};
519+
use std::time::Duration;
520+
use tower::Service;
521+
522+
#[tokio::test]
523+
async fn test_metrics_labels() {
524+
let exporter = InMemoryMetricExporter::default();
525+
let reader = PeriodicReader::builder(exporter.clone())
526+
.with_interval(Duration::from_millis(100))
527+
.build();
528+
let meter_provider = SdkMeterProvider::builder().with_reader(reader).build();
529+
let meter = meter_provider.meter("test");
530+
531+
let layer = HTTPMetricsLayerBuilder::builder()
532+
.with_meter(meter)
533+
.build()
534+
.unwrap();
535+
536+
let service = tower::service_fn(|_req: Request<String>| async {
537+
Ok::<_, std::convert::Infallible>(
538+
Response::builder()
539+
.status(StatusCode::OK)
540+
.body(String::from("Hello, World!"))
541+
.unwrap(),
542+
)
543+
});
544+
545+
let mut service = layer.layer(service);
546+
547+
let request = Request::builder()
548+
.method("GET")
549+
.uri("https://example.com/test")
550+
.body("test body".to_string())
551+
.unwrap();
552+
553+
let _response = service.call(request).await.unwrap();
554+
555+
tokio::time::sleep(Duration::from_millis(200)).await;
556+
557+
let metrics = exporter.get_finished_metrics().unwrap();
558+
assert!(!metrics.is_empty());
559+
560+
let resource_metrics = &metrics[0];
561+
let scope_metrics = resource_metrics
562+
.scope_metrics()
563+
.next()
564+
.expect("Should have scope metrics");
565+
566+
let duration_metric = scope_metrics
567+
.metrics()
568+
.find(|m| m.name() == HTTP_SERVER_DURATION_METRIC)
569+
.expect("Duration metric should exist");
570+
571+
if let AggregatedMetrics::F64(MetricData::Histogram(histogram)) = duration_metric.data() {
572+
let data_point = histogram
573+
.data_points()
574+
.next()
575+
.expect("Should have data point");
576+
let attributes: Vec<_> = data_point.attributes().collect();
577+
578+
// Duration metric should have 5 attributes: protocol_name, protocol_version, url_scheme, method, status_code
579+
assert_eq!(
580+
attributes.len(),
581+
5,
582+
"Duration metric should have exactly 5 attributes"
583+
);
584+
585+
let protocol_name = attributes
586+
.iter()
587+
.find(|kv| kv.key.as_str() == NETWORK_PROTOCOL_NAME_LABEL)
588+
.expect("Protocol name should be present");
589+
assert_eq!(protocol_name.value.as_str(), "http");
590+
591+
let protocol_version = attributes
592+
.iter()
593+
.find(|kv| kv.key.as_str() == NETWORK_PROTOCOL_VERSION_LABEL)
594+
.expect("Protocol version should be present");
595+
assert_eq!(protocol_version.value.as_str(), "1.1");
596+
597+
let url_scheme = attributes
598+
.iter()
599+
.find(|kv| kv.key.as_str() == URL_SCHEME_LABEL)
600+
.expect("URL scheme should be present");
601+
assert_eq!(url_scheme.value.as_str(), "https");
602+
603+
let method = attributes
604+
.iter()
605+
.find(|kv| kv.key.as_str() == HTTP_REQUEST_METHOD_LABEL)
606+
.expect("HTTP method should be present");
607+
assert_eq!(method.value.as_str(), "GET");
608+
609+
let status_code = attributes
610+
.iter()
611+
.find(|kv| kv.key.as_str() == HTTP_RESPONSE_STATUS_CODE_LABEL)
612+
.expect("Status code should be present");
613+
if let opentelemetry::Value::I64(code) = &status_code.value {
614+
assert_eq!(*code, 200);
615+
} else {
616+
panic!("Expected i64 status code");
617+
}
618+
} else {
619+
panic!("Expected histogram data for duration metric");
620+
}
621+
622+
let request_body_size_metric = scope_metrics
623+
.metrics()
624+
.find(|m| m.name() == HTTP_SERVER_REQUEST_BODY_SIZE_METRIC);
625+
626+
if let Some(metric) = request_body_size_metric {
627+
if let AggregatedMetrics::F64(MetricData::Histogram(histogram)) = metric.data() {
628+
let data_point = histogram
629+
.data_points()
630+
.next()
631+
.expect("Should have data point");
632+
let attributes: Vec<_> = data_point.attributes().collect();
633+
634+
// Request body size metric should have 5 attributes: protocol_name, protocol_version, url_scheme, method, status_code
635+
assert_eq!(
636+
attributes.len(),
637+
5,
638+
"Request body size metric should have exactly 5 attributes"
639+
);
640+
641+
let protocol_name = attributes
642+
.iter()
643+
.find(|kv| kv.key.as_str() == NETWORK_PROTOCOL_NAME_LABEL)
644+
.expect("Protocol name should be present in request body size");
645+
assert_eq!(protocol_name.value.as_str(), "http");
646+
647+
let protocol_version = attributes
648+
.iter()
649+
.find(|kv| kv.key.as_str() == NETWORK_PROTOCOL_VERSION_LABEL)
650+
.expect("Protocol version should be present in request body size");
651+
assert_eq!(protocol_version.value.as_str(), "1.1");
652+
653+
let url_scheme = attributes
654+
.iter()
655+
.find(|kv| kv.key.as_str() == URL_SCHEME_LABEL)
656+
.expect("URL scheme should be present in request body size");
657+
assert_eq!(url_scheme.value.as_str(), "https");
658+
659+
let method = attributes
660+
.iter()
661+
.find(|kv| kv.key.as_str() == HTTP_REQUEST_METHOD_LABEL)
662+
.expect("HTTP method should be present in request body size");
663+
assert_eq!(method.value.as_str(), "GET");
664+
665+
let status_code = attributes
666+
.iter()
667+
.find(|kv| kv.key.as_str() == HTTP_RESPONSE_STATUS_CODE_LABEL)
668+
.expect("Status code should be present in request body size");
669+
if let opentelemetry::Value::I64(code) = &status_code.value {
670+
assert_eq!(*code, 200);
671+
} else {
672+
panic!("Expected i64 status code");
673+
}
674+
}
675+
}
676+
677+
// Test response body size metric
678+
let response_body_size_metric = scope_metrics
679+
.metrics()
680+
.find(|m| m.name() == HTTP_SERVER_RESPONSE_BODY_SIZE_METRIC);
681+
682+
if let Some(metric) = response_body_size_metric {
683+
if let AggregatedMetrics::F64(MetricData::Histogram(histogram)) = metric.data() {
684+
let data_point = histogram
685+
.data_points()
686+
.next()
687+
.expect("Should have data point");
688+
let attributes: Vec<_> = data_point.attributes().collect();
689+
690+
// Response body size metric should have 5 attributes: protocol_name, protocol_version, url_scheme, method, status_code
691+
assert_eq!(
692+
attributes.len(),
693+
5,
694+
"Response body size metric should have exactly 5 attributes"
695+
);
696+
697+
let protocol_name = attributes
698+
.iter()
699+
.find(|kv| kv.key.as_str() == NETWORK_PROTOCOL_NAME_LABEL)
700+
.expect("Protocol name should be present in response body size");
701+
assert_eq!(protocol_name.value.as_str(), "http");
702+
703+
let protocol_version = attributes
704+
.iter()
705+
.find(|kv| kv.key.as_str() == NETWORK_PROTOCOL_VERSION_LABEL)
706+
.expect("Protocol version should be present in response body size");
707+
assert_eq!(protocol_version.value.as_str(), "1.1");
708+
709+
let url_scheme = attributes
710+
.iter()
711+
.find(|kv| kv.key.as_str() == URL_SCHEME_LABEL)
712+
.expect("URL scheme should be present in response body size");
713+
assert_eq!(url_scheme.value.as_str(), "https");
714+
715+
let method = attributes
716+
.iter()
717+
.find(|kv| kv.key.as_str() == HTTP_REQUEST_METHOD_LABEL)
718+
.expect("HTTP method should be present in response body size");
719+
assert_eq!(method.value.as_str(), "GET");
720+
721+
let status_code = attributes
722+
.iter()
723+
.find(|kv| kv.key.as_str() == HTTP_RESPONSE_STATUS_CODE_LABEL)
724+
.expect("Status code should be present in response body size");
725+
if let opentelemetry::Value::I64(code) = &status_code.value {
726+
assert_eq!(*code, 200);
727+
} else {
728+
panic!("Expected i64 status code");
729+
}
730+
}
731+
}
732+
733+
// Test active requests metric
734+
let active_requests_metric = scope_metrics
735+
.metrics()
736+
.find(|m| m.name() == HTTP_SERVER_ACTIVE_REQUESTS_METRIC);
737+
738+
if let Some(metric) = active_requests_metric {
739+
if let AggregatedMetrics::I64(MetricData::Sum(sum)) = metric.data() {
740+
let data_point = sum.data_points().next().expect("Should have data point");
741+
let attributes: Vec<_> = data_point.attributes().collect();
742+
743+
// Active requests metric should have 2 attributes: method, url_scheme
744+
assert_eq!(
745+
attributes.len(),
746+
2,
747+
"Active requests metric should have exactly 2 attributes"
748+
);
749+
750+
let method = attributes
751+
.iter()
752+
.find(|kv| kv.key.as_str() == HTTP_REQUEST_METHOD_LABEL)
753+
.expect("HTTP method should be present in active requests");
754+
assert_eq!(method.value.as_str(), "GET");
755+
756+
let url_scheme = attributes
757+
.iter()
758+
.find(|kv| kv.key.as_str() == URL_SCHEME_LABEL)
759+
.expect("URL scheme should be present in active requests");
760+
assert_eq!(url_scheme.value.as_str(), "https");
761+
}
762+
}
763+
}
764+
}

0 commit comments

Comments
 (0)