use std::collections::BTreeMap;
use regex::Regex;
use serde_json::{json, Value};
struct Guards {
_propagation: Option<provide_telemetry::propagation::PropagationGuard>,
_context: Vec<provide_telemetry::context::ContextGuard>,
}
impl Guards {
fn new() -> Self {
Self {
_propagation: None,
_context: Vec::new(),
}
}
}
fn as_str<'a>(v: &'a Value, key: &str) -> &'a str {
v.get(key).and_then(Value::as_str).unwrap_or("")
}
fn as_object(v: &Value, key: &str) -> BTreeMap<String, Value> {
v.get(key)
.and_then(Value::as_object)
.map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default()
}
fn exec_setup(step: &Value) {
let overrides = as_object(step, "overrides");
for (key, value) in &overrides {
let env_key = match key.as_str() {
"serviceName" => "PROVIDE_TELEMETRY_SERVICE_NAME",
"environment" => "PROVIDE_TELEMETRY_ENVIRONMENT",
"version" => "PROVIDE_TELEMETRY_VERSION",
_ => continue,
};
if let Some(s) = value.as_str() {
unsafe { std::env::set_var(env_key, s) };
}
}
provide_telemetry::setup_telemetry(None).ok();
}
fn exec_setup_invalid(step: &Value, variables: &mut BTreeMap<String, Value>) {
let into = as_str(step, "into");
let overrides = as_object(step, "overrides");
for (key, value) in &overrides {
let env_key = match key.as_str() {
"samplingLogsRate" => "PROVIDE_SAMPLING_LOGS_RATE",
"samplingTracesRate" => "PROVIDE_SAMPLING_TRACES_RATE",
"samplingMetricsRate" => "PROVIDE_SAMPLING_METRICS_RATE",
"serviceName" => "PROVIDE_TELEMETRY_SERVICE_NAME",
_ => continue,
};
let val_str = match value {
Value::Number(n) => n.to_string(),
Value::String(s) => s.clone(),
_ => value.to_string(),
};
unsafe { std::env::set_var(env_key, &val_str) };
}
let result = provide_telemetry::setup_telemetry(None);
let raised = result.is_err();
let error = result.err().map(|e| e.to_string()).unwrap_or_default();
for key in overrides.keys() {
let env_key = match key.as_str() {
"samplingLogsRate" => "PROVIDE_SAMPLING_LOGS_RATE",
"samplingTracesRate" => "PROVIDE_SAMPLING_TRACES_RATE",
"samplingMetricsRate" => "PROVIDE_SAMPLING_METRICS_RATE",
"serviceName" => "PROVIDE_TELEMETRY_SERVICE_NAME",
_ => continue,
};
unsafe { std::env::remove_var(env_key) };
}
if !into.is_empty() {
variables.insert(
into.to_string(),
json!({ "raised": raised, "error": error }),
);
}
}
fn exec_bind_propagation(step: &Value, guards: &mut Guards) {
let traceparent = step.get("traceparent").and_then(Value::as_str);
let baggage = step.get("baggage").and_then(Value::as_str);
let ctx = provide_telemetry::extract_w3c_context(traceparent, None, baggage);
let guard = provide_telemetry::bind_propagation_context(ctx);
guards._propagation = Some(guard);
}
fn exec_clear_propagation(guards: &mut Guards) {
guards._propagation = None;
}
fn exec_get_trace_context(step: &Value, variables: &mut BTreeMap<String, Value>) {
let into = as_str(step, "into");
if into.is_empty() {
return;
}
let ctx = provide_telemetry::get_trace_context();
let trace_id = ctx
.get("trace_id")
.and_then(|v| v.as_ref())
.cloned()
.unwrap_or_default();
let span_id = ctx
.get("span_id")
.and_then(|v| v.as_ref())
.cloned()
.unwrap_or_default();
variables.insert(
into.to_string(),
json!({ "trace_id": trace_id, "span_id": span_id }),
);
}
fn exec_flush(step: &Value, variables: &mut BTreeMap<String, Value>) {
let into = as_str(step, "into");
if into.is_empty() {
return;
}
let ok = provide_telemetry::flush_telemetry(None).is_ok();
variables.insert(into.to_string(), json!({ "ok": ok }));
}
fn exec_bind_context(step: &Value, guards: &mut Guards) {
let fields = as_object(step, "fields");
let pairs: Vec<(String, Value)> = fields.into_iter().collect();
let guard = provide_telemetry::bind_context(pairs);
guards._context.push(guard);
}
fn exec_register_secret_pattern(step: &Value) {
let name = as_str(step, "name");
let pattern = as_str(step, "pattern");
assert!(
!name.is_empty() && !pattern.is_empty(),
"register_secret_pattern requires name and pattern"
);
let compiled = Regex::new(pattern).expect("secret pattern must compile");
provide_telemetry::register_secret_pattern(name, compiled);
}
fn exec_emit_log(step: &Value) {
let message = as_str(step, "message");
let logger = provide_telemetry::get_logger(Some("contract"));
let extra: BTreeMap<String, Value> = step
.get("fields")
.and_then(|f| f.as_object())
.map(|obj| {
obj.iter()
.filter(|(k, _)| k.as_str() != "event")
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
})
.unwrap_or_default();
if extra.is_empty() {
logger.info(message);
} else {
logger.info_fields(message, &extra);
}
}
fn exec_capture_log(step: &Value, variables: &mut BTreeMap<String, Value>) {
let into = as_str(step, "into");
if into.is_empty() {
return;
}
let raw = String::from_utf8(provide_telemetry::take_json_capture()).unwrap_or_default();
let mut record = Value::Null;
for line in raw.lines().rev() {
if let Ok(v) = serde_json::from_str::<Value>(line) {
record = v;
break;
}
}
if let Value::Object(map) = &record {
let mut flat = serde_json::Map::new();
for (k, v) in map {
match v {
Value::String(s) => {
flat.insert(k.clone(), Value::String(s.clone()));
}
_ => {
flat.insert(k.clone(), v.clone());
}
}
}
variables.insert(into.to_string(), Value::Object(flat));
} else {
variables.insert(into.to_string(), record);
}
}
fn exec_get_runtime_status(step: &Value, variables: &mut BTreeMap<String, Value>) {
let into = as_str(step, "into");
if into.is_empty() {
return;
}
let status = provide_telemetry::get_runtime_status();
let config = provide_telemetry::get_runtime_config();
let service_name = config
.as_ref()
.map(|c| c.service_name.clone())
.unwrap_or_default();
variables.insert(
into.to_string(),
json!({
"active": status.setup_done,
"service_name": service_name,
"setup_done": status.setup_done,
}),
);
}
fn run_case(case_id: &str, case: &Value) -> Value {
let steps = case
.get("steps")
.and_then(Value::as_array)
.expect("case must have steps array");
let mut variables: BTreeMap<String, Value> = BTreeMap::new();
let mut guards = Guards::new();
unsafe { std::env::set_var("PROVIDE_LOG_FORMAT", "json") };
provide_telemetry::enable_json_capture_for_tests();
for step in steps {
let op = as_str(step, "op");
match op {
"setup" => exec_setup(step),
"setup_invalid" => exec_setup_invalid(step, &mut variables),
"shutdown" => {
provide_telemetry::shutdown_telemetry(None).ok();
}
"flush" => exec_flush(step, &mut variables),
"bind_propagation" => exec_bind_propagation(step, &mut guards),
"clear_propagation" => exec_clear_propagation(&mut guards),
"get_trace_context" => exec_get_trace_context(step, &mut variables),
"bind_context" => exec_bind_context(step, &mut guards),
"register_secret_pattern" => exec_register_secret_pattern(step),
"emit_log" => exec_emit_log(step),
"capture_log" => exec_capture_log(step, &mut variables),
"get_runtime_status" => exec_get_runtime_status(step, &mut variables),
unknown => panic!("unsupported contract operation: '{unknown}'"),
}
}
json!({ "case": case_id, "variables": variables })
}
fn main() {
let case_id =
std::env::var("PROVIDE_CONTRACT_CASE").expect("PROVIDE_CONTRACT_CASE env var required");
let yaml_path = std::path::Path::new("../spec/contract_fixtures.yaml");
let yaml_content = std::fs::read_to_string(yaml_path).unwrap_or_else(|_| {
std::fs::read_to_string("spec/contract_fixtures.yaml")
.expect("cannot read spec/contract_fixtures.yaml")
});
let doc: Value = serde_yaml::from_str(&yaml_content).expect("failed to parse YAML");
let cases = doc
.get("contract_cases")
.expect("YAML missing contract_cases key");
let case = cases
.get(&case_id)
.unwrap_or_else(|| panic!("unknown contract case: {case_id}"));
let result = run_case(&case_id, case);
println!(
"{}",
serde_json::to_string(&result).expect("failed to serialize result")
);
}