Skip to main content

netget/client/prometheus/
actions.rs

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}