mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
Support managed OpenTelemetry metadata and PEM client identities (#57658)
## Why Managed OpenTelemetry requirements could not specify span attributes or tracestate metadata. ## What changed - Accept `otel.span_attributes` and `otel.tracestate` in requirements, merge them by attribute or member field, and expose them through app-server configuration and requirements reads. - Protect required entries and their ancestors from configuration writes while preserving unmanaged sibling values and their origins. For example, requiring `otel.tracestate.vendor.tenant` leaves `vendor.region` writable. Empty metadata maps impose no requirements. - Select Rustls when configuring an OTLP HTTP client certificate and private key so PEM client identities are accepted. ## Testing Add coverage for metadata precedence, effective configuration, field-level write protection, and preservation of unmanaged values. Add blocking and asynchronous HTTP client construction tests using generated PEM certificates and keys. GitOrigin-RevId: ada6000cc5e575b9a33451ec4aa58b6686dc20f7
This commit is contained in:
Generated
+2
@@ -4393,11 +4393,13 @@ dependencies = [
|
||||
"opentelemetry_sdk",
|
||||
"os_info",
|
||||
"pretty_assertions",
|
||||
"rcgen",
|
||||
"reqwest 0.12.28",
|
||||
"seccompiler",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum_macros 0.28.0",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
|
||||
+21
@@ -16678,6 +16678,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"span_attributes": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tool_result": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -16697,6 +16706,18 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tracestate": {
|
||||
"additionalProperties": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
+21
@@ -12670,6 +12670,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"span_attributes": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tool_result": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -12689,6 +12698,18 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tracestate": {
|
||||
"additionalProperties": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
+21
@@ -1294,6 +1294,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"span_attributes": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tool_result": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1313,6 +1322,18 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tracestate": {
|
||||
"additionalProperties": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -4,4 +4,4 @@
|
||||
import type { OtelExporter } from "./OtelExporter";
|
||||
import type { OtelToolResultLogConfig } from "./OtelToolResultLogConfig";
|
||||
|
||||
export type OtelRequirements = { tool_result: OtelToolResultLogConfig | null, log_user_prompt: boolean | null, log_agent_responses: boolean | null, log_guardian_assessments: boolean | null, environment: string | null, exporter: OtelExporter | null, trace_exporter: OtelExporter | null, metrics_exporter: OtelExporter | null, };
|
||||
export type OtelRequirements = { tool_result: OtelToolResultLogConfig | null, log_user_prompt: boolean | null, log_agent_responses: boolean | null, log_guardian_assessments: boolean | null, environment: string | null, exporter: OtelExporter | null, trace_exporter: OtelExporter | null, metrics_exporter: OtelExporter | null, span_attributes: { [key in string]?: string } | null, tracestate: { [key in string]?: { [key in string]?: string } } | null, };
|
||||
|
||||
@@ -513,6 +513,8 @@ pub struct OtelRequirements {
|
||||
pub exporter: Option<OtelExporter>,
|
||||
pub trace_exporter: Option<OtelExporter>,
|
||||
pub metrics_exporter: Option<OtelExporter>,
|
||||
pub span_attributes: Option<HashMap<String, String>>,
|
||||
pub tracestate: Option<BTreeMap<String, BTreeMap<String, String>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
|
||||
@@ -711,3 +711,11 @@ managed fields. Configuration writes that overlap managed settings are rejected.
|
||||
|
||||
Exporter header values are redacted in effective config, source layers, managed
|
||||
requirements, and write-override metadata. Runtime exporters retain the original values.
|
||||
|
||||
Managed span attributes and tracestate metadata are included in effective config
|
||||
and requirements reads. Unmanaged metadata remains writable.
|
||||
|
||||
Tracestate requirements merge individual fields within each member. For example,
|
||||
requiring `otel.tracestate.vendor.tenant` preserves an ordinary `vendor.region`.
|
||||
Only required fields and edits to their ancestors are protected; sibling fields
|
||||
retain their origins and remain writable.
|
||||
|
||||
@@ -1722,7 +1722,9 @@ log_agent_responses = true
|
||||
log_guardian_assessments = false
|
||||
exporter = "none"
|
||||
trace_exporter = "none"
|
||||
metrics_exporter = "none""#,
|
||||
metrics_exporter = "none"
|
||||
span_attributes = { "service.name" = "managed-service" }
|
||||
tracestate = { managed = { tenant = "enterprise" }, empty = {} }"#,
|
||||
),
|
||||
);
|
||||
for (key_path, managed) in [
|
||||
@@ -1733,8 +1735,14 @@ metrics_exporter = "none""#,
|
||||
("otel.exporter.otlp-http.endpoint", true),
|
||||
("otel.trace_exporter.otlp-grpc.endpoint", true),
|
||||
("otel.metrics_exporter.otlp-http.headers", true),
|
||||
("otel.span_attributes.\"service.name\"", true),
|
||||
("otel.tracestate.managed.tenant", true),
|
||||
("otel.environment", false),
|
||||
("otel.tool_result.max_bytes", true),
|
||||
("otel.span_attributes.\"service.version\"", false),
|
||||
("otel.tracestate.local.tenant", false),
|
||||
("otel.tracestate.managed.region", false),
|
||||
("otel.tracestate.empty.tenant", false),
|
||||
] {
|
||||
let result = service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
@@ -1829,7 +1837,9 @@ async fn read_projects_managed_otel_values_and_preserves_unmanaged_settings() {
|
||||
tool_result = { max_bytes = 1234 }
|
||||
log_user_prompt = true
|
||||
environment = "user"
|
||||
exporter = "statsig""#,
|
||||
exporter = "statsig"
|
||||
span_attributes = { "service.name" = "user-service", "service.version" = "user-version" }
|
||||
tracestate = { managed = { tenant = "user-tenant", region = "us" }, local = { tenant = "preserved" }, empty = { tenant = "also-preserved" } }"#,
|
||||
)
|
||||
.expect("write user config");
|
||||
let service = ConfigManager::new_for_tests(
|
||||
@@ -1839,7 +1849,9 @@ exporter = "statsig""#,
|
||||
CloudConfigBundleFixture::loader_with_enterprise_requirement(
|
||||
r#"[otel]
|
||||
log_user_prompt = false
|
||||
exporter = "none""#,
|
||||
exporter = "none"
|
||||
span_attributes = { "service.name" = "managed-service" }
|
||||
tracestate = { managed = { tenant = "enterprise" }, empty = {} }"#,
|
||||
),
|
||||
);
|
||||
let response = service
|
||||
@@ -1865,15 +1877,28 @@ exporter = "none""#,
|
||||
"exporter": "none",
|
||||
"trace_exporter": null,
|
||||
"metrics_exporter": null,
|
||||
"span_attributes": null,
|
||||
"tracestate": null,
|
||||
"span_attributes": {
|
||||
"service.name": "managed-service",
|
||||
"service.version": "user-version",
|
||||
},
|
||||
"tracestate": {
|
||||
"managed": { "tenant": "enterprise", "region": "us" },
|
||||
"local": { "tenant": "preserved" },
|
||||
"empty": { "tenant": "also-preserved" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
for (path, user_owned) in [
|
||||
("otel.log_user_prompt", false),
|
||||
("otel.exporter", false),
|
||||
("otel.span_attributes.service.name", false),
|
||||
("otel.tracestate.managed.tenant", false),
|
||||
("otel.tracestate.managed.region", true),
|
||||
("otel.environment", true),
|
||||
("otel.tool_result.max_bytes", true),
|
||||
("otel.span_attributes.service.version", true),
|
||||
("otel.tracestate.local.tenant", true),
|
||||
("otel.tracestate.empty.tenant", true),
|
||||
] {
|
||||
assert_eq!(response.origins.contains_key(path), user_owned, "{path}");
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ pub(crate) fn map_otel_requirements_to_api(otel: OtelRequirementsToml) -> OtelRe
|
||||
exporter: otel.exporter.map(map_otel_exporter_to_api),
|
||||
trace_exporter: otel.trace_exporter.map(map_otel_exporter_to_api),
|
||||
metrics_exporter: otel.metrics_exporter.map(map_otel_exporter_to_api),
|
||||
span_attributes: otel.span_attributes,
|
||||
tracestate: otel.tracestate,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -599,6 +599,105 @@ async fn config_read_disables_guardian_v2_when_managed_config_requires_guardian_
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn managed_otel_tracestate_merges_fields_and_protects_only_required_fields() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_config(
|
||||
&codex_home,
|
||||
r#"[otel.tracestate.vendor]
|
||||
tenant = "user"
|
||||
region = "us"
|
||||
"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("requirements.toml"),
|
||||
"[otel.tracestate.vendor]\ntenant = \"enterprise\"\n",
|
||||
)?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
|
||||
let read_id = app_server
|
||||
.send_config_read_request(ConfigReadParams {
|
||||
include_layers: false,
|
||||
cwd: None,
|
||||
})
|
||||
.await?;
|
||||
let response: ConfigReadResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(read_id)).await??;
|
||||
assert_eq!(
|
||||
response.config.additional["otel"]["tracestate"],
|
||||
json!({ "vendor": { "tenant": "enterprise", "region": "us" } }),
|
||||
);
|
||||
assert!(
|
||||
!response
|
||||
.origins
|
||||
.contains_key("otel.tracestate.vendor.tenant")
|
||||
);
|
||||
assert!(
|
||||
response
|
||||
.origins
|
||||
.contains_key("otel.tracestate.vendor.region")
|
||||
);
|
||||
|
||||
let write_id = app_server
|
||||
.send_config_value_write_request(ConfigValueWriteParams {
|
||||
file_path: None,
|
||||
key_path: "otel.tracestate.vendor.region".to_string(),
|
||||
value: json!("eu"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version: None,
|
||||
})
|
||||
.await?;
|
||||
let _: ConfigWriteResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(write_id)).await??;
|
||||
|
||||
for key_path in [
|
||||
"otel.tracestate.vendor.tenant",
|
||||
"otel.tracestate.vendor",
|
||||
"otel.tracestate",
|
||||
] {
|
||||
let write_id = app_server
|
||||
.send_config_value_write_request(ConfigValueWriteParams {
|
||||
file_path: None,
|
||||
key_path: key_path.to_string(),
|
||||
value: json!("user"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version: None,
|
||||
})
|
||||
.await?;
|
||||
let error: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
app_server.read_stream_until_error_message(RequestId::Integer(write_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
error
|
||||
.error
|
||||
.data
|
||||
.as_ref()
|
||||
.and_then(|data| data.get("config_write_error_code")),
|
||||
Some(&json!("configRequirementReadonly")),
|
||||
"{key_path}",
|
||||
);
|
||||
}
|
||||
let read_id = app_server
|
||||
.send_config_read_request(ConfigReadParams {
|
||||
include_layers: false,
|
||||
cwd: None,
|
||||
})
|
||||
.await?;
|
||||
let response: ConfigReadResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(read_id)).await??;
|
||||
assert_eq!(
|
||||
response.config.additional["otel"]["tracestate"],
|
||||
json!({ "vendor": { "tenant": "enterprise", "region": "eu" } }),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case::test_case("json", "none"; "json_and_disabled_metrics")]
|
||||
#[test_case::test_case("binary", "statsig"; "binary_and_statsig_metrics")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -623,7 +722,9 @@ log_guardian_assessments = false
|
||||
environment = "managed"
|
||||
exporter = { otlp-http = { endpoint = "http://127.0.0.1:1/v1/logs", protocol = "$PROTOCOL", headers = { Authorization = "Bearer managed-token" }, tls = { ca-certificate = "ca.pem", client-certificate = "client.pem", client-private-key = "client-key.pem" } } }
|
||||
trace_exporter = { otlp-grpc = { endpoint = "http://127.0.0.1:4317", headers = { "x-api-key" = "managed-grpc-secret" }, tls = { ca-certificate = "ca.pem" } } }
|
||||
metrics_exporter = "$METRICS_EXPORTER""#
|
||||
metrics_exporter = "$METRICS_EXPORTER"
|
||||
span_attributes = { "service.name" = "managed-service" }
|
||||
tracestate = { managed = { tenant = "enterprise" } }"#
|
||||
.replace("$PROTOCOL", protocol)
|
||||
.replace("$METRICS_EXPORTER", metrics_exporter),
|
||||
)?;
|
||||
@@ -662,6 +763,8 @@ metrics_exporter = "$METRICS_EXPORTER""#
|
||||
},
|
||||
},
|
||||
"metrics_exporter": metrics_exporter,
|
||||
"span_attributes": { "service.name": "managed-service" },
|
||||
"tracestate": { "managed": { "tenant": "enterprise" } },
|
||||
})
|
||||
);
|
||||
// API clients must preserve certificate paths from servers on either OS.
|
||||
|
||||
@@ -120,7 +120,7 @@ fn build_trace_provider(endpoint: &str) -> anyhow::Result<OtelProvider> {
|
||||
},
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::new(),
|
||||
span_attributes: HashMap::new(),
|
||||
tracestate: BTreeMap::new(),
|
||||
})
|
||||
.map_err(|error| anyhow::anyhow!("failed to build code-mode host OTEL provider: {error}"))?
|
||||
|
||||
@@ -1122,6 +1122,8 @@ pub struct OtelRequirementsToml {
|
||||
pub exporter: Option<OtelExporterKind>,
|
||||
pub trace_exporter: Option<OtelExporterKind>,
|
||||
pub metrics_exporter: Option<OtelExporterKind>,
|
||||
pub span_attributes: Option<HashMap<String, String>>,
|
||||
pub tracestate: Option<BTreeMap<String, BTreeMap<String, String>>>,
|
||||
}
|
||||
|
||||
impl OtelRequirementsToml {
|
||||
@@ -1134,6 +1136,17 @@ impl OtelRequirementsToml {
|
||||
&& self.exporter.is_none()
|
||||
&& self.trace_exporter.is_none()
|
||||
&& self.metrics_exporter.is_none()
|
||||
&& !self.has_metadata()
|
||||
}
|
||||
|
||||
fn has_metadata(&self) -> bool {
|
||||
self.span_attributes
|
||||
.as_ref()
|
||||
.is_some_and(|fields| !fields.is_empty())
|
||||
|| self
|
||||
.tracestate
|
||||
.as_ref()
|
||||
.is_some_and(|members| members.values().any(|fields| !fields.is_empty()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1847,6 +1860,11 @@ impl ConfigRequirementsToml {
|
||||
.find_map(|(is_managed, managed_path, field)| {
|
||||
(is_managed && config_paths_overlap(segments, managed_path)).then_some(field)
|
||||
})
|
||||
.or_else(|| {
|
||||
otel.and_then(|otel| {
|
||||
exact_otel_metadata_requirement_for_config_path(otel, segments)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1862,6 +1880,15 @@ macro_rules! otel_merge_functions {
|
||||
requirements.get_or_insert_default().$field = Some(value.clone());
|
||||
contributed = true;
|
||||
})+
|
||||
if higher_priority.has_metadata() {
|
||||
let target = requirements.get_or_insert_default();
|
||||
merge_otel_metadata(
|
||||
&mut target.span_attributes,
|
||||
&mut target.tracestate,
|
||||
higher_priority,
|
||||
);
|
||||
contributed = true;
|
||||
}
|
||||
contributed
|
||||
}
|
||||
|
||||
@@ -1875,6 +1902,11 @@ macro_rules! otel_merge_functions {
|
||||
conflict |= config.$field.as_ref().is_some_and(|current| current != value);
|
||||
config.$field = Some(value.clone());
|
||||
})+
|
||||
conflict |= merge_otel_metadata(
|
||||
&mut config.span_attributes,
|
||||
&mut config.tracestate,
|
||||
requirements,
|
||||
);
|
||||
conflict
|
||||
}
|
||||
};
|
||||
@@ -1891,6 +1923,70 @@ otel_merge_functions!(
|
||||
metrics_exporter,
|
||||
);
|
||||
|
||||
fn merge_otel_metadata(
|
||||
span_attributes: &mut Option<HashMap<String, String>>,
|
||||
tracestate: &mut Option<BTreeMap<String, BTreeMap<String, String>>>,
|
||||
requirements: &OtelRequirementsToml,
|
||||
) -> bool {
|
||||
let mut conflict = false;
|
||||
if let Some(attributes) = &requirements.span_attributes
|
||||
&& !attributes.is_empty()
|
||||
{
|
||||
let configured = span_attributes.get_or_insert_default();
|
||||
for (key, value) in attributes {
|
||||
conflict |= configured
|
||||
.insert(key.clone(), value.clone())
|
||||
.is_some_and(|configured| configured != *value);
|
||||
}
|
||||
}
|
||||
if let Some(members) = &requirements.tracestate {
|
||||
for (member, fields) in members.iter().filter(|(_, fields)| !fields.is_empty()) {
|
||||
let configured = tracestate
|
||||
.get_or_insert_default()
|
||||
.entry(member.clone())
|
||||
.or_default();
|
||||
for (key, value) in fields {
|
||||
conflict |= configured
|
||||
.insert(key.clone(), value.clone())
|
||||
.is_some_and(|configured| configured != *value);
|
||||
}
|
||||
}
|
||||
}
|
||||
conflict
|
||||
}
|
||||
|
||||
fn exact_otel_metadata_requirement_for_config_path(
|
||||
requirements: &OtelRequirementsToml,
|
||||
segments: &[String],
|
||||
) -> Option<&'static str> {
|
||||
if requirements
|
||||
.span_attributes
|
||||
.as_ref()
|
||||
.is_some_and(|attributes| {
|
||||
attributes.keys().any(|key| {
|
||||
config_paths_overlap(segments, &["otel", "span_attributes", key.as_str()])
|
||||
})
|
||||
})
|
||||
{
|
||||
return Some("otel.span_attributes");
|
||||
}
|
||||
|
||||
requirements
|
||||
.tracestate
|
||||
.as_ref()
|
||||
.is_some_and(|members| {
|
||||
members.iter().any(|(member, fields)| {
|
||||
fields.keys().any(|field| {
|
||||
config_paths_overlap(
|
||||
segments,
|
||||
&["otel", "tracestate", member.as_str(), field.as_str()],
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.then_some("otel.tracestate")
|
||||
}
|
||||
|
||||
fn config_paths_overlap(segments: &[String], managed_path: &[&str]) -> bool {
|
||||
segments
|
||||
.iter()
|
||||
@@ -2471,9 +2567,16 @@ mod tests {
|
||||
#[test_case::test_case(&["otel", "exporter", "otlp-http", "endpoint"], Some("otel.exporter"); "exporter_endpoint")]
|
||||
#[test_case::test_case(&["otel", "trace_exporter"], Some("otel.trace_exporter"); "trace_exporter")]
|
||||
#[test_case::test_case(&["otel", "metrics_exporter"], Some("otel.metrics_exporter"); "metrics_exporter")]
|
||||
#[test_case::test_case(&["otel", "span_attributes", "service.name"], None; "unmanaged_span_attribute")]
|
||||
#[test_case::test_case(&["otel", "tracestate", "user"], None; "unmanaged_tracestate")]
|
||||
fn otel_requirements_project_and_lock_exact_scalar_and_exporter_values(
|
||||
#[test_case::test_case(&["otel", "span_attributes"], Some("otel.span_attributes"); "span_attributes")]
|
||||
#[test_case::test_case(&["otel", "span_attributes", "service.name"], Some("otel.span_attributes"); "span_attributes_service_name")]
|
||||
#[test_case::test_case(&["otel", "span_attributes", "other"], None; "span_attributes_other")]
|
||||
#[test_case::test_case(&["otel", "tracestate"], Some("otel.tracestate"); "tracestate")]
|
||||
#[test_case::test_case(&["otel", "tracestate", "managed", "tenant"], Some("otel.tracestate"); "tracestate_managed_tenant")]
|
||||
#[test_case::test_case(&["otel", "tracestate", "managed"], Some("otel.tracestate"); "tracestate_managed")]
|
||||
#[test_case::test_case(&["otel", "tracestate", "managed", "region"], None; "tracestate_managed_region")]
|
||||
#[test_case::test_case(&["otel", "tracestate", "other"], None; "tracestate_other")]
|
||||
#[test_case::test_case(&["otel", "tracestate", "empty"], None; "tracestate_empty")]
|
||||
fn otel_requirements_project_exact_values_and_lock_only_managed_map_entries(
|
||||
segments: &[&str],
|
||||
expected: Option<&str>,
|
||||
) -> Result<()> {
|
||||
@@ -2486,6 +2589,8 @@ environment = "managed"
|
||||
exporter = "none"
|
||||
trace_exporter = "none"
|
||||
metrics_exporter = "none"
|
||||
span_attributes = { "service.name" = "managed" }
|
||||
tracestate = { managed = { tenant = "enterprise" }, empty = {} }
|
||||
"#,
|
||||
)?;
|
||||
let mut configured: ConfigToml = from_str(
|
||||
@@ -2497,8 +2602,8 @@ environment = "user"
|
||||
exporter = "statsig"
|
||||
trace_exporter = { otlp-grpc = { endpoint = "https://user.example" } }
|
||||
metrics_exporter = "statsig"
|
||||
span_attributes = { "service.name" = "user" }
|
||||
tracestate = { user = { tenant = "preserved" } }
|
||||
span_attributes = { "service.name" = "user", other = "preserved" }
|
||||
tracestate = { managed = { tenant = "user", region = "us" }, other = { keep = "yes" } }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
@@ -2514,8 +2619,8 @@ environment = "managed"
|
||||
exporter = "none"
|
||||
trace_exporter = "none"
|
||||
metrics_exporter = "none"
|
||||
span_attributes = { "service.name" = "user" }
|
||||
tracestate = { user = { tenant = "preserved" } }
|
||||
span_attributes = { "service.name" = "managed", other = "preserved" }
|
||||
tracestate = { managed = { tenant = "enterprise", region = "us" }, other = { keep = "yes" } }
|
||||
"#
|
||||
)?
|
||||
);
|
||||
@@ -2527,11 +2632,17 @@ tracestate = { user = { tenant = "preserved" } }
|
||||
expected,
|
||||
);
|
||||
|
||||
let empty: ConfigRequirementsToml = from_str("[otel]")?;
|
||||
assert!(empty.is_empty());
|
||||
let mut configured = ConfigToml::default();
|
||||
empty.apply_exact_to_config(&mut configured);
|
||||
assert_eq!(configured.otel, None);
|
||||
for empty in [
|
||||
"[otel]",
|
||||
"[otel.span_attributes]",
|
||||
"[otel.tracestate.empty]",
|
||||
] {
|
||||
let requirements: ConfigRequirementsToml = from_str(empty)?;
|
||||
assert!(requirements.is_empty(), "{empty}");
|
||||
let mut configured = ConfigToml::default();
|
||||
requirements.apply_exact_to_config(&mut configured);
|
||||
assert_eq!(configured.otel, None, "{empty}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
//! - `rules.prefix_rules` append high-priority rules first.
|
||||
//! - `hooks` append high-priority event groups first while failing closed on
|
||||
//! active managed-dir conflicts.
|
||||
//! - `otel` replaces exact managed values, treating exporters atomically.
|
||||
//! - `otel` replaces exact managed values, treats exporters atomically, and
|
||||
//! merges span attributes and fields within each tracestate member by key.
|
||||
//! - `permissions.filesystem.deny_read` is a high-priority-first union across
|
||||
//! layers.
|
||||
//! - `auto_review.required_on_models` is a high-priority-first union across layers.
|
||||
|
||||
@@ -341,7 +341,7 @@ refresh_interval_ms = 12345
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn otel_requirements_compose_exact_values_and_atomic_exporters_with_relative_tls_paths() {
|
||||
fn otel_requirements_compose_exact_values_atomic_exporters_and_metadata() {
|
||||
let low_dir = tempdir().expect("low-priority requirements directory");
|
||||
let high_dir = tempdir().expect("high-priority requirements directory");
|
||||
let low_base = AbsolutePathBuf::from_absolute_path(low_dir.path()).expect("absolute low base");
|
||||
@@ -359,6 +359,8 @@ environment = "low"
|
||||
exporter = { otlp-http = { endpoint = "https://low.example", protocol = "json", headers = { stale = "low" }, tls = { ca-certificate = "low-ca.pem" } } }
|
||||
trace_exporter = { otlp-grpc = { endpoint = "https://low.example", tls = { client-certificate = "low-client.pem" } } }
|
||||
metrics_exporter = "statsig"
|
||||
span_attributes = { shared = "low", lower = "preserved" }
|
||||
tracestate = { shared = { tenant = "low", region = "us" }, lower = { tenant = "preserved" }, empty = {} }
|
||||
"#,
|
||||
)
|
||||
.with_base_dir(low_base);
|
||||
@@ -372,12 +374,22 @@ log_user_prompt = false
|
||||
environment = "high"
|
||||
exporter = { otlp-http = { endpoint = "https://high.example", protocol = "binary", tls = { ca-certificate = "high-ca.pem" } } }
|
||||
metrics_exporter = "none"
|
||||
span_attributes = { shared = "high", higher = "added" }
|
||||
tracestate = { shared = { tenant = "replacement" }, lower = {}, higher = { tenant = "added" } }
|
||||
"#,
|
||||
)
|
||||
.with_base_dir(high_base);
|
||||
let expected_source = RequirementSource::composite([high.source.clone(), low.source.clone()]);
|
||||
let composed = compose_requirements_for_hostname(
|
||||
vec![low, high, layer("empty", "Empty", "[otel]")],
|
||||
vec![
|
||||
low,
|
||||
high,
|
||||
layer(
|
||||
"empty",
|
||||
"Empty",
|
||||
"[otel]\nspan_attributes = {}\ntracestate = { lower = {} }",
|
||||
),
|
||||
],
|
||||
/*hostname*/ None,
|
||||
)
|
||||
.expect("compose OTEL requirements")
|
||||
@@ -394,6 +406,8 @@ environment = "high"
|
||||
exporter = { otlp-http = { endpoint = "https://high.example", protocol = "binary", tls = { ca-certificate = '$HIGH_CA' } } }
|
||||
trace_exporter = { otlp-grpc = { endpoint = "https://low.example", tls = { client-certificate = '$LOW_CLIENT' } } }
|
||||
metrics_exporter = "none"
|
||||
span_attributes = { shared = "high", lower = "preserved", higher = "added" }
|
||||
tracestate = { shared = { tenant = "replacement", region = "us" }, lower = { tenant = "preserved" }, higher = { tenant = "added" } }
|
||||
"#
|
||||
.replace(
|
||||
"$HIGH_CA",
|
||||
@@ -407,34 +421,25 @@ metrics_exporter = "none"
|
||||
composed.value,
|
||||
*expected_requirements(expected).otel.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
compose(vec![layer(
|
||||
"mixed",
|
||||
"Mixed",
|
||||
"[otel]\nexporter = 'none'\nspan_attributes = {}\ntracestate = { empty = {} }",
|
||||
)]),
|
||||
Ok(Some(expected_requirements("[otel]\nexporter = 'none'"))),
|
||||
);
|
||||
|
||||
assert_eq!(compose(vec![layer("empty", "Empty", "[otel]")]), Ok(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_otel_trace_metadata_is_rejected_with_its_requirements_source() {
|
||||
for field in ["span_attributes", "tracestate"] {
|
||||
let error = compose(vec![layer(
|
||||
"req_metadata",
|
||||
"Managed policy",
|
||||
&format!("[otel]\n{field} = {{}}"),
|
||||
)])
|
||||
.expect_err("metadata is unsupported");
|
||||
let RequirementsCompositionError::Parse {
|
||||
layer_source,
|
||||
message,
|
||||
} = error
|
||||
else {
|
||||
panic!("expected a layer parse error: {error:?}");
|
||||
};
|
||||
for empty in [
|
||||
"[otel]",
|
||||
"[otel.span_attributes]",
|
||||
"[otel.tracestate.empty]",
|
||||
] {
|
||||
assert_eq!(
|
||||
layer_source,
|
||||
RequirementSource::EnterpriseManaged {
|
||||
id: "req_metadata".to_string(),
|
||||
name: "Managed policy".to_string(),
|
||||
},
|
||||
compose(vec![layer("empty", "Empty", empty)]),
|
||||
Ok(None),
|
||||
"{empty}",
|
||||
);
|
||||
assert!(message.contains(&format!("unknown field `{field}`")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -662,7 +662,7 @@ pub struct OtelConfigToml {
|
||||
pub metrics_exporter: Option<OtelExporterKind>,
|
||||
|
||||
/// Attributes to add to every exported trace span.
|
||||
pub span_attributes: Option<BTreeMap<String, String>>,
|
||||
pub span_attributes: Option<HashMap<String, String>>,
|
||||
|
||||
/// Semicolon-separated `key:value` fields to upsert into W3C tracestate members.
|
||||
pub tracestate: Option<BTreeMap<String, BTreeMap<String, String>>>,
|
||||
@@ -679,7 +679,7 @@ pub struct OtelConfig {
|
||||
pub exporter: OtelExporterKind,
|
||||
pub trace_exporter: OtelExporterKind,
|
||||
pub metrics_exporter: OtelExporterKind,
|
||||
pub span_attributes: BTreeMap<String, String>,
|
||||
pub span_attributes: HashMap<String, String>,
|
||||
pub tracestate: BTreeMap<String, BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
@@ -694,7 +694,7 @@ impl Default for OtelConfig {
|
||||
exporter: OtelExporterKind::None,
|
||||
trace_exporter: OtelExporterKind::None,
|
||||
metrics_exporter: OtelExporterKind::Statsig,
|
||||
span_attributes: BTreeMap::new(),
|
||||
span_attributes: HashMap::new(),
|
||||
tracestate: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10400,7 +10400,7 @@ beta = "two"
|
||||
|
||||
assert_eq!(
|
||||
config.otel.span_attributes,
|
||||
BTreeMap::from([("example.trace_attr".to_string(), "enabled".to_string())])
|
||||
HashMap::from([("example.trace_attr".to_string(), "enabled".to_string())])
|
||||
);
|
||||
assert_eq!(
|
||||
config.otel.tracestate,
|
||||
@@ -10450,7 +10450,7 @@ alpha = "one\ntwo"
|
||||
assert_eq!(config.otel.environment, "test");
|
||||
assert_eq!(
|
||||
config.otel.span_attributes,
|
||||
BTreeMap::from([("example.trace_attr".to_string(), "enabled".to_string())])
|
||||
HashMap::from([("example.trace_attr".to_string(), "enabled".to_string())])
|
||||
);
|
||||
assert_eq!(
|
||||
config.otel.tracestate,
|
||||
@@ -13940,6 +13940,8 @@ environment = "production"
|
||||
exporter = { otlp-http = { endpoint = "https://managed.example/logs", protocol = "binary", headers = { Authorization = "managed-secret" } } }
|
||||
trace_exporter = "none"
|
||||
metrics_exporter = { otlp-grpc = { endpoint = "https://managed.example:4317" } }
|
||||
span_attributes = { "deployment.environment" = "production", team = "platform" }
|
||||
tracestate = { vendor = { tenant = "managed" }, managed = { required = "yes" } }
|
||||
"#,
|
||||
r#"
|
||||
[otel]
|
||||
@@ -13949,10 +13951,10 @@ environment = "production"
|
||||
exporter = { otlp-http = { endpoint = "https://managed.example/logs", protocol = "binary", headers = { Authorization = "managed-secret" } } }
|
||||
trace_exporter = "none"
|
||||
metrics_exporter = { otlp-grpc = { endpoint = "https://managed.example:4317" } }
|
||||
span_attributes = { "deployment.environment" = "user", machine = "laptop" }
|
||||
tracestate = { vendor = { tenant = "user", region = "us" }, other = { keep = "yes" } }
|
||||
span_attributes = { "deployment.environment" = "production", machine = "laptop", team = "platform" }
|
||||
tracestate = { vendor = { tenant = "managed", region = "us" }, other = { keep = "yes" }, managed = { required = "yes" } }
|
||||
"#,
|
||||
true; "managed_exact_values_and_user_metadata"
|
||||
true; "managed_exact_values_and_unmanaged_metadata"
|
||||
)]
|
||||
#[test_case::test_case(
|
||||
"[otel]\nenvironment = 'user'", "[otel]\ntool_result = { max_bytes = 1024 }",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
use codex_config::types::DEFAULT_OTEL_ENVIRONMENT;
|
||||
@@ -40,16 +41,16 @@ pub(crate) fn resolve_config(
|
||||
}
|
||||
|
||||
fn resolve_span_attributes(
|
||||
span_attributes: Option<BTreeMap<String, String>>,
|
||||
span_attributes: Option<HashMap<String, String>>,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> BTreeMap<String, String> {
|
||||
) -> HashMap<String, String> {
|
||||
let Some(span_attributes) = span_attributes else {
|
||||
return BTreeMap::new();
|
||||
return HashMap::new();
|
||||
};
|
||||
|
||||
let mut valid_attributes = BTreeMap::new();
|
||||
let mut valid_attributes = HashMap::new();
|
||||
for (key, value) in span_attributes {
|
||||
let attribute = BTreeMap::from([(key.clone(), value.clone())]);
|
||||
let attribute = HashMap::from([(key.clone(), value.clone())]);
|
||||
if let Err(err) = codex_otel::validate_span_attributes(&attribute) {
|
||||
push_invalid_config_warning("otel.span_attributes", err, startup_warnings);
|
||||
continue;
|
||||
|
||||
@@ -643,7 +643,7 @@ fn exported_logs(
|
||||
},
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::new(),
|
||||
span_attributes: HashMap::new(),
|
||||
tracestate: BTreeMap::new(),
|
||||
})
|
||||
.expect("OTEL settings")
|
||||
|
||||
@@ -67,6 +67,8 @@ opentelemetry_sdk = { workspace = true, features = [
|
||||
"testing",
|
||||
] }
|
||||
pretty_assertions = { workspace = true }
|
||||
rcgen = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[target.'cfg(unix)'.dev-dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
@@ -39,7 +39,7 @@ let settings = OtelSettings {
|
||||
tls: None,
|
||||
},
|
||||
metrics_exporter: OtelExporter::None,
|
||||
span_attributes: std::collections::BTreeMap::new(),
|
||||
span_attributes: std::collections::HashMap::new(),
|
||||
tracestate: std::collections::BTreeMap::new(),
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ pub(crate) fn resolve_exporter(exporter: &OtelExporter) -> OtelExporter {
|
||||
}
|
||||
|
||||
/// Validates configured span attributes before they are attached to exported spans.
|
||||
pub fn validate_span_attributes(attributes: &BTreeMap<String, String>) -> std::io::Result<()> {
|
||||
pub fn validate_span_attributes(attributes: &HashMap<String, String>) -> std::io::Result<()> {
|
||||
if attributes.keys().any(String::is_empty) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
@@ -58,7 +58,7 @@ pub struct OtelSettings {
|
||||
pub trace_exporter: OtelExporter,
|
||||
pub metrics_exporter: OtelExporter,
|
||||
pub runtime_metrics: bool,
|
||||
pub span_attributes: BTreeMap<String, String>,
|
||||
pub span_attributes: HashMap<String, String>,
|
||||
pub tracestate: BTreeMap<String, BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ fn build_http_client_inner(
|
||||
key_location.display()
|
||||
))
|
||||
})?;
|
||||
builder = builder.identity(identity).https_only(true);
|
||||
builder = builder.use_rustls_tls().identity(identity).https_only(true);
|
||||
}
|
||||
(Some(_), None) | (None, Some(_)) => {
|
||||
return Err(config_error(
|
||||
@@ -234,7 +234,13 @@ fn config_error(message: impl Into<String>) -> Box<dyn Error> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rcgen::CertifiedKey;
|
||||
use rcgen::generate_simple_self_signed;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
#[test]
|
||||
@@ -274,4 +280,51 @@ mod tests {
|
||||
|
||||
assert!(client.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_http_client_accepts_pem_client_identity() {
|
||||
let (_temp_dir, tls) = write_test_mtls_config();
|
||||
|
||||
build_http_client(&tls, OTEL_EXPORTER_OTLP_TIMEOUT)
|
||||
.expect("blocking HTTP client should accept PEM client identity");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_async_http_client_accepts_pem_client_identity() {
|
||||
let (_temp_dir, tls) = write_test_mtls_config();
|
||||
|
||||
let factory = HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault);
|
||||
build_async_http_client(&factory, Some(&tls), OTEL_EXPORTER_OTLP_TIMEOUT)
|
||||
.expect("async HTTP client should accept PEM client identity");
|
||||
}
|
||||
|
||||
fn write_test_mtls_config() -> (TempDir, OtelTlsConfig) {
|
||||
let temp_dir = tempfile::tempdir().expect("temporary TLS fixture directory");
|
||||
let CertifiedKey { cert, signing_key } =
|
||||
generate_simple_self_signed(vec!["localhost".to_string()])
|
||||
.expect("test certificate should generate");
|
||||
let certificate_path = temp_dir.path().join("client.pem");
|
||||
let private_key_path = temp_dir.path().join("client-key.pem");
|
||||
fs::write(&certificate_path, cert.pem()).expect("client certificate should write");
|
||||
fs::write(&private_key_path, signing_key.serialize_pem())
|
||||
.expect("client private key should write");
|
||||
|
||||
(
|
||||
temp_dir,
|
||||
OtelTlsConfig {
|
||||
ca_certificate: Some(
|
||||
AbsolutePathBuf::from_absolute_path(&certificate_path)
|
||||
.expect("CA certificate path should be absolute"),
|
||||
),
|
||||
client_certificate: Some(
|
||||
AbsolutePathBuf::from_absolute_path(&certificate_path)
|
||||
.expect("client certificate path should be absolute"),
|
||||
),
|
||||
client_private_key: Some(
|
||||
AbsolutePathBuf::from_absolute_path(&private_key_path)
|
||||
.expect("client private key path should be absolute"),
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ use opentelemetry_sdk::trace::TracerProviderBuilder;
|
||||
use opentelemetry_sdk::trace::span_processor_with_async_runtime::BatchSpanProcessor as TokioBatchSpanProcessor;
|
||||
use opentelemetry_semantic_conventions as semconv;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::mem::ManuallyDrop;
|
||||
@@ -411,7 +412,7 @@ fn normalize_host_name(host_name: &str) -> Option<String> {
|
||||
|
||||
fn tracer_provider_builder(
|
||||
resource: &Resource,
|
||||
span_attributes: BTreeMap<String, String>,
|
||||
span_attributes: HashMap<String, String>,
|
||||
) -> TracerProviderBuilder {
|
||||
let builder = SdkTracerProvider::builder().with_resource(resource.clone());
|
||||
if span_attributes.is_empty() {
|
||||
@@ -429,7 +430,7 @@ fn tracer_provider_builder(
|
||||
/// per-span metadata, so they need to be attached before each span is exported.
|
||||
#[derive(Debug)]
|
||||
struct SpanAttributesProcessor {
|
||||
attributes: BTreeMap<String, String>,
|
||||
attributes: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl SpanProcessor for SpanAttributesProcessor {
|
||||
@@ -536,7 +537,7 @@ fn build_logger(
|
||||
fn build_tracer_provider(
|
||||
resource: &Resource,
|
||||
exporter: &OtelExporter,
|
||||
span_attributes: BTreeMap<String, String>,
|
||||
span_attributes: HashMap<String, String>,
|
||||
factory: &codex_http_client::HttpClientFactory,
|
||||
) -> Result<SdkTracerProvider, Box<dyn Error>> {
|
||||
let span_exporter = match crate::config::resolve_exporter(exporter) {
|
||||
@@ -876,7 +877,7 @@ mod tests {
|
||||
trace_exporter: OtelExporter::None,
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::new(),
|
||||
span_attributes: HashMap::new(),
|
||||
tracestate: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use opentelemetry_sdk::metrics::data::AggregatedMetrics;
|
||||
use opentelemetry_sdk::metrics::data::MetricData;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
fn install() -> MetricsClient {
|
||||
@@ -94,7 +95,7 @@ fn rejected_opt_out_preserves_recording_and_accepted_opt_out_survives_shutdown()
|
||||
},
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::from([(String::new(), "invalid".to_string())]),
|
||||
span_attributes: HashMap::from([(String::new(), "invalid".to_string())]),
|
||||
tracestate: BTreeMap::new(),
|
||||
};
|
||||
assert_eq!(
|
||||
|
||||
@@ -411,7 +411,7 @@ fn otlp_http_exporter_sends_logs_to_collector()
|
||||
trace_exporter: OtelExporter::None,
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::new(),
|
||||
span_attributes: HashMap::new(),
|
||||
tracestate: BTreeMap::new(),
|
||||
})?
|
||||
.expect("otel provider");
|
||||
@@ -471,7 +471,7 @@ fn otel_provider_rejects_header_unsafe_configured_tracestate() {
|
||||
},
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::new(),
|
||||
span_attributes: HashMap::new(),
|
||||
tracestate: BTreeMap::from([(
|
||||
"example".to_string(),
|
||||
BTreeMap::from([("alpha".to_string(), "one\ntwo".to_string())]),
|
||||
@@ -537,7 +537,7 @@ fn otlp_http_exporter_sends_traces_to_collector()
|
||||
},
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::from([(
|
||||
span_attributes: HashMap::from([(
|
||||
"test.configured_attribute".to_string(),
|
||||
"configured-value".to_string(),
|
||||
)]),
|
||||
@@ -683,7 +683,7 @@ async fn otlp_http_exporter_sends_traces_to_collector_with_bounded_shutdown_in_t
|
||||
},
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::new(),
|
||||
span_attributes: HashMap::new(),
|
||||
tracestate: BTreeMap::new(),
|
||||
})?
|
||||
.expect("otel provider");
|
||||
@@ -777,7 +777,7 @@ fn otlp_http_exporter_times_out_when_collector_stalls_during_bounded_shutdown()
|
||||
},
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::new(),
|
||||
span_attributes: HashMap::new(),
|
||||
tracestate: BTreeMap::new(),
|
||||
})
|
||||
.expect("build otel provider")
|
||||
@@ -877,7 +877,7 @@ fn otlp_http_exporter_sends_traces_to_collector_in_current_thread_tokio_runtime(
|
||||
},
|
||||
metrics_exporter: OtelExporter::None,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::new(),
|
||||
span_attributes: HashMap::new(),
|
||||
tracestate: BTreeMap::new(),
|
||||
})
|
||||
.map_err(|err| err.to_string())?
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_otel::OtelProvider;
|
||||
use codex_otel::OtelSettings;
|
||||
use codex_otel::StatsigMetricsSettings;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
const WFP_SETUP_SERVICE_NAME: &str = "codex-windows-sandbox-setup";
|
||||
@@ -58,7 +59,7 @@ fn build_wfp_metrics_provider(
|
||||
trace_exporter: OtelExporter::None,
|
||||
metrics_exporter: OtelExporter::Statsig,
|
||||
runtime_metrics: false,
|
||||
span_attributes: BTreeMap::new(),
|
||||
span_attributes: HashMap::new(),
|
||||
tracestate: BTreeMap::new(),
|
||||
})
|
||||
.map_err(|err| anyhow::anyhow!("failed to initialize WFP setup metrics provider: {err}"))
|
||||
|
||||
@@ -9207,8 +9207,10 @@ class OtelRequirements(BaseModel):
|
||||
log_guardian_assessments: bool | None = None
|
||||
log_user_prompt: bool | None = None
|
||||
metrics_exporter: OtelExporter | None = None
|
||||
span_attributes: dict[str, Any] | None = None
|
||||
tool_result: OtelToolResultLogConfig | None = None
|
||||
trace_exporter: OtelExporter | None = None
|
||||
tracestate: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class OverriddenMetadata(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user