|
9 | 9 | //! - No circular dependencies |
10 | 10 |
|
11 | 11 | use anyhow::{Context, Result}; |
| 12 | +use reqwest::header::{HeaderName, HeaderValue}; |
12 | 13 | use std::collections::{HashMap, HashSet}; |
13 | 14 | use url::Url; |
14 | 15 |
|
15 | 16 | use crate::agent_kinds::{AgentKind, parse_agent_kind}; |
16 | | -use crate::config::{CalciforgeConfig, CredentialOwner, GatewayRetryConfig}; |
| 17 | +use crate::config::{ |
| 18 | + CalciforgeConfig, CredentialOwner, GatewayRetryConfig, ProxyObservabilityConfig, |
| 19 | +}; |
17 | 20 | use crate::model_names::{ |
18 | 21 | configured_agent_selectors, configured_first_class_model_ids, resolve_model_alias_chain, |
19 | 22 | }; |
@@ -558,6 +561,10 @@ fn validate_proxy_config(proxy: &crate::config::ProxyConfig, result: &mut Valida |
558 | 561 | } |
559 | 562 | } |
560 | 563 |
|
| 564 | + for (index, sink) in proxy.observability.iter().enumerate() { |
| 565 | + validate_proxy_observability_config(index, sink, result); |
| 566 | + } |
| 567 | + |
561 | 568 | if !proxy.enabled { |
562 | 569 | return; |
563 | 570 | } |
@@ -789,6 +796,72 @@ fn validate_proxy_config(proxy: &crate::config::ProxyConfig, result: &mut Valida |
789 | 796 | } |
790 | 797 | } |
791 | 798 |
|
| 799 | +fn validate_proxy_observability_config( |
| 800 | + index: usize, |
| 801 | + sink: &ProxyObservabilityConfig, |
| 802 | + result: &mut ValidationResult, |
| 803 | +) { |
| 804 | + if sink.timeout_ms == 0 { |
| 805 | + result.add_error(format!( |
| 806 | + "Proxy observability sink #{index} timeout_ms cannot be zero" |
| 807 | + )); |
| 808 | + } |
| 809 | + |
| 810 | + let kind = sink.kind.trim().to_ascii_lowercase().replace('_', "-"); |
| 811 | + if !crate::proxy::telemetry::SUPPORTED_OBSERVABILITY_KINDS.contains(&kind.as_str()) { |
| 812 | + result.add_error(format!( |
| 813 | + "Proxy observability sink #{index} kind '{}' is invalid. Use one of: {}", |
| 814 | + sink.kind, |
| 815 | + crate::proxy::telemetry::SUPPORTED_OBSERVABILITY_KINDS.join(", ") |
| 816 | + )); |
| 817 | + return; |
| 818 | + } |
| 819 | + |
| 820 | + if matches!( |
| 821 | + kind.as_str(), |
| 822 | + "http-json" | "webhook" | "otel" | "otlp" | "traceloop" |
| 823 | + ) { |
| 824 | + match sink.endpoint.as_deref().map(str::trim) { |
| 825 | + Some(endpoint) if !endpoint.is_empty() => validate_http_url( |
| 826 | + &format!("Proxy observability sink #{index} endpoint"), |
| 827 | + endpoint, |
| 828 | + result, |
| 829 | + false, |
| 830 | + ), |
| 831 | + _ => result.add_error(format!( |
| 832 | + "Proxy observability sink #{index} kind '{}' requires endpoint", |
| 833 | + sink.kind |
| 834 | + )), |
| 835 | + } |
| 836 | + } |
| 837 | + |
| 838 | + if kind == "log" |
| 839 | + && sink |
| 840 | + .endpoint |
| 841 | + .as_deref() |
| 842 | + .is_some_and(|s| !s.trim().is_empty()) |
| 843 | + { |
| 844 | + result.add_warning(format!( |
| 845 | + "Proxy observability sink #{index} kind='log' ignores endpoint" |
| 846 | + )); |
| 847 | + } |
| 848 | + |
| 849 | + for (name, value) in &sink.headers { |
| 850 | + if HeaderName::from_bytes(name.as_bytes()).is_err() { |
| 851 | + result.add_error(format!( |
| 852 | + "Proxy observability sink #{index} header name '{}' is invalid", |
| 853 | + name |
| 854 | + )); |
| 855 | + } |
| 856 | + if HeaderValue::from_str(value).is_err() { |
| 857 | + result.add_error(format!( |
| 858 | + "Proxy observability sink #{index} header '{}' has an invalid value", |
| 859 | + name |
| 860 | + )); |
| 861 | + } |
| 862 | + } |
| 863 | +} |
| 864 | + |
792 | 865 | fn validate_gateway_retry_config( |
793 | 866 | label: &str, |
794 | 867 | retry: &GatewayRetryConfig, |
|
0 commit comments