1pub mod actions;
3use crate::llm::actions::client_trait::{Client, ClientActionResult};
4use crate::protocol::{ConnectContext, Event};
5use crate::server::gelf::codec::{self, Compression, Transport};
6use crate::state::{client_handles::ClientSendOutcome, AccessLogOwner, ClientStatus};
7use crate::{console_error, console_info};
8pub use actions::GelfClientProtocol;
9use anyhow::{bail, Context, Result};
10use serde_json::{json, Value};
11use std::time::Duration;
12use tokio::{
13 io::{AsyncReadExt, AsyncWriteExt},
14 net::TcpStream,
15};
16const IO_TIMEOUT: Duration = Duration::from_secs(10);
17
18pub struct GelfClient;
19impl GelfClient {
20 pub async fn connect(ctx: ConnectContext) -> Result<std::net::SocketAddr> {
21 let transport = Transport::parse(
22 &ctx.startup_params
23 .as_ref()
24 .map(|p| p.get_optional_string("transport"))
25 .transpose()?
26 .flatten()
27 .unwrap_or_else(|| codec::DEFAULT_TRANSPORT.into()),
28 )?;
29 let compression = Compression::parse(
30 &ctx.startup_params
31 .as_ref()
32 .map(|p| p.get_optional_string("compression"))
33 .transpose()?
34 .flatten()
35 .unwrap_or_else(|| codec::DEFAULT_COMPRESSION.into()),
36 transport,
37 )?;
38 let chunk_size = usize::try_from(
39 ctx.startup_params
40 .as_ref()
41 .map(|p| p.get_optional_u64("chunk_size"))
42 .transpose()?
43 .flatten()
44 .unwrap_or(codec::DEFAULT_CHUNK_SIZE as u64),
45 )?;
46 anyhow::ensure!(
47 (13..=codec::MAX_DATAGRAM_BYTES).contains(&chunk_size),
48 "chunk_size must be 13..8192"
49 );
50 let (peer, local_addr, mut reader, mut writer) = match transport {
51 Transport::Tcp => {
52 let socket = tokio::time::timeout(IO_TIMEOUT, TcpStream::connect(&ctx.remote_addr))
53 .await
54 .context("GELF connect deadline exceeded")??;
55 let peer = socket.peer_addr()?;
56 let local = socket.local_addr()?;
57 let (reader, writer) = socket.into_split();
58 (peer, local, Some(reader), Writer::Tcp(writer))
59 }
60 Transport::Udp => {
61 let peer =
62 tokio::time::timeout(IO_TIMEOUT, tokio::net::lookup_host(&ctx.remote_addr))
63 .await
64 .context("GELF resolve deadline exceeded")??
65 .next()
66 .context("collector resolved to no addresses")?;
67 let socket = tokio::net::UdpSocket::bind(if peer.is_ipv4() {
68 "0.0.0.0:0"
69 } else {
70 "[::]:0"
71 })
72 .await?;
73 socket.connect(peer).await?;
74 let local = socket.local_addr()?;
75 (peer, local, None, Writer::Udp(socket))
76 }
77 };
78 let mut commands =
79 crate::client::command_support::register_command_channel(&ctx.state, ctx.client_id)
80 .await;
81 ctx.state
82 .update_client_status(ctx.client_id, ClientStatus::Connected)
83 .await;
84 console_info!(
85 ctx.status_tx,
86 "GELF emitter {} connected to {}",
87 ctx.client_id,
88 peer
89 );
90 let registrar = ctx.state.clone();
91 let client_id = ctx.client_id;
92 let handle = tokio::spawn(async move {
93 let protocol = GelfClientProtocol::new();
94 let connected = async {
95 let event = Event::new(
96 &actions::GELF_CONNECTED_EVENT,
97 json!({"remote_addr":peer.to_string(),"local_addr":local_addr.to_string(),"transport":transport.as_str()}),
98 );
99 let instruction = ctx
100 .state
101 .get_instruction_for_client(client_id)
102 .await
103 .unwrap_or_default();
104 let configured = ctx
105 .state
106 .get_client_event_handler_config(client_id)
107 .await
108 .is_some_and(|c| c.find_handler("gelf_connected").is_some());
109 if instruction.is_empty() && !configured {
110 return Ok(crate::llm::ClientLlmResult {
111 actions: vec![],
112 memory_updates: None,
113 });
114 }
115 let memory = ctx
116 .state
117 .get_memory_for_client(client_id)
118 .await
119 .unwrap_or_default();
120 crate::client::llm_budget::call_llm_for_client(
121 &ctx.llm_client,
122 &ctx.state,
123 client_id.to_string(),
124 &instruction,
125 &memory,
126 Some(&event),
127 &protocol,
128 &ctx.status_tx,
129 )
130 .await
131 };
132 tokio::pin!(connected);
133 let mut connected_done = false;
134 let mut unexpected = [0u8; 1];
135 'session: loop {
136 tokio::select! {
137 result = async { match reader.as_mut() { Some(reader)=>reader.read(&mut unexpected).await, None=>std::future::pending().await } } => {
138 match result {
139 Ok(0) => { console_info!(ctx.status_tx, "GELF collector closed the connection"); },
140 Ok(_) => { console_error!(ctx.status_tx, "GELF collector sent unexpected response data; closing"); },
141 Err(error) => { console_error!(ctx.status_tx, "GELF collector read failed: {}", error); }
142 }
143 break;
144 }
145 result = &mut connected, if !connected_done => {
146 connected_done = true;
147 match result {
148 Ok(result) => {
149 if let Some(memory) = result.memory_updates { ctx.state.set_memory_for_client(client_id, memory).await; }
150 for action in result.actions {
151 match Self::apply(&mut writer, &protocol, compression, chunk_size, action).await {
152 Ok(ClientSendOutcome::Disconnected) => break 'session,
153 Ok(ClientSendOutcome::Rejected { error }) => { console_error!(ctx.status_tx, "GELF initial action rejected: {}", error); }
154 Ok(_) => {},
155 Err(error) => { console_error!(ctx.status_tx, "GELF send failed: {}", error); break 'session; }
156 }
157 }
158 }
159 Err(error) => { console_error!(ctx.status_tx, "GELF connected handler failed: {}", error); }
160 }
161 }
162 command = commands.recv() => {
163 let Some(command) = command else { break; };
164 let action = command.action.clone();
165 let result = Self::apply(&mut writer, &protocol, compression, chunk_size, action.clone()).await;
166 let disconnect = result.is_err() || matches!(result, Ok(ClientSendOutcome::Disconnected));
167 let response = match &result { Ok(outcome) => serde_json::to_value(outcome).unwrap_or(Value::Null), Err(error) => json!({"error":error.to_string()}) };
168 ctx.state.record_access_log(AccessLogOwner::Client(client_id.as_u32()), "GELF", None, "injected_action", action, vec![response]).await;
169 crate::client::command_support::reply(command, result);
170 if disconnect { break; }
171 }
172 }
173 let _ = ctx.status_tx.send("__UPDATE_UI__".into());
174 }
175 ctx.state.remove_client_handle(client_id).await;
178 ctx.state
179 .update_client_status(client_id, ClientStatus::Disconnected)
180 .await;
181 let _ = ctx.status_tx.send("__UPDATE_UI__".into());
182 });
183 registrar.register_client_task(client_id, handle).await;
184 Ok(local_addr)
185 }
186 async fn apply(
187 writer: &mut Writer,
188 protocol: &GelfClientProtocol,
189 compression: Compression,
190 chunk_size: usize,
191 action: Value,
192 ) -> Result<ClientSendOutcome> {
193 let result = match protocol.execute_action(action) {
194 Ok(r) => r,
195 Err(error) => {
196 return Ok(ClientSendOutcome::Rejected {
197 error: error.to_string(),
198 })
199 }
200 };
201 match result {
202 ClientActionResult::Custom { name, data } if name == "send_gelf_message" => {
203 let message: codec::Message = serde_json::from_value(data)?;
204 let frames = match writer {
206 Writer::Udp(_) => {
207 codec::encode_udp(&message, compression, chunk_size, rand::random())
208 }
209 Writer::Tcp(_) => codec::encode_json(&message).map(|mut b| {
210 b.push(0);
211 vec![b]
212 }),
213 };
214 let frames = match frames {
215 Ok(b) => b,
216 Err(error) => {
217 return Ok(ClientSendOutcome::Rejected {
218 error: error.to_string(),
219 })
220 }
221 };
222 let bytes_sent = frames.iter().map(Vec::len).sum();
223 tokio::time::timeout(IO_TIMEOUT,async {
224 for bytes in frames {
225 match writer {
226 Writer::Tcp(stream)=>stream.write_all(&bytes).await?,
227 Writer::Udp(socket)=>{let n=socket.send(&bytes).await?;anyhow::ensure!(n==bytes.len(),"partial UDP write");}
228 }
229 }
230 Ok::<(),anyhow::Error>(())
231 }).await.context("GELF write deadline exceeded; transport closed to avoid replay after partial send")??;
232 Ok(ClientSendOutcome::Sent { bytes_sent })
233 }
234 ClientActionResult::Disconnect => Ok(ClientSendOutcome::Disconnected),
235 _ => bail!("unsupported GELF action result"),
236 }
237 }
238}
239enum Writer {
240 Tcp(tokio::net::tcp::OwnedWriteHalf),
241 Udp(tokio::net::UdpSocket),
242}