Skip to main content

tibba_model/
http_stat.rs

1// Copyright 2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{
16    Error, HttpDetectorModel, Model, ModelListParams, ResultValue, Schema, SchemaAllowCreate,
17    SchemaAllowEdit, SchemaOption, SchemaOptionValue, SchemaType, SchemaView, SqlxSnafu,
18    format_datetime,
19};
20use serde::{Deserialize, Serialize};
21use snafu::ResultExt;
22use sqlx::FromRow;
23use sqlx::{Pool, Postgres, QueryBuilder};
24use std::collections::HashMap;
25use time::PrimitiveDateTime;
26
27type Result<T> = std::result::Result<T, Error>;
28
29#[derive(FromRow)]
30struct HttpStatSchema {
31    id: i64,
32    target_id: i64,
33    target_name: String,
34    url: String,
35    dns_lookup: i32,
36    quic_connect: i32,
37    tcp_connect: i32,
38    tls_handshake: i32,
39    server_processing: i32,
40    content_transfer: i32,
41    total: i32,
42    addr: String,
43    status_code: i32,
44    tls: String,
45    alpn: String,
46    subject: String,
47    issuer: String,
48    cert_not_before: String,
49    cert_not_after: String,
50    cert_cipher: String,
51    cert_domains: String,
52    body_size: i32,
53    error: String,
54    result: i16,
55    remark: String,
56    region: String,
57    created: PrimitiveDateTime,
58    modified: PrimitiveDateTime,
59}
60
61#[derive(Default, Deserialize, Serialize, Debug, Clone)]
62pub struct HttpStat {
63    pub id: i64,
64    pub target_id: i64,
65    pub target_name: String,
66    pub url: String,
67    pub dns_lookup: i32,
68    pub quic_connect: i32,
69    pub tcp_connect: i32,
70    pub tls_handshake: i32,
71    pub server_processing: i32,
72    pub content_transfer: i32,
73    pub total: i32,
74    pub addr: String,
75    pub status_code: i32,
76    pub tls: String,
77    pub alpn: String,
78    pub subject: String,
79    pub issuer: String,
80    pub cert_not_before: String,
81    pub cert_not_after: String,
82    pub cert_cipher: String,
83    pub cert_domains: Vec<String>,
84    pub body_size: i32,
85    pub region: String,
86    pub error: String,
87    pub result: i16,
88    pub remark: String,
89    pub created: String,
90    pub modified: String,
91}
92
93impl From<HttpStatSchema> for HttpStat {
94    fn from(schema: HttpStatSchema) -> Self {
95        Self {
96            id: schema.id,
97            target_id: schema.target_id,
98            target_name: schema.target_name,
99            url: schema.url,
100            dns_lookup: schema.dns_lookup,
101            quic_connect: schema.quic_connect,
102            tcp_connect: schema.tcp_connect,
103            tls_handshake: schema.tls_handshake,
104            server_processing: schema.server_processing,
105            content_transfer: schema.content_transfer,
106            total: schema.total,
107            addr: schema.addr,
108            status_code: schema.status_code,
109            tls: schema.tls,
110            alpn: schema.alpn,
111            subject: schema.subject,
112            issuer: schema.issuer,
113            cert_not_before: schema.cert_not_before,
114            cert_not_after: schema.cert_not_after,
115            cert_cipher: schema.cert_cipher,
116            cert_domains: schema
117                .cert_domains
118                .split(',')
119                .map(|s| s.to_string())
120                .collect(),
121            body_size: schema.body_size,
122            region: schema.region,
123            error: schema.error,
124            result: schema.result,
125            remark: schema.remark,
126            created: format_datetime(schema.created),
127            modified: format_datetime(schema.modified),
128        }
129    }
130}
131
132#[derive(Debug, Deserialize, Serialize, Default)]
133
134pub struct HttpStatInsertParams {
135    pub target_id: i64,
136    pub target_name: String,
137    pub url: String,
138    pub dns_lookup: Option<i32>,
139    pub quic_connect: Option<i32>,
140    pub tcp_connect: Option<i32>,
141    pub tls_handshake: Option<i32>,
142    pub server_processing: Option<i32>,
143    pub content_transfer: Option<i32>,
144    pub total: Option<i32>,
145    pub addr: String,
146    pub status_code: Option<i16>,
147    pub tls: Option<String>,
148    pub alpn: Option<String>,
149    pub subject: Option<String>,
150    pub issuer: Option<String>,
151    pub cert_not_before: Option<String>,
152    pub cert_not_after: Option<String>,
153    pub cert_cipher: Option<String>,
154    pub cert_domains: Option<String>,
155    pub body_size: Option<i32>,
156    pub region: String,
157    pub error: Option<String>,
158    pub result: i16,
159    pub remark: String,
160}
161
162pub struct HttpStatModel {}
163
164impl HttpStatModel {
165    pub async fn add_stat(
166        &self,
167        pool: &Pool<Postgres>,
168        params: HttpStatInsertParams,
169    ) -> Result<u64> {
170        let row: (i64,) = sqlx::query_as(
171            r#"INSERT INTO http_stats (target_id, target_name, url, dns_lookup, quic_connect, tcp_connect, tls_handshake, server_processing, content_transfer, total, addr, status_code, tls, alpn, subject, issuer, cert_not_before, cert_not_after, cert_cipher, cert_domains, body_size, region, error, result, remark) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25) RETURNING id"#,
172        )
173        .bind(params.target_id)
174        .bind(params.target_name)
175        .bind(params.url)
176        .bind(params.dns_lookup.unwrap_or(-1))
177        .bind(params.quic_connect.unwrap_or(-1))
178        .bind(params.tcp_connect.unwrap_or(-1))
179        .bind(params.tls_handshake.unwrap_or(-1))
180        .bind(params.server_processing.unwrap_or(-1))
181        .bind(params.content_transfer.unwrap_or(-1))
182        .bind(params.total.unwrap_or(-1))
183        .bind(params.addr)
184        .bind(params.status_code.unwrap_or(0))
185        .bind(params.tls.unwrap_or_default())
186        .bind(params.alpn.unwrap_or_default())
187        .bind(params.subject.unwrap_or_default())
188        .bind(params.issuer.unwrap_or_default())
189        .bind(params.cert_not_before.unwrap_or_default())
190        .bind(params.cert_not_after.unwrap_or_default())
191        .bind(params.cert_cipher.unwrap_or_default())
192        .bind(params.cert_domains.unwrap_or_default())
193        .bind(params.body_size.unwrap_or(-1))
194        .bind(params.region)
195        .bind(params.error.unwrap_or_default())
196        .bind(params.result)
197        .bind(params.remark)
198        .fetch_one(pool)
199        .await
200        .context(SqlxSnafu)?;
201
202        Ok(row.0 as u64)
203    }
204    pub async fn list_by_created(
205        &self,
206        pool: &Pool<Postgres>,
207        created_range: (&str, &str),
208    ) -> Result<Vec<HttpStat>> {
209        let detectors = sqlx::query_as::<_, HttpStatSchema>(
210            r#"SELECT * FROM http_stats WHERE created >= $1 AND created <= $2"#,
211        )
212        .bind(created_range.0)
213        .bind(created_range.1)
214        .fetch_all(pool)
215        .await
216        .context(SqlxSnafu)?;
217        Ok(detectors.into_iter().map(|schema| schema.into()).collect())
218    }
219}
220
221impl Model for HttpStatModel {
222    type Output = HttpStat;
223    fn new() -> Self {
224        Self {}
225    }
226    fn keyword(&self) -> String {
227        "target_name".to_string()
228    }
229    async fn schema_view(&self, pool: &Pool<Postgres>) -> SchemaView {
230        let mut detector_options = vec![];
231        let detector_model = HttpDetectorModel {};
232        if let Ok(detectors) = detector_model.list_enabled(pool).await {
233            for detector in detectors {
234                detector_options.push(SchemaOption {
235                    label: detector.name,
236                    value: SchemaOptionValue::String(detector.id.to_string()),
237                });
238            }
239            detector_options.sort_by_key(|option| option.label.clone());
240        }
241        SchemaView {
242            schemas: vec![
243                Schema {
244                    name: "target_id".to_string(),
245                    category: SchemaType::String,
246                    hidden: true,
247                    filterable: !detector_options.is_empty(),
248                    options: Some(detector_options),
249                    ..Default::default()
250                },
251                Schema {
252                    name: "target_name".to_string(),
253                    label: Some("name".to_string()),
254                    category: SchemaType::String,
255                    fixed: true,
256                    ..Default::default()
257                },
258                Schema {
259                    name: "url".to_string(),
260                    category: SchemaType::String,
261                    max_width: Some(200),
262                    ..Default::default()
263                },
264                Schema {
265                    name: "result".to_string(),
266                    category: SchemaType::Result,
267                    filterable: true,
268                    options: Some(vec![
269                        SchemaOption {
270                            label: "Success".to_string(),
271                            value: SchemaOptionValue::String(
272                                (ResultValue::Success as u8).to_string(),
273                            ),
274                        },
275                        SchemaOption {
276                            label: "Failed".to_string(),
277                            value: SchemaOptionValue::String(
278                                (ResultValue::Failed as u8).to_string(),
279                            ),
280                        },
281                    ]),
282                    ..Default::default()
283                },
284                Schema {
285                    name: "total".to_string(),
286                    category: SchemaType::Number,
287                    hidden_values: vec!["-1".to_string()],
288                    filterable: true,
289                    options: Some(vec![
290                        SchemaOption {
291                            label: ">= 1s".to_string(),
292                            value: SchemaOptionValue::String("1000".to_string()),
293                        },
294                        SchemaOption {
295                            label: ">= 2s".to_string(),
296                            value: SchemaOptionValue::String("2000".to_string()),
297                        },
298                        SchemaOption {
299                            label: ">= 3s".to_string(),
300                            value: SchemaOptionValue::String("3000".to_string()),
301                        },
302                    ]),
303                    ..Default::default()
304                },
305                Schema {
306                    name: "dns_lookup".to_string(),
307                    category: SchemaType::Number,
308                    hidden_values: vec!["-1".to_string()],
309                    ..Default::default()
310                },
311                Schema {
312                    name: "quic_connect".to_string(),
313                    category: SchemaType::Number,
314                    hidden_values: vec!["-1".to_string()],
315                    ..Default::default()
316                },
317                Schema {
318                    name: "tcp_connect".to_string(),
319                    category: SchemaType::Number,
320                    hidden_values: vec!["-1".to_string()],
321                    ..Default::default()
322                },
323                Schema {
324                    name: "tls_handshake".to_string(),
325                    category: SchemaType::Number,
326                    hidden_values: vec!["-1".to_string()],
327                    ..Default::default()
328                },
329                Schema {
330                    name: "server_processing".to_string(),
331                    category: SchemaType::Number,
332                    hidden_values: vec!["-1".to_string()],
333                    ..Default::default()
334                },
335                Schema {
336                    name: "content_transfer".to_string(),
337                    category: SchemaType::Number,
338                    hidden_values: vec!["-1".to_string()],
339                    ..Default::default()
340                },
341                Schema {
342                    name: "timing".to_string(),
343                    category: SchemaType::PopoverCard,
344                    combinations: Some(vec![
345                        "dns_lookup".to_string(),
346                        "quic_connect".to_string(),
347                        "tcp_connect".to_string(),
348                        "tls_handshake".to_string(),
349                        "server_processing".to_string(),
350                        "content_transfer".to_string(),
351                        "total".to_string(),
352                    ]),
353                    ..Default::default()
354                },
355                Schema {
356                    name: "addr".to_string(),
357                    category: SchemaType::String,
358                    ..Default::default()
359                },
360                Schema {
361                    name: "status_code".to_string(),
362                    category: SchemaType::Number,
363                    hidden_values: vec!["0".to_string()],
364                    ..Default::default()
365                },
366                Schema {
367                    name: "tls".to_string(),
368                    category: SchemaType::String,
369                    ..Default::default()
370                },
371                Schema {
372                    name: "alpn".to_string(),
373                    category: SchemaType::String,
374                    ..Default::default()
375                },
376                Schema {
377                    name: "subject".to_string(),
378                    category: SchemaType::String,
379                    ..Default::default()
380                },
381                Schema {
382                    name: "issuer".to_string(),
383                    category: SchemaType::String,
384                    ..Default::default()
385                },
386                Schema {
387                    name: "cert_not_before".to_string(),
388                    category: SchemaType::Date,
389                    ..Default::default()
390                },
391                Schema {
392                    name: "cert_not_after".to_string(),
393                    category: SchemaType::Date,
394                    ..Default::default()
395                },
396                Schema {
397                    name: "cert_cipher".to_string(),
398                    category: SchemaType::String,
399                    ..Default::default()
400                },
401                Schema {
402                    name: "cert_domains".to_string(),
403                    category: SchemaType::Strings,
404                    ..Default::default()
405                },
406                Schema {
407                    name: "body_size".to_string(),
408                    category: SchemaType::ByteSize,
409                    ..Default::default()
410                },
411                Schema {
412                    name: "error".to_string(),
413                    category: SchemaType::String,
414                    ..Default::default()
415                },
416                Schema {
417                    name: "region".to_string(),
418                    category: SchemaType::String,
419                    ..Default::default()
420                },
421                Schema::new_readonly_remark(),
422                Schema::new_created(),
423                Schema::new_filterable_modified(),
424            ],
425            allow_edit: SchemaAllowEdit {
426                disabled: true,
427                ..Default::default()
428            },
429            allow_create: SchemaAllowCreate {
430                disabled: true,
431                ..Default::default()
432            },
433        }
434    }
435
436    fn push_filter_conditions<'args>(
437        &self,
438        qb: &mut QueryBuilder<'args, Postgres>,
439        filters: &HashMap<String, String>,
440    ) -> Result<()> {
441        if let Some(result) = filters.get("result").and_then(|s| s.parse::<i16>().ok()) {
442            qb.push(" AND result = ");
443            qb.push_bind(result);
444        }
445        if let Some(target_id) = filters.get("target_id").and_then(|s| s.parse::<i64>().ok()) {
446            qb.push(" AND target_id = ");
447            qb.push_bind(target_id);
448        }
449        if let Some(total) = filters.get("total").and_then(|s| s.parse::<i32>().ok()) {
450            qb.push(" AND total >= ");
451            qb.push_bind(total);
452        }
453        Ok(())
454    }
455
456    async fn list(
457        &self,
458        pool: &Pool<Postgres>,
459        params: &ModelListParams,
460    ) -> Result<Vec<Self::Output>> {
461        let mut qb = QueryBuilder::new("SELECT * FROM http_stats");
462        self.push_conditions(&mut qb, params)?;
463        params.push_pagination(&mut qb);
464        let stats = qb
465            .build_query_as::<HttpStatSchema>()
466            .fetch_all(pool)
467            .await
468            .context(SqlxSnafu)?;
469        Ok(stats.into_iter().map(|s| s.into()).collect())
470    }
471    async fn count(&self, pool: &Pool<Postgres>, params: &ModelListParams) -> Result<i64> {
472        let mut qb = QueryBuilder::new("SELECT COUNT(*) FROM http_stats");
473        self.push_conditions(&mut qb, params)?;
474        let count = qb
475            .build_query_scalar::<i64>()
476            .fetch_one(pool)
477            .await
478            .context(SqlxSnafu)?;
479        Ok(count)
480    }
481    async fn get_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<Option<Self::Output>> {
482        let stat = sqlx::query_as::<_, HttpStatSchema>(r#"SELECT * FROM http_stats WHERE id = $1"#)
483            .bind(id as i64)
484            .fetch_optional(pool)
485            .await
486            .context(SqlxSnafu)?;
487        Ok(stat.map(|schema| schema.into()))
488    }
489}