Skip to main content

pingap_logger/
access.rs

1// Copyright 2024-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 bytes::BytesMut;
16use chrono::format::SecondsFormat;
17use chrono::{Local, Utc};
18use pingap_core::{Ctx, HOST_NAME_TAG, format_duration, get_hostname};
19use pingap_util::format_byte_size;
20use pingora::http::ResponseHeader;
21use pingora::proxy::Session;
22use regex::Regex;
23use std::sync::LazyLock;
24use std::time::Instant;
25use substring::Substring;
26
27// Enum representing different types of log tags that can be used in the logging format
28#[derive(Debug, Clone, PartialEq)]
29pub enum TagCategory {
30    Fill,     // Static text
31    Host,     // Server hostname
32    Method,   // HTTP method (GET, POST, etc.)
33    Path,     // Request path
34    Proto,    // Protocol version
35    Query,    // Query parameters
36    Remote,   // Remote address
37    ClientIp, // Client IP address
38    Scheme,
39    Uri,
40    Referrer,
41    UserAgent,
42    When,
43    WhenUtcIso,
44    WhenUnix,
45    Size,
46    SizeHuman,
47    Status,
48    Latency,
49    LatencyHuman,
50    Cookie,
51    RequestHeader,
52    ResponseHeader,
53    Context,
54    PayloadSize,
55    PayloadSizeHuman,
56    RequestId,
57}
58
59// Represents a single tag in the log format
60#[derive(Debug, Clone)]
61pub struct Tag {
62    pub category: TagCategory,
63    pub data: Option<String>, // Optional data associated with the tag
64}
65
66#[derive(Debug, Default, Clone)]
67pub struct Parser {
68    pub needs_timestamp: bool,
69    pub capacity: usize,
70    pub tags: Vec<Tag>,
71}
72
73// Parses special tags with prefixes like ~, >, <, :, $
74fn format_extra_tag(key: &str) -> Option<Tag> {
75    // Requires at least 2 chars (prefix + content)
76    if key.len() < 2 {
77        return None;
78    }
79    let key = key.substring(1, key.len() - 1);
80    let ch = key.substring(0, 1);
81    let value = key.substring(1, key.len());
82    match ch {
83        "~" => Some(Tag {
84            // Cookie values
85            category: TagCategory::Cookie,
86            data: Some(value.to_string()),
87        }),
88        ">" => Some(Tag {
89            // Request headers
90            category: TagCategory::RequestHeader,
91            data: Some(value.to_string()),
92        }),
93        "<" => Some(Tag {
94            // Response headers
95            category: TagCategory::ResponseHeader,
96            data: Some(value.to_string()),
97        }),
98        ":" => Some(Tag {
99            category: TagCategory::Context,
100            data: Some(value.to_string()),
101        }),
102        "$" => {
103            if key.as_bytes() == HOST_NAME_TAG {
104                Some(Tag {
105                    category: TagCategory::Fill,
106                    data: Some(get_hostname().to_string()),
107                })
108            } else {
109                Some(Tag {
110                    category: TagCategory::Fill,
111                    data: Some(std::env::var(value).unwrap_or_default()),
112                })
113            }
114        },
115        _ => None,
116    }
117}
118
119// Predefined log formats
120static COMBINED: &str = r###"{remote} "{method} {uri} {proto}" {status} {size_human} "{referer}" "{user_agent}""###;
121static COMMON: &str =
122    r###"{remote} "{method} {uri} {proto}" {status} {size_human}""###;
123static SHORT: &str = r###"{remote} {method} {uri} {proto} {status} {size_human} - {latency}ms"###;
124static TINY: &str = r###"{method} {uri} {status} {size_human} - {latency}ms"###;
125
126impl From<&str> for Parser {
127    fn from(value: &str) -> Self {
128        let value = match value {
129            "combined" => COMBINED,
130            "common" => COMMON,
131            "short" => SHORT,
132            "tiny" => TINY,
133            _ => value,
134        };
135        let Ok(reg) = Regex::new(r"(\{[a-zA-Z_<>\-~:$]+*\})") else {
136            return Parser {
137                needs_timestamp: false,
138                capacity: 0,
139                tags: vec![Tag {
140                    category: TagCategory::Fill,
141                    data: Some(value.to_string()),
142                }],
143            };
144        };
145        let mut current = 0;
146        let mut end = 0;
147        let mut tags = vec![];
148
149        while let Some(result) = reg.find_at(value, current) {
150            if end < result.start() {
151                tags.push(Tag {
152                    category: TagCategory::Fill,
153                    data: Some(
154                        value.substring(end, result.start()).to_string(),
155                    ),
156                });
157            }
158            let key = result.as_str();
159
160            match key {
161                "{host}" => tags.push(Tag {
162                    category: TagCategory::Host,
163                    data: None,
164                }),
165                "{method}" => tags.push(Tag {
166                    category: TagCategory::Method,
167                    data: None,
168                }),
169                "{path}" => tags.push(Tag {
170                    category: TagCategory::Path,
171                    data: None,
172                }),
173                "{proto}" => tags.push(Tag {
174                    category: TagCategory::Proto,
175                    data: None,
176                }),
177                "{query}" => tags.push(Tag {
178                    category: TagCategory::Query,
179                    data: None,
180                }),
181                "{remote}" => tags.push(Tag {
182                    category: TagCategory::Remote,
183                    data: None,
184                }),
185                "{client_ip}" => tags.push(Tag {
186                    category: TagCategory::ClientIp,
187                    data: None,
188                }),
189                "{scheme}" => tags.push(Tag {
190                    category: TagCategory::Scheme,
191                    data: None,
192                }),
193                "{uri}" => tags.push(Tag {
194                    category: TagCategory::Uri,
195                    data: None,
196                }),
197                "{referer}" => tags.push(Tag {
198                    category: TagCategory::Referrer,
199                    data: None,
200                }),
201                "{user_agent}" => tags.push(Tag {
202                    category: TagCategory::UserAgent,
203                    data: None,
204                }),
205                "{when}" => tags.push(Tag {
206                    category: TagCategory::When,
207                    data: None,
208                }),
209                "{when_utc_iso}" => tags.push(Tag {
210                    category: TagCategory::WhenUtcIso,
211                    data: None,
212                }),
213                "{when_unix}" => tags.push(Tag {
214                    category: TagCategory::WhenUnix,
215                    data: None,
216                }),
217                "{size}" => tags.push(Tag {
218                    category: TagCategory::Size,
219                    data: None,
220                }),
221                "{size_human}" => tags.push(Tag {
222                    category: TagCategory::SizeHuman,
223                    data: None,
224                }),
225                "{status}" => tags.push(Tag {
226                    category: TagCategory::Status,
227                    data: None,
228                }),
229                "{latency}" => tags.push(Tag {
230                    category: TagCategory::Latency,
231                    data: None,
232                }),
233                "{latency_human}" => tags.push(Tag {
234                    category: TagCategory::LatencyHuman,
235                    data: None,
236                }),
237                "{payload_size}" => tags.push(Tag {
238                    category: TagCategory::PayloadSize,
239                    data: None,
240                }),
241                "{payload_size_human}" => tags.push(Tag {
242                    category: TagCategory::PayloadSizeHuman,
243                    data: None,
244                }),
245                "{request_id}" => tags.push(Tag {
246                    category: TagCategory::RequestId,
247                    data: None,
248                }),
249                _ => {
250                    if let Some(tag) = format_extra_tag(key) {
251                        tags.push(tag);
252                    }
253                },
254            }
255
256            end = result.end();
257            current = result.start() + 1;
258        }
259        if end < value.len() {
260            tags.push(Tag {
261                category: TagCategory::Fill,
262                data: Some(value.substring(end, value.len()).to_string()),
263            });
264        }
265        let needs_timestamp = tags.iter().any(|t| {
266            matches!(
267                t.category,
268                TagCategory::When
269                    | TagCategory::WhenUtcIso
270                    | TagCategory::WhenUnix
271                    | TagCategory::Latency
272                    | TagCategory::LatencyHuman
273            )
274        });
275        let capacity = if *LOG_CAPACITY > 0 {
276            *LOG_CAPACITY
277        } else {
278            Parser::estimate_capacity(&tags)
279        };
280        Parser {
281            capacity,
282            tags,
283            needs_timestamp,
284        }
285    }
286}
287
288fn get_resp_header_value<'a>(
289    resp_header: &'a ResponseHeader,
290    key: &str,
291) -> Option<&'a [u8]> {
292    resp_header.headers.get(key).map(|v| v.as_bytes())
293}
294
295static LOG_CAPACITY: LazyLock<usize> = LazyLock::new(|| {
296    std::env::var("PINGAP_ACCESS_LOG_CAPACITY")
297        .unwrap_or_default()
298        .parse::<usize>()
299        .unwrap_or_default()
300});
301
302impl Parser {
303    // Add a method to estimate capacity based on tag types
304    fn estimate_capacity(tags: &[Tag]) -> usize {
305        // Base size plus estimation for each tag type
306        let mut size = 128; // Base size
307        for tag in tags {
308            size += match tag.category {
309                TagCategory::Fill => tag.data.as_ref().map_or(0, |s| s.len()),
310                TagCategory::Uri | TagCategory::Path => 64, // URIs can be long
311                TagCategory::UserAgent => 100, // User agents are often long
312                // Add more specific estimates for other tag types
313                _ => 16, // Default estimate for other tags
314            };
315        }
316        size
317    }
318    // Formats a log entry based on the session and context
319    pub fn format(&self, session: &Session, ctx: &Ctx) -> BytesMut {
320        // Better capacity estimation based on tag types and count
321        let mut buf = BytesMut::with_capacity(self.capacity);
322        let req_header = session.req_header();
323
324        // Then only calculate if needed
325        let (now, instant) = if self.needs_timestamp {
326            let n = Utc::now();
327            (Some(n), Some(Instant::now()))
328        } else {
329            (None, None)
330        };
331
332        const EMPTY_FIELD: &[u8] = b"-";
333
334        // Process each tag in the format string
335        for tag in self.tags.iter() {
336            match tag.category {
337                TagCategory::Fill => {
338                    // Static text, just append it
339                    if let Some(data) = &tag.data {
340                        buf.extend_from_slice(data.as_bytes());
341                    }
342                },
343                TagCategory::Host => {
344                    // Add the host from request headers
345                    match pingap_core::get_host(req_header) {
346                        Some(host) if !host.is_empty() => {
347                            buf.extend_from_slice(host.as_bytes())
348                        },
349                        _ => buf.extend_from_slice(EMPTY_FIELD),
350                    }
351                },
352                TagCategory::Method => {
353                    let method = req_header.method.as_str();
354                    if method.is_empty() {
355                        buf.extend_from_slice(EMPTY_FIELD);
356                    } else {
357                        buf.extend_from_slice(method.as_bytes());
358                    }
359                },
360                TagCategory::Path => {
361                    let path = req_header.uri.path();
362                    if path.is_empty() {
363                        buf.extend_from_slice(EMPTY_FIELD);
364                    } else {
365                        buf.extend_from_slice(path.as_bytes());
366                    }
367                },
368                TagCategory::Proto => {
369                    if session.is_http2() {
370                        buf.extend_from_slice(b"HTTP/2.0");
371                    } else {
372                        buf.extend_from_slice(b"HTTP/1.1");
373                    }
374                },
375                TagCategory::Query => match req_header.uri.query() {
376                    Some(query) if !query.is_empty() => {
377                        buf.extend_from_slice(query.as_bytes())
378                    },
379                    _ => buf.extend_from_slice(EMPTY_FIELD),
380                },
381                TagCategory::Remote => match &ctx.conn.remote_addr {
382                    Some(addr) if !addr.is_empty() => {
383                        buf.extend_from_slice(addr.as_bytes())
384                    },
385                    _ => buf.extend_from_slice(EMPTY_FIELD),
386                },
387                TagCategory::ClientIp => {
388                    if let Some(client_ip) = &ctx.conn.client_ip {
389                        if client_ip.is_empty() {
390                            buf.extend_from_slice(EMPTY_FIELD);
391                        } else {
392                            buf.extend_from_slice(client_ip.as_bytes());
393                        }
394                    } else {
395                        let client_ip = pingap_core::get_client_ip(session);
396                        if client_ip.is_empty() {
397                            buf.extend_from_slice(EMPTY_FIELD);
398                        } else {
399                            buf.extend_from_slice(client_ip.as_bytes());
400                        }
401                    }
402                },
403                TagCategory::Scheme => {
404                    if ctx.conn.tls_version.is_some() {
405                        buf.extend_from_slice(b"https");
406                    } else {
407                        buf.extend_from_slice(b"http");
408                    }
409                },
410                TagCategory::Uri => match req_header.uri.path_and_query() {
411                    Some(value) if !value.as_str().is_empty() => {
412                        buf.extend_from_slice(value.as_str().as_bytes())
413                    },
414                    _ => buf.extend_from_slice(EMPTY_FIELD),
415                },
416                TagCategory::Referrer => {
417                    let value = session.get_header_bytes("referer");
418                    if value.is_empty() {
419                        buf.extend_from_slice(EMPTY_FIELD);
420                    } else {
421                        buf.extend_from_slice(value);
422                    }
423                },
424                TagCategory::UserAgent => {
425                    let value = session.get_header_bytes("user-agent");
426                    if value.is_empty() {
427                        buf.extend_from_slice(EMPTY_FIELD);
428                    } else {
429                        buf.extend_from_slice(value);
430                    }
431                },
432                TagCategory::When => {
433                    if let Some(now) = &now {
434                        buf.extend_from_slice(
435                            now.with_timezone(&Local)
436                                .to_rfc3339_opts(SecondsFormat::Millis, false)
437                                .as_bytes(),
438                        );
439                    } else {
440                        buf.extend_from_slice(EMPTY_FIELD);
441                    }
442                },
443                TagCategory::WhenUtcIso => {
444                    if let Some(now) = &now {
445                        buf.extend_from_slice(
446                            now.to_rfc3339_opts(SecondsFormat::Millis, true)
447                                .as_bytes(),
448                        );
449                    } else {
450                        buf.extend_from_slice(EMPTY_FIELD);
451                    }
452                },
453                TagCategory::WhenUnix => {
454                    if let Some(now) = &now {
455                        buf.extend_from_slice(
456                            itoa::Buffer::new()
457                                .format(now.timestamp_millis())
458                                .as_bytes(),
459                        );
460                    } else {
461                        buf.extend_from_slice(EMPTY_FIELD);
462                    }
463                },
464                TagCategory::Size => {
465                    buf.extend_from_slice(
466                        itoa::Buffer::new()
467                            .format(session.body_bytes_sent())
468                            .as_bytes(),
469                    );
470                },
471                TagCategory::SizeHuman => {
472                    format_byte_size(&mut buf, session.body_bytes_sent());
473                },
474                TagCategory::Status => {
475                    if let Some(status) = &ctx.state.status {
476                        buf.extend_from_slice(status.as_str().as_bytes());
477                    } else {
478                        buf.extend_from_slice(b"-");
479                    }
480                },
481                TagCategory::Latency => {
482                    if let Some(instant) = instant {
483                        let ms = (instant - ctx.timing.created_at).as_millis();
484                        buf.extend_from_slice(
485                            itoa::Buffer::new().format(ms).as_bytes(),
486                        );
487                    } else {
488                        buf.extend_from_slice(EMPTY_FIELD);
489                    }
490                },
491                TagCategory::LatencyHuman => {
492                    if let Some(instant) = instant {
493                        let ms = (instant - ctx.timing.created_at).as_millis();
494                        format_duration(&mut buf, ms as u64);
495                    } else {
496                        buf.extend_from_slice(EMPTY_FIELD);
497                    }
498                },
499                TagCategory::Cookie => {
500                    if let Some(cookie) = &tag.data {
501                        if let Some(value) =
502                            pingap_core::get_cookie_value(req_header, cookie)
503                        {
504                            buf.extend_from_slice(value.as_bytes());
505                        }
506                    } else {
507                        buf.extend_from_slice(EMPTY_FIELD);
508                    }
509                },
510                TagCategory::RequestHeader => {
511                    if let Some(key) = &tag.data {
512                        match req_header.headers.get(key) {
513                            Some(value) if !value.is_empty() => {
514                                buf.extend_from_slice(value.as_bytes())
515                            },
516                            _ => buf.extend_from_slice(EMPTY_FIELD),
517                        }
518                    } else {
519                        buf.extend_from_slice(EMPTY_FIELD);
520                    }
521                },
522                TagCategory::ResponseHeader => {
523                    if let Some(resp_header) = session.response_written() {
524                        if let Some(key) = &tag.data {
525                            match get_resp_header_value(resp_header, key) {
526                                Some(value) if !value.is_empty() => {
527                                    buf.extend_from_slice(value)
528                                },
529                                _ => buf.extend_from_slice(EMPTY_FIELD),
530                            }
531                        } else {
532                            buf.extend_from_slice(EMPTY_FIELD);
533                        }
534                    } else {
535                        buf.extend_from_slice(EMPTY_FIELD);
536                    }
537                },
538                TagCategory::PayloadSize => {
539                    buf.extend_from_slice(
540                        itoa::Buffer::new()
541                            .format(ctx.state.payload_size)
542                            .as_bytes(),
543                    );
544                },
545                TagCategory::PayloadSizeHuman => {
546                    format_byte_size(&mut buf, ctx.state.payload_size);
547                },
548                TagCategory::RequestId => {
549                    if let Some(key) = &ctx.state.request_id {
550                        buf.extend_from_slice(key.as_bytes());
551                    } else {
552                        buf.extend_from_slice(EMPTY_FIELD);
553                    }
554                },
555                TagCategory::Context => {
556                    if let Some(key) = &tag.data {
557                        ctx.append_log_value(&mut buf, key.as_str());
558                    } else {
559                        buf.extend_from_slice(EMPTY_FIELD);
560                    }
561                },
562            };
563        }
564
565        buf
566    }
567}
568
569/// Parse the access log directive
570///
571/// # Arguments
572///
573/// * `access_log` - The access log directive
574///
575/// # Returns
576///
577/// * `(access_log, path)` - The access log directive and the path
578///
579/// # Examples
580///
581/// ```
582/// use pingap_logger::parse_access_log_directive;
583/// let (access_log, path) = parse_access_log_directive(Some(
584///     &"{when} {host} {method} {path}".to_string(),
585/// ));
586/// assert_eq!(Some("{when} {host} {method} {path}".to_string()), access_log);
587/// assert_eq!(None, path);
588/// ```
589///
590/// ```
591/// use pingap_logger::parse_access_log_directive;
592/// let (access_log, path) = parse_access_log_directive(Some(
593///     &"/var/log/pingap.log {when} {host} {method} {path}".to_string(),
594/// ));
595/// assert_eq!(Some("{when} {host} {method} {path}".to_string()), access_log);
596/// assert_eq!(Some("/var/log/pingap.log".to_string()), path);
597/// ```
598pub fn parse_access_log_directive(
599    access_log: Option<&String>,
600) -> (Option<String>, Option<String>) {
601    let default_value = (access_log.cloned(), None);
602    let Some(access_log) = access_log else {
603        return default_value;
604    };
605    if access_log.starts_with('{') {
606        return default_value;
607    }
608    let Some((path, access)) = access_log.split_once(' ') else {
609        return default_value;
610    };
611
612    if !["combined", "common", "short", "tiny"].contains(&access)
613        && !access.starts_with('{')
614    {
615        return default_value;
616    }
617
618    (Some(access.to_string()), Some(path.to_string()))
619}
620
621#[cfg(test)]
622mod tests {
623    use super::{
624        Parser, Tag, TagCategory, format_extra_tag, get_resp_header_value,
625        parse_access_log_directive,
626    };
627    use http::Method;
628    use pingap_core::{
629        ConnectionInfo, Ctx, RequestState, Timing, UpstreamInfo,
630    };
631    use pingora::{http::ResponseHeader, proxy::Session};
632    use pretty_assertions::assert_eq;
633    use tokio_test::io::Builder;
634
635    #[test]
636    fn test_parse_access_log_directive() {
637        let (access, path) = parse_access_log_directive(Some(
638            &"{when} {host} {method} {path}".to_string(),
639        ));
640        assert_eq!(Some("{when} {host} {method} {path}".to_string()), access);
641        assert_eq!(None, path);
642
643        let (access, path) = parse_access_log_directive(Some(
644            &"/var/log/pingap.log {when} {host} {method} {path}".to_string(),
645        ));
646        assert_eq!(Some("{when} {host} {method} {path}".to_string()), access);
647        assert_eq!(Some("/var/log/pingap.log".to_string()), path);
648    }
649
650    #[test]
651    fn test_format_extra_tag() {
652        assert_eq!(true, format_extra_tag(":").is_none());
653
654        let cookie = format_extra_tag("{~deviceId}").unwrap();
655        assert_eq!(TagCategory::Cookie, cookie.category);
656        assert_eq!("deviceId", cookie.data.unwrap());
657
658        let req_header = format_extra_tag("{>X-User}").unwrap();
659        assert_eq!(TagCategory::RequestHeader, req_header.category);
660        assert_eq!("X-User", req_header.data.unwrap());
661
662        let resp_header = format_extra_tag("{<X-Response-Id}").unwrap();
663        assert_eq!(TagCategory::ResponseHeader, resp_header.category);
664        assert_eq!("X-Response-Id", resp_header.data.unwrap());
665
666        let hostname = format_extra_tag("{$hostname}").unwrap();
667        assert_eq!(TagCategory::Fill, hostname.category);
668        assert_eq!(false, hostname.data.unwrap().is_empty());
669
670        let env = format_extra_tag("{$HOME}").unwrap();
671        assert_eq!(TagCategory::Fill, env.category);
672        assert_eq!(false, env.data.unwrap().is_empty());
673    }
674    #[test]
675    fn test_parse_format() {
676        let tests = [
677            (
678                "{host}",
679                Tag {
680                    category: TagCategory::Host,
681                    data: None,
682                },
683            ),
684            (
685                "{method}",
686                Tag {
687                    category: TagCategory::Method,
688                    data: None,
689                },
690            ),
691            (
692                "{path}",
693                Tag {
694                    category: TagCategory::Path,
695                    data: None,
696                },
697            ),
698            (
699                "{proto}",
700                Tag {
701                    category: TagCategory::Proto,
702                    data: None,
703                },
704            ),
705            (
706                "{query}",
707                Tag {
708                    category: TagCategory::Query,
709                    data: None,
710                },
711            ),
712            (
713                "{remote}",
714                Tag {
715                    category: TagCategory::Remote,
716                    data: None,
717                },
718            ),
719            (
720                "{client_ip}",
721                Tag {
722                    category: TagCategory::ClientIp,
723                    data: None,
724                },
725            ),
726            (
727                "{scheme}",
728                Tag {
729                    category: TagCategory::Scheme,
730                    data: None,
731                },
732            ),
733            (
734                "{uri}",
735                Tag {
736                    category: TagCategory::Uri,
737                    data: None,
738                },
739            ),
740            (
741                "{referer}",
742                Tag {
743                    category: TagCategory::Referrer,
744                    data: None,
745                },
746            ),
747            (
748                "{user_agent}",
749                Tag {
750                    category: TagCategory::UserAgent,
751                    data: None,
752                },
753            ),
754            (
755                "{when}",
756                Tag {
757                    category: TagCategory::When,
758                    data: None,
759                },
760            ),
761            (
762                "{when_utc_iso}",
763                Tag {
764                    category: TagCategory::WhenUtcIso,
765                    data: None,
766                },
767            ),
768            (
769                "{when_unix}",
770                Tag {
771                    category: TagCategory::WhenUnix,
772                    data: None,
773                },
774            ),
775            (
776                "{size}",
777                Tag {
778                    category: TagCategory::Size,
779                    data: None,
780                },
781            ),
782            (
783                "{size_human}",
784                Tag {
785                    category: TagCategory::SizeHuman,
786                    data: None,
787                },
788            ),
789            (
790                "{status}",
791                Tag {
792                    category: TagCategory::Status,
793                    data: None,
794                },
795            ),
796            (
797                "{latency}",
798                Tag {
799                    category: TagCategory::Latency,
800                    data: None,
801                },
802            ),
803            (
804                "{latency_human}",
805                Tag {
806                    category: TagCategory::LatencyHuman,
807                    data: None,
808                },
809            ),
810            (
811                "{payload_size}",
812                Tag {
813                    category: TagCategory::PayloadSize,
814                    data: None,
815                },
816            ),
817            (
818                "{payload_size_human}",
819                Tag {
820                    category: TagCategory::PayloadSizeHuman,
821                    data: None,
822                },
823            ),
824            (
825                "{request_id}",
826                Tag {
827                    category: TagCategory::RequestId,
828                    data: None,
829                },
830            ),
831        ];
832
833        for (value, tag) in tests {
834            let p = Parser::from(value);
835            assert_eq!(tag.category, p.tags[0].category);
836        }
837    }
838
839    #[tokio::test]
840    async fn test_logger() {
841        let p: Parser =
842            "{host} {method} {path} {proto} {query} {remote} {client_ip} \
843{scheme} {uri} {referer} {user_agent} {size} \
844{size_human} {status} {payload_size} {payload_size_human} \
845{~deviceId} {>accept} {:upstream_reused} {:upstream_addr} \
846{:processing} {:upstream_connect_time_human} {:location} \
847{:connection_time_human} {:tls_version} {request_id}"
848                .into();
849        let headers = [
850            "Host: github.com",
851            "User-Agent: pingap/0.1.1",
852            "Cookie: deviceId=abc",
853            "Accept: application/json",
854        ]
855        .join("\r\n");
856        let input_header =
857            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
858        let mock_io = Builder::new().read(input_header.as_bytes()).build();
859
860        let mut session = Session::new_h1(Box::new(mock_io));
861        session.read_request().await.unwrap();
862        assert_eq!(Method::GET, session.req_header().method);
863
864        let ctx = Ctx {
865            conn: ConnectionInfo {
866                remote_addr: Some("10.1.1.1".to_string()),
867                client_ip: Some("1.1.1.1".to_string()),
868                tls_version: Some("1.2".to_string()),
869                ..Default::default()
870            },
871            upstream: UpstreamInfo {
872                reused: true,
873                address: "192.186.1.1:6188".to_string(),
874                location: "test".to_string().into(),
875                ..Default::default()
876            },
877            timing: Timing {
878                connection_duration: 300,
879                upstream_connect: Some(100),
880                ..Default::default()
881            },
882            state: RequestState {
883                request_id: Some("nanoid".to_string()),
884                processing_count: 1,
885                ..Default::default()
886            },
887            ..Default::default()
888        };
889        let log = p.format(&session, &ctx);
890        assert_eq!(
891            "github.com GET /vicanso/pingap HTTP/1.1 size=1 10.1.1.1 1.1.1.1 https /vicanso/pingap?size=1 - pingap/0.1.1 0 0B - 0 0B abc application/json true 192.186.1.1:6188 1 100ms test 300ms 1.2 nanoid",
892            log
893        );
894
895        let p: Parser = "{when_utc_iso}".into();
896        let log = p.format(&session, &ctx);
897        assert_eq!(true, log.len() > 20);
898
899        let p: Parser = "{when}".into();
900        let log = p.format(&session, &ctx);
901        assert_eq!(true, log.len() > 20);
902
903        let p: Parser = "{when_unix}".into();
904        let log = p.format(&session, &ctx);
905        assert_eq!(true, log.len() == 13);
906    }
907
908    #[test]
909    fn test_get_resp_header_value() {
910        let mut header =
911            ResponseHeader::build_no_case(200, Some(1024)).unwrap();
912        header
913            .append_header("Content-Type", "application/json")
914            .unwrap();
915        let value = get_resp_header_value(&header, "content-type");
916        assert_eq!(
917            "application/json",
918            std::str::from_utf8(value.unwrap()).unwrap()
919        );
920
921        let value = get_resp_header_value(&header, "content-type-not-exists");
922        assert_eq!(None, value);
923    }
924}