1use crate::llm::actions::{
2 client_trait::{Client, ClientActionResult},
3 protocol_trait::Protocol,
4 ActionDefinition, Parameter, ParameterDefinition,
5};
6use crate::protocol::log_template::LogTemplate;
7use crate::protocol::{ConnectContext, EventType};
8use crate::state::AppState;
9use anyhow::{bail, Result};
10use serde_json::{json, Value};
11use std::sync::LazyLock;
12#[derive(Default)]
13pub struct PrometheusClientProtocol;
14impl PrometheusClientProtocol {
15 pub fn new() -> Self {
16 Self
17 }
18}
19fn p(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
20 Parameter {
21 name: name.into(),
22 type_hint: kind.into(),
23 description: description.into(),
24 required,
25 }
26}
27fn scrape() -> ActionDefinition {
28 ActionDefinition{name:"scrape_metrics".into(),description:"GET metrics from this exporter's origin, negotiate text 0.0.4 or OpenMetrics 1.0.0 and parse families/samples. No raw body or PromQL service.".into(),parameters:vec![
29 p("path","string","Optional origin path/query; defaults to startup metrics_path",false),
30 p("format","string","auto (prefer OpenMetrics with text fallback), text, or openmetrics; default auto",false),
31 ],example:json!({"type":"scrape_metrics","format":"auto"}),log_template: Some(LogTemplate::new().with_info("Scrape Prometheus exporter metrics"))}
32}
33fn disconnect() -> ActionDefinition {
34 ActionDefinition {
35 name: "disconnect".into(),
36 description: "Cancel the scrape and close this logical client".into(),
37 parameters: vec![],
38 example: json!({"type":"disconnect"}),
39 log_template: Some(
40 LogTemplate::new().with_info("Cancel Prometheus scrape and close exporter client"),
41 ),
42 }
43}
44fn actions() -> Vec<ActionDefinition> {
45 vec![scrape(), disconnect()]
46}
47fn event(id: &str, description: &str, params: Vec<Parameter>) -> EventType {
48 EventType::new(id, description, scrape().example)
49 .with_parameters(params)
50 .with_actions(actions())
51}
52pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
53 event(
54 "prometheus_connected",
55 "Exporter client ready; explicitly request a scrape",
56 vec![
57 p("origin", "string", "HTTP(S) exporter origin", true),
58 p("metrics_path", "string", "Default scrape path/query", true),
59 ],
60 )
61});
62pub static METRICS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
63 event("prometheus_metrics","Complete, parsed exposition from one successful scrape",vec![
64 p("request","object","Typed originating action",true),p("path","string","Scraped path/query",true),
65 p("format","string","text or openmetrics from Content-Type",true),p("content_type","string","Exporter Content-Type",true),
66 p("sample_count","integer","Number of samples in the complete parsed exposition",true),
67 p("metrics","array","Families {name,type,help?,unit?,samples:[{name,suffix,labels,value,timestamp_ms? or timestamp_seconds?,exemplar?}]}; finite values are numbers, NaN/+Inf/-Inf are strings. Text timestamps are integer milliseconds; OpenMetrics and exemplar timestamps are seconds. Counter family names differ by format.",true),
68])
69});
70pub static ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
71 event("prometheus_scrape_error","Scrape failed; no partial metrics are accepted. The client remains available for a new request",vec![p("request","object","Originating action",true),p("path","string","Requested exporter path and query string",true),p("error","string","Transport, HTTP, format or parse refusal",true)])
72});
73impl Protocol for PrometheusClientProtocol {
74 fn protocol_name(&self) -> &'static str {
75 "Prometheus"
76 }
77 fn stack_name(&self) -> &'static str {
78 "ETH>IP>TCP>HTTP>Prometheus"
79 }
80 fn keywords(&self) -> Vec<&'static str> {
81 vec!["prometheus", "openmetrics", "exporter", "metrics"]
82 }
83 fn description(&self) -> &'static str {
84 "Scrape and parse structured exporter metrics"
85 }
86 fn group_name(&self) -> &'static str {
87 "AI & API"
88 }
89 fn example_prompt(&self) -> &'static str {
90 "Scrape Prometheus metrics from localhost:9100"
91 }
92 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
93 actions()
94 }
95 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
96 vec![]
97 }
98 fn get_event_types(&self) -> Vec<EventType> {
99 vec![
100 CONNECTED_EVENT.clone(),
101 METRICS_EVENT.clone(),
102 ERROR_EVENT.clone(),
103 ]
104 }
105 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
106 vec![
107 ParameterDefinition{name:"metrics_path".into(),type_hint:"string".into(),description:"Origin path/query used when a scrape action omits path".into(),required:false,example:json!("/metrics"),default:Some(json!(super::DEFAULT_PATH))},
108 ParameterDefinition{name:"scrape_timeout_secs".into(),type_hint:"integer".into(),description:"Whole scrape deadline, 1..30 seconds; sent in X-Prometheus-Scrape-Timeout-Seconds".into(),required:false,example:json!(10),default:Some(json!(super::DEFAULT_TIMEOUT_SECS))},
109 ]
110 }
111 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
112 use crate::protocol::metadata::*;
113 ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).privilege_requirement(PrivilegeRequirement::None).well_known_port(9100)
114 .implementation("Bounded HTTP exporter requests and typed text/OpenMetrics parser")
115 .llm_control("Explicit scrape path and format; handle typed family/sample results or scrape errors")
116 .e2e_testing("tests/client/prometheus: independent Prometheus exporter, NetGet exporter pair, negotiation/parser/bounds/cancellation and event handlers")
117 .notes("Exporter scraping only; no PromQL query API, remote write, protobuf/native histograms, target discovery, scrape scheduler, redirects, compression or persistent metric store. Legacy metric/label names negotiated with escaping=underscores. Native HTTPS verifies system roots; browser HTTP only. Body4MiB, samples20000, families4096, labels64, text16KiB; one active scrape, queues8 and4 handler followups. Manual handlers do not block injection/disconnect.")
118 .max_inbound_bytes(super::exposition::MAX_BODY).build()
119 }
120 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
121 let llm = json!({"type":"open_client","base_stack":"prometheus","protocol":"prometheus","remote_addr":"127.0.0.1:9100","instruction":"Scrape metrics once and explain them"});
122 let mut fixed = llm.clone();
123 fixed["event_handlers"] = json!([{"event_pattern":"prometheus_connected","handler":{"type":"static","actions":[scrape().example]}},{"event_pattern":"*","handler":{"type":"static","actions":[]}}]);
124 let mut script = fixed.clone();
125 script["event_handlers"][0]["handler"] = json!({"type":"script","language":"python","code":"import json, sys\njson.dump({'actions':[{'type':'scrape_metrics','format':'auto'}]},sys.stdout)"});
126 crate::llm::actions::StartupExamples::new(llm, script, fixed)
127 }
128}
129impl Client for PrometheusClientProtocol {
130 fn connect(
131 &self,
132 ctx: ConnectContext,
133 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
134 {
135 Box::pin(super::connect(ctx))
136 }
137 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
138 match v["type"].as_str() {
139 Some("scrape_metrics") => {
140 super::request(&v, super::DEFAULT_PATH)?;
141 Ok(ClientActionResult::Custom {
142 name: "scrape_metrics".into(),
143 data: v,
144 })
145 }
146 Some("disconnect") => Ok(ClientActionResult::Disconnect),
147 _ => bail!("unknown Prometheus client action"),
148 }
149 }
150}