1use crate::models::WriteDataPoint;
4use crate::{Client, Http, RequestError, ReqwestProcessing};
5
6use bytes::BufMut;
7use futures::{Stream, StreamExt};
8use reqwest::header::HeaderMap;
9use reqwest::{Body, Method, StatusCode};
10use snafu::ResultExt;
11use std::io::{self, Write};
12
13impl Client {
14 pub async fn write_line_protocol(
18 &self,
19 org: &str,
20 bucket: &str,
21 body: impl Into<Body> + Send,
22 ) -> Result<(), RequestError> {
23 self.write_line_protocol_with_precision(org, bucket, body, TimestampPrecision::Nanoseconds)
24 .await
25 }
26
27 pub async fn write_line_protocol_with_precision(
29 &self,
30 org: &str,
31 bucket: &str,
32 body: impl Into<Body> + Send,
33 precision: TimestampPrecision,
34 ) -> Result<(), RequestError> {
35 self.write_line_protocol_with_precision_headers(
36 org,
37 bucket,
38 body,
39 precision,
40 HeaderMap::new(),
41 )
42 .await
43 }
44
45 async fn write_line_protocol_with_precision_headers(
46 &self,
47 org: &str,
48 bucket: &str,
49 body: impl Into<Body> + Send,
50 precision: TimestampPrecision,
51 headers: HeaderMap,
52 ) -> Result<(), RequestError> {
53 let body = body.into();
54 let write_url = self.url("/api/v2/write");
55
56 let response = self
57 .request(Method::POST, &write_url)
58 .headers(headers)
59 .query(&[
60 ("bucket", bucket),
61 ("org", org),
62 ("precision", precision.api_short_name()),
63 ])
64 .body(body)
65 .send()
66 .await
67 .context(ReqwestProcessing)?;
68
69 if response.status() != StatusCode::NO_CONTENT {
70 let status = response.status();
71 let text = response.text().await.context(ReqwestProcessing)?;
72 Http { status, text }.fail()?;
73 }
74
75 Ok(())
76 }
77
78 pub async fn write(
83 &self,
84 bucket: &str,
85 body: impl Stream<Item = impl WriteDataPoint> + Send + Sync + 'static,
86 ) -> Result<(), RequestError> {
87 self.write_with_precision(bucket, body, TimestampPrecision::Nanoseconds)
88 .await
89 }
90
91 pub async fn write_with_precision(
94 &self,
95 bucket: &str,
96 body: impl Stream<Item = impl WriteDataPoint> + Send + Sync + 'static,
97 timestamp_precision: TimestampPrecision,
98 ) -> Result<(), RequestError> {
99 let mut buffer = bytes::BytesMut::new();
100
101 let body = body.map(move |point| {
102 let mut w = (&mut buffer).writer();
103 point.write_data_point_to(&mut w)?;
104 w.flush()?;
105 Ok::<_, io::Error>(buffer.split().freeze())
106 });
107
108 #[cfg(feature = "gzip")]
109 {
110 use crate::Compression;
111 use async_compression::tokio::bufread::GzipEncoder;
112 use async_compression::Level;
113 use reqwest::header::HeaderValue;
114 use tokio_util::io::{ReaderStream, StreamReader};
115
116 match self.compression {
117 Compression::Gzip => {
118 let encoder = GzipEncoder::with_quality(StreamReader::new(body), Level::Best);
119 let body: Body = Body::wrap_stream(ReaderStream::new(encoder));
120
121 let mut headers = HeaderMap::new();
122 headers.insert("Content-Encoding", HeaderValue::from_static("gzip"));
123
124 return self
125 .write_line_protocol_with_precision_headers(
126 &self.org,
127 bucket,
128 body,
129 timestamp_precision,
130 headers,
131 )
132 .await;
133 }
134 Compression::None => {
135 }
137 }
138 }
139 let body: Body = Body::wrap_stream(body);
140
141 self.write_line_protocol_with_precision(&self.org, bucket, body, timestamp_precision)
142 .await
143 }
144}
145
146#[derive(Debug, PartialEq, Copy, Clone)]
148pub enum TimestampPrecision {
149 Seconds,
151 Milliseconds,
153 Microseconds,
155 Nanoseconds,
157}
158
159impl TimestampPrecision {
160 fn api_short_name(&self) -> &str {
161 match self {
162 Self::Seconds => "s",
163 Self::Milliseconds => "ms",
164 Self::Microseconds => "us",
165 Self::Nanoseconds => "ns",
166 }
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::models::DataPoint;
174 use futures::stream;
175 use mockito::mock;
176
177 #[tokio::test]
178 async fn writing_points() {
179 let org = "some-org";
180 let bucket = "some-bucket";
181 let token = "some-token";
182
183 let mock_server = mock(
184 "POST",
185 format!("/api/v2/write?bucket={}&org={}&precision=ns", bucket, org).as_str(),
186 )
187 .match_header("Authorization", format!("Token {}", token).as_str())
188 .match_body(
189 "\
190cpu,host=server01 usage=0.5
191cpu,host=server01,region=us-west usage=0.87
192",
193 )
194 .with_status(204)
195 .create();
196
197 let client = Client::new(mockito::server_url(), org, token);
198
199 let points = vec![
200 DataPoint::builder("cpu")
201 .tag("host", "server01")
202 .field("usage", 0.5)
203 .build()
204 .unwrap(),
205 DataPoint::builder("cpu")
206 .tag("host", "server01")
207 .tag("region", "us-west")
208 .field("usage", 0.87)
209 .build()
210 .unwrap(),
211 ];
212
213 let result = client.write(bucket, stream::iter(points)).await;
219 mock_server.assert();
220 assert!(result.is_ok());
221 }
222
223 #[tokio::test]
224 async fn writing_points_with_precision() {
225 let org = "some-org";
226 let bucket = "some-bucket";
227 let token = "some-token";
228
229 let mock_server = mock(
230 "POST",
231 format!("/api/v2/write?bucket={}&org={}&precision=s", bucket, org).as_str(),
232 )
233 .match_header("Authorization", format!("Token {}", token).as_str())
234 .match_body(
235 "\
236cpu,host=server01 usage=0.5 1671095854
237",
238 )
239 .with_status(204)
240 .create();
241
242 let client = Client::new(mockito::server_url(), org, token);
243
244 let point = DataPoint::builder("cpu")
245 .tag("host", "server01")
246 .field("usage", 0.5)
247 .timestamp(1671095854)
248 .build()
249 .unwrap();
250 let points = vec![point];
251
252 let result = client
258 .write_with_precision(bucket, stream::iter(points), TimestampPrecision::Seconds)
259 .await;
260 mock_server.assert();
261 assert!(result.is_ok());
262 }
263
264 #[tokio::test]
265 async fn status_code_correctly_interpreted() {
266 let org = "org";
267 let token = "token";
268 let bucket = "bucket";
269
270 let make_mock_server = |status| {
271 mock(
272 "POST",
273 format!("/api/v2/write?bucket={}&org={}&precision=ns", bucket, org).as_str(),
274 )
275 .with_status(status)
276 .create()
277 };
278
279 let write_with_status = |status| async move {
280 let mock_server = make_mock_server(status);
281 let client = Client::new(mockito::server_url(), org, token);
282 let points: Vec<DataPoint> = vec![];
283 let res = client.write(bucket, stream::iter(points)).await;
284 mock_server.assert();
285 res
286 };
287
288 assert!(write_with_status(204).await.is_ok());
290
291 for status in [200, 201, 400, 401, 404, 413, 429, 500, 503] {
293 assert!(write_with_status(status).await.is_err());
294 }
295 }
296}