netget/client/graphite/
actions.rs1use 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}