Files
jcoffey-dev 1e7e8dfd48
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 7m26s
Vendor upstream's opentelemetry-rust fork at the pinned commit
2026-10-27 22:55:35 -07:00

266 lines
12 KiB
Rust

use std::collections::HashMap;
use std::path::Path;
use tempfile::TempDir;
const TONIC_OUT_DIR: &str = "src/proto/tonic";
const TONIC_PROTO_FILES: &[&str] = &[
"src/proto/opentelemetry-proto/opentelemetry/proto/common/v1/common.proto",
"src/proto/opentelemetry-proto/opentelemetry/proto/resource/v1/resource.proto",
"src/proto/opentelemetry-proto/opentelemetry/proto/trace/v1/trace.proto",
"src/proto/opentelemetry-proto/opentelemetry/proto/collector/trace/v1/trace_service.proto",
"src/proto/opentelemetry-proto/opentelemetry/proto/metrics/v1/metrics.proto",
"src/proto/opentelemetry-proto/opentelemetry/proto/collector/metrics/v1/metrics_service.proto",
"src/proto/opentelemetry-proto/opentelemetry/proto/logs/v1/logs.proto",
"src/proto/opentelemetry-proto/opentelemetry/proto/collector/logs/v1/logs_service.proto",
"src/proto/opentelemetry-proto/opentelemetry/proto/profiles/v1development/profiles.proto",
"src/proto/opentelemetry-proto/opentelemetry/proto/collector/profiles/v1development/profiles_service.proto",
"src/proto/tracez.proto",
];
const TONIC_INCLUDES: &[&str] = &["src/proto/opentelemetry-proto", "src/proto"];
#[test]
fn build_tonic() {
let before_build = build_content_map(TONIC_OUT_DIR, true);
let out_dir = TempDir::new().expect("failed to create temp dir to store the generated files");
// build the generated files into OUT_DIR for now so we don't have to touch the src unless we have to
let mut builder = tonic_prost_build::configure()
.build_server(true)
.build_client(true)
.server_mod_attribute(".", "#[cfg(feature = \"gen-tonic\")]")
.client_mod_attribute(".", "#[cfg(feature = \"gen-tonic\")]")
.type_attribute(
".",
"#[cfg_attr(feature = \"with-schemars\", derive(schemars::JsonSchema))]",
)
.type_attribute(
".",
"#[cfg_attr(feature = \"with-serde\", derive(serde::Serialize, serde::Deserialize))]",
)
.type_attribute(
".",
"#[cfg_attr(feature = \"with-serde\", serde(rename_all = \"camelCase\"))]",
);
// Optional numeric, string and array fields need to default to their default value otherwise
// JSON files without those field cannot deserialize
// we cannot add serde(default) to all generated types because enums cannot be annotated with serde(default)
for path in [
"collector.trace.v1.ExportTraceServiceRequest",
"collector.logs.v1.ExportLogsServiceRequest",
"collector.metrics.v1.ExportMetricsServiceRequest",
"collector.profiles.v1development.ExportProfilesServiceRequest",
"collector.trace.v1.ExportTracePartialSuccess",
"collector.logs.v1.ExportLogsPartialSuccess",
"collector.metrics.v1.ExportMetricsPartialSuccess",
"collector.profiles.v1development.ExportProfilesPartialSuccess",
"trace.v1.Span",
"trace.v1.Span.Link",
"trace.v1.ScopeSpans",
"trace.v1.ResourceSpans",
"common.v1.InstrumentationScope",
"resource.v1.Resource",
"trace.v1.Span.Event",
"trace.v1.Status",
"logs.v1.LogRecord",
"logs.v1.ScopeLogs",
"logs.v1.ResourceLogs",
"metrics.v1.Metric",
"metrics.v1.ResourceMetrics",
"metrics.v1.ScopeMetrics",
"metrics.v1.Gauge",
"metrics.v1.Sum",
"metrics.v1.Histogram",
"metrics.v1.ExponentialHistogram",
"metrics.v1.Summary",
"metrics.v1.NumberDataPoint",
"metrics.v1.HistogramDataPoint",
"metrics.v1.SummaryDataPoint",
"profiles.v1development.Function",
] {
builder = builder.type_attribute(
path,
"#[cfg_attr(feature = \"with-serde\", serde(default))]",
)
}
// special serializer and deserializer for traceId and spanId
// OTLP/JSON format uses hex string for traceId and spanId
// the proto file uses bytes for traceId and spanId
// Thus, special serializer and deserializer are needed
for path in [
"trace.v1.Span.trace_id",
"trace.v1.Span.span_id",
"trace.v1.Span.parent_span_id",
"trace.v1.Span.Link.trace_id",
"trace.v1.Span.Link.span_id",
"logs.v1.LogRecord.span_id",
"logs.v1.LogRecord.trace_id",
"metrics.v1.Exemplar.span_id",
"metrics.v1.Exemplar.trace_id",
"profiles.v1development.Profile.profile_id",
] {
builder = builder
.field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_to_hex_string\", deserialize_with = \"crate::proto::serializers::deserialize_from_hex_string\"))]")
}
// key_strindex is a profiling-only reference field added to common.v1.KeyValue.
// For non-profiles signals we keep the existing JSON shape by defaulting it on input
// and omitting it from output when it is zero.
builder = builder.field_attribute(
"common.v1.KeyValue.key_strindex",
"#[cfg_attr(feature = \"with-serde\", serde(default, skip_serializing_if = \"crate::proto::serializers::is_default\"))]",
);
// special serializer and deserializer for timestamp
// OTLP/JSON format may use string for timestamp
// the proto file uses u64 for timestamp
// Thus, special serializer and deserializer are needed
for path in [
//trace
"trace.v1.Span.start_time_unix_nano",
"trace.v1.Span.end_time_unix_nano",
"trace.v1.Span.Event.time_unix_nano",
//logs
"logs.v1.LogRecord.time_unix_nano",
"logs.v1.LogRecord.observed_time_unix_nano",
//metrics
"metrics.v1.HistogramDataPoint.start_time_unix_nano",
"metrics.v1.HistogramDataPoint.time_unix_nano",
"metrics.v1.NumberDataPoint.start_time_unix_nano",
"metrics.v1.NumberDataPoint.time_unix_nano",
"metrics.v1.ExponentialHistogramDataPoint.start_time_unix_nano",
"metrics.v1.ExponentialHistogramDataPoint.time_unix_nano",
"metrics.v1.SummaryDataPoint.start_time_unix_nano",
"metrics.v1.SummaryDataPoint.time_unix_nano",
"metrics.v1.Exemplar.time_unix_nano",
] {
builder = builder
.field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_string_to_u64\"))]")
}
for path in ["profiles.v1development.Profile.time_nanos"] {
builder = builder
.field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_i64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_string_to_i64\"))]")
}
for path in ["profiles.v1development.Sample.timestamps_unix_nano"] {
builder = builder
.field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_vec_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_vec_string_to_vec_u64\"))]")
}
// special serializer and deserializer for metrics count
// OTLP/JSON format may use string for count
// the proto file uses u64 for count
// Thus, special serializer and deserializer are needed
for path in [
// metrics count and bucket fields
"metrics.v1.HistogramDataPoint.count",
"metrics.v1.ExponentialHistogramDataPoint.count",
"metrics.v1.ExponentialHistogramDataPoint.zero_count",
"metrics.v1.SummaryDataPoint.count",
] {
builder = builder.field_attribute(
path,
"#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_string_to_u64\"))]",
);
}
// special serializer and deserializer for metrics bucket counts
// OTLP/JSON format may use string for bucket counts
// the proto file uses u64 for bucket counts
// Thus, special serializer and deserializer are needed
for path in [
"metrics.v1.HistogramDataPoint.bucket_counts",
"metrics.v1.ExponentialHistogramDataPoint.Buckets.bucket_counts",
] {
builder = builder.field_attribute(
path,
"#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_vec_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_vec_string_to_vec_u64\"))]",
);
}
// Special handling for floating-point fields that might contain NaN, Infinity, or -Infinity
// TODO: More needs to be added here as we find more fields that need this special handling
for path in [
// metrics
"metrics.v1.SummaryDataPoint.ValueAtQuantile.value",
"metrics.v1.SummaryDataPoint.ValueAtQuantile.quantile",
] {
builder = builder.field_attribute(
path,
"#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_f64_special\", deserialize_with = \"crate::proto::serializers::deserialize_f64_special\"))]",
);
}
// special serializer and deserializer for value
// The Value::value field must be hidden
builder = builder
.field_attribute("common.v1.AnyValue.value", "#[cfg_attr(feature =\"with-serde\", serde(flatten, serialize_with = \"crate::proto::serializers::serialize_to_value\", deserialize_with = \"crate::proto::serializers::deserialize_from_value\"))]");
// flatten
for path in ["metrics.v1.Metric.data", "metrics.v1.NumberDataPoint.value"] {
builder =
builder.field_attribute(path, "#[cfg_attr(feature =\"with-serde\", serde(flatten))]");
}
builder
.out_dir(out_dir.path())
.compile_protos(TONIC_PROTO_FILES, TONIC_INCLUDES)
.expect("cannot compile protobuf using tonic");
let after_build = build_content_map(out_dir.path(), true);
ensure_files_are_same(before_build, after_build, TONIC_OUT_DIR);
}
fn build_content_map(path: impl AsRef<Path>, normalize_line_feed: bool) -> HashMap<String, String> {
std::fs::read_dir(path)
.expect("cannot open dictionary of generated files")
.flatten()
.map(|entry| {
let path = entry.path();
let file_name = path
.file_name()
.expect("file name should always exist for generated files");
let mut file_contents = std::fs::read_to_string(path.clone())
.expect("cannot read from existing generated file");
if normalize_line_feed {
file_contents = get_platform_specific_string(file_contents);
}
(file_name.to_string_lossy().to_string(), file_contents)
})
.collect()
}
/// Returns a String which uses the platform specific new line feed character.
fn get_platform_specific_string(input: String) -> String {
if cfg!(windows) && !input.ends_with("\r\n") && input.ends_with('\n') {
return input.replace('\n', "\r\n");
}
input
}
fn ensure_files_are_same(
before_build: HashMap<String, String>,
after_build: HashMap<String, String>,
target_dir: &'static str,
) {
if after_build == before_build {
return;
}
if std::env::var("CI").is_ok() {
panic!("generated file has changed but it's a CI environment, please rerun this test locally and commit the changes");
}
// if there is at least one changes we will just copy the whole directory over
for (file_name, content) in after_build {
std::fs::write(Path::new(target_dir).join(file_name), content)
.expect("cannot write to the proto generate file. If it's happening in CI env, please return the test locally and commit the change");
}
panic!("generated file has changed, please commit the change file and rerun the test");
}