Skip to main content

netget/client/graphite/
actions.rs

1use crate::llm::actions::{
2    client_trait::{Client, ClientActionResult},
3    protocol_trait::Protocol,
4    ActionDefinition, StartupExamples,
5};
6use crate::protocol::log_template::LogTemplate;
7use crate::protocol::{
8    metadata::{DevelopmentState, ProtocolMetadataV2},
9    ConnectContext, EventType,
10};
11use crate::server::graphite::{
12    actions::parameter,
13    codec::{self, Metric},
14};
15use crate::state::app_state::AppState;
16use anyhow::{bail, Context, Result};
17use serde_json::{json, Value};
18use std::sync::LazyLock;
19
20#[derive(Default)]
21pub struct GraphiteClientProtocol;
22impl GraphiteClientProtocol {
23    pub fn new() -> Self {
24        Self
25    }
26}
27pub fn send_action() -> ActionDefinition {
28    ActionDefinition { name: "send_graphite_batch".into(), description: "Send timestamped Carbon plaintext metrics over TCP. Validates the whole action before writing. A successful write confirms local transport acceptance, not collector persistence. No automatic retries.".into(), parameters: vec![parameter("metrics", "array", "1..256 objects with path (nonempty UTF-8 without whitespace/control), value (finite number), timestamp (nonnegative UNIX seconds, possibly fractional; -1 requests receiver time). Each encoded line <=4096 bytes; action <=64KiB. Semicolon tags are opaque parts of path.", true)], example: json!({"type":"send_graphite_batch","metrics":[{"path":"servers.demo.load","value":0.5,"timestamp":1700000000}]}), log_template: Some(LogTemplate::new().with_info("-> Graphite Carbon metrics={metrics_len}")) }
29}
30fn disconnect_action() -> ActionDefinition {
31    ActionDefinition {
32        name: "disconnect".into(),
33        description: "Close the Carbon TCP connection and remove its command handle".into(),
34        parameters: vec![],
35        example: json!({"type":"disconnect"}),
36        log_template: Some(LogTemplate::new().with_info("-> Graphite Carbon disconnect")),
37    }
38}
39pub static GRAPHITE_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
40    EventType::new("graphite_connected", "TCP connection established; Carbon plaintext has no greeting or per-metric acknowledgments.", send_action().example)
41        .with_parameters(vec![parameter("remote_addr", "string", "Collector address", true), parameter("local_addr", "string", "Emitter address", true)])
42        .with_actions(vec![send_action(), disconnect_action()])
43});
44impl Protocol for GraphiteClientProtocol {
45    fn protocol_name(&self) -> &'static str {
46        "Graphite"
47    }
48    fn stack_name(&self) -> &'static str {
49        "ETH>IP>TCP>Graphite"
50    }
51    fn keywords(&self) -> Vec<&'static str> {
52        vec!["graphite", "carbon"]
53    }
54    fn description(&self) -> &'static str {
55        "Emit structured timestamped Graphite Carbon plaintext metrics over TCP"
56    }
57    fn example_prompt(&self) -> &'static str {
58        "Send a Graphite metric servers.demo.load with value 0.5 to localhost:2003"
59    }
60    fn group_name(&self) -> &'static str {
61        "Network Services"
62    }
63    fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
64        vec![send_action(), disconnect_action()]
65    }
66    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
67        vec![]
68    }
69    fn get_event_types(&self) -> Vec<EventType> {
70        vec![GRAPHITE_CONNECTED_EVENT.clone()]
71    }
72    fn metadata(&self) -> ProtocolMetadataV2 {
73        ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).well_known_port(2003)
74            .implementation("Native UTF-8 Carbon plaintext TCP encoder with bounded action batches")
75            .llm_control("One connected event; structured metric batches also available via command injection")
76            .e2e_testing("Exact wire, handler, rejection and lifecycle tests; official Carbon MetricLineReceiver interoperability")
77            .notes("TCP only; no UDP, Pickle, TLS, authentication, storage/query API, acknowledgment or retry. 4096 bytes per line, 64KiB/256 metrics per action; 10s connect/write deadline.").build()
78    }
79    fn get_startup_examples(&self) -> StartupExamples {
80        StartupExamples::new(
81            json!({"type":"open_client","base_stack":"graphite","remote_addr":"localhost:2003","instruction":"Send servers.demo.load 0.5 at receiver time (-1)"}),
82            json!({"type":"open_client","base_stack":"graphite","remote_addr":"localhost:2003","event_handlers":[{"event_pattern":"graphite_connected","handler":{"type":"script","language":"python","code":"import json,sys\njson.load(sys.stdin)\nprint(json.dumps({'actions':[{'type':'send_graphite_batch','metrics':[{'path':'servers.demo.load','value':0.5,'timestamp':-1}]}]}))"}}]}),
83            json!({"type":"open_client","base_stack":"graphite","remote_addr":"localhost:2003","event_handlers":[{"event_pattern":"graphite_connected","handler":{"type":"static","actions":[send_action().example]}}]}),
84        )
85    }
86}
87impl Client for GraphiteClientProtocol {
88    fn connect(
89        &self,
90        ctx: ConnectContext,
91    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
92    {
93        Box::pin(super::GraphiteClient::connect(ctx))
94    }
95    fn execute_action(&self, action: Value) -> Result<ClientActionResult> {
96        match action["type"].as_str() {
97            Some("send_graphite_batch") => {
98                let metrics: Vec<Metric> = serde_json::from_value(
99                    action.get("metrics").context("missing metrics")?.clone(),
100                )
101                .context("invalid structured metrics")?;
102                codec::encode_batch(&metrics)?;
103                Ok(ClientActionResult::Custom {
104                    name: "send_graphite_batch".into(),
105                    data: serde_json::to_value(metrics)?,
106                })
107            }
108            Some("disconnect") => Ok(ClientActionResult::Disconnect),
109            _ => bail!("unknown Graphite emitter action"),
110        }
111    }
112}