Skip to main content

pingap_core/
ctx.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 crate::{Ja4Fingerprint, Plugin, now_ms};
16use ahash::AHashMap;
17use bytes::BytesMut;
18use http::StatusCode;
19use http::Uri;
20use http::{HeaderName, HeaderValue};
21#[cfg(feature = "tracing")]
22use opentelemetry::{
23    Context,
24    global::{BoxedSpan, BoxedTracer, ObjectSafeSpan},
25    trace::{SpanKind, TraceContextExt, Tracer},
26};
27use pingora::cache::CacheKey;
28use pingora::http::RequestHeader;
29use pingora::protocols::Digest;
30use pingora::protocols::TimingDigest;
31use pingora::proxy::Session;
32use pingora_limits::inflight::Guard;
33use std::borrow::Cow;
34use std::fmt::Write;
35use std::sync::Arc;
36use std::time::{Duration, Instant, SystemTime};
37use strum::EnumString;
38
39// Constants for time conversions in milliseconds.
40const SECOND: u64 = 1_000;
41const MINUTE: u64 = 60 * SECOND;
42const HOUR: u64 = 60 * MINUTE;
43
44#[inline]
45/// Format the duration in human readable format, checking smaller units first.
46/// e.g., ms, s, m, h.
47pub fn format_duration(buf: &mut BytesMut, ms: u64) {
48    if ms < SECOND {
49        // Format as milliseconds if less than a second.
50        buf.extend_from_slice(itoa::Buffer::new().format(ms).as_bytes());
51        buf.extend_from_slice(b"ms");
52    } else if ms < MINUTE {
53        // Format as seconds with one decimal place if less than a minute.
54        buf.extend_from_slice(
55            itoa::Buffer::new().format(ms / SECOND).as_bytes(),
56        );
57        let value = (ms % SECOND) / 100;
58        if value != 0 {
59            buf.extend_from_slice(b".");
60            buf.extend_from_slice(itoa::Buffer::new().format(value).as_bytes());
61        }
62        buf.extend_from_slice(b"s");
63    } else if ms < HOUR {
64        // Format as minutes with one decimal place if less than an hour.
65        buf.extend_from_slice(
66            itoa::Buffer::new().format(ms / MINUTE).as_bytes(),
67        );
68        let value = ms % MINUTE * 10 / MINUTE;
69        if value != 0 {
70            buf.extend_from_slice(b".");
71            buf.extend_from_slice(itoa::Buffer::new().format(value).as_bytes());
72        }
73        buf.extend_from_slice(b"m");
74    } else {
75        // Format as hours with one decimal place.
76        buf.extend_from_slice(itoa::Buffer::new().format(ms / HOUR).as_bytes());
77        let value = ms % HOUR * 10 / HOUR;
78        if value != 0 {
79            buf.extend_from_slice(b".");
80            buf.extend_from_slice(itoa::Buffer::new().format(value).as_bytes());
81        }
82        buf.extend_from_slice(b"h");
83    }
84}
85
86#[derive(PartialEq)]
87pub enum ModifiedMode {
88    Upstream,
89    Response,
90}
91
92impl From<&str> for ModifiedMode {
93    fn from(value: &str) -> Self {
94        match value {
95            "upstream" => ModifiedMode::Upstream,
96            _ => ModifiedMode::Response,
97        }
98    }
99}
100
101/// Trait for modifying the response body.
102pub trait ModifyResponseBody: Sync + Send {
103    /// Handles the modification of response body data.
104    fn handle(
105        &mut self,
106        session: &Session,
107        body: &mut Option<bytes::Bytes>,
108        end_of_stream: bool,
109    ) -> pingora::Result<()>;
110    /// Returns the name of the modifier.
111    fn name(&self) -> &str {
112        "unknown"
113    }
114}
115
116/// Information about a single client connection.
117#[derive(Default)]
118pub struct ConnectionInfo {
119    /// A unique identifier for the connection.
120    pub id: usize,
121    /// The IP address of the client.
122    pub client_ip: Option<String>,
123    /// The remote address of the client connection.
124    pub remote_addr: Option<String>,
125    /// The remote port of the client connection.
126    pub remote_port: Option<u16>,
127    /// The server address the client connected to.
128    pub server_addr: Option<String>,
129    /// The server port the client connected to.
130    pub server_port: Option<u16>,
131    /// The TLS version used for the connection, if any.
132    ///
133    /// Stored as `Cow<'static, str>` so values borrowed from pingora's
134    /// `SslDigest` (usually `&'static str`) do not allocate per request.
135    pub tls_version: Option<Cow<'static, str>>,
136    /// The TLS cipher used for the connection, if any.
137    pub tls_cipher: Option<Cow<'static, str>>,
138    /// The JA4 fingerprint of the client's ClientHello, when the server
139    /// collects it. Shared by every request on the connection.
140    pub ja4: Option<Arc<Ja4Fingerprint>>,
141    /// Indicates whether the connection was reused (e.g., HTTP keep-alive).
142    pub reused: bool,
143}
144
145/// All timing-related metrics for the request lifecycle.
146// #[derive(Default)]
147pub struct Timing {
148    /// Timestamp in milliseconds when the request was created.
149    pub created_at: Instant,
150    /// The total duration of the client connection in milliseconds.
151    /// May be large for reused connections.
152    pub connection_duration: u64,
153    /// The duration of the TLS handshake with the client in milliseconds.
154    pub tls_handshake: Option<i32>,
155    /// The total duration to connect to the upstream server in milliseconds.
156    pub upstream_connect: Option<i32>,
157    /// The duration of the TCP connection to the upstream server in milliseconds.
158    pub upstream_tcp_connect: Option<i32>,
159    /// The duration of the TLS handshake with the upstream server in milliseconds.
160    pub upstream_tls_handshake: Option<i32>,
161    /// How long the upstream connect waited for an offload thread before it
162    /// began, in milliseconds. Only present when
163    /// `basic.upstream_connect_offload_*` is on; it separates scheduling
164    /// delay in the offload pool from network latency.
165    pub upstream_connect_offload_wait: Option<i32>,
166    /// The duration the upstream server took to process the request in milliseconds.
167    pub upstream_processing: Option<i32>,
168    /// The duration from sending the request to receiving the upstream response in milliseconds.
169    pub upstream_response: Option<i32>,
170    /// The total duration of the upstream connection in milliseconds.
171    pub upstream_connection_duration: Option<u64>,
172    /// The duration of the cache lookup in milliseconds.
173    pub cache_lookup: Option<i32>,
174    /// The duration spent waiting for a cache lock in milliseconds.
175    pub cache_lock: Option<i32>,
176}
177
178impl Default for Timing {
179    fn default() -> Self {
180        Self {
181            created_at: Instant::now(),
182            connection_duration: 0,
183            tls_handshake: None,
184            upstream_connect: None,
185            upstream_tcp_connect: None,
186            upstream_tls_handshake: None,
187            upstream_connect_offload_wait: None,
188            upstream_processing: None,
189            upstream_response: None,
190            upstream_connection_duration: None,
191            cache_lookup: None,
192            cache_lock: None,
193        }
194    }
195}
196
197/// Trait for upstream instance, used to handle the upstream instance lifecycle.
198pub trait UpstreamInstance: Send + Sync {
199    fn on_transport_failure(&self, address: &str);
200    fn on_response(&self, address: &str, status: StatusCode);
201    /// Marks the request as finished on this upstream.
202    ///
203    /// Returns the number of in-flight requests still being processed *after*
204    /// releasing this one (so a zero means the upstream is idle).
205    fn completed(&self) -> i32;
206}
207
208/// Trait for location instance
209pub trait LocationInstance: Send + Sync {
210    /// Get location's name
211    fn name(&self) -> &str;
212    /// Get the upstream of location
213    fn upstream(&self) -> &str;
214    /// Rewrites the request url. Returns whether the path changed; named
215    /// captures of the rewrite pattern are added to `variables`.
216    fn rewrite(
217        &self,
218        header: &mut RequestHeader,
219        variables: &mut Option<AHashMap<String, String>>,
220    ) -> bool;
221    /// Returns the proxy header to upstream
222    fn headers(&self) -> Option<&Vec<(HeaderName, HeaderValue, bool)>>;
223    /// Returns the client body size limit
224    fn client_body_size_limit(&self) -> usize;
225    /// Called when the request is received from the client
226    /// Returns
227    /// `Result<(u64, i32)>` - A tuple containing:
228    ///   - The new total number of accepted requests (u64)
229    ///   - The new number of currently processing requests (i32)
230    fn on_request(&self) -> pingora::Result<(u64, i32)>;
231    /// Called when the response is received from the upstream
232    fn on_response(&self);
233}
234
235/// Information about the upstream (backend) server.
236#[derive(Default)]
237pub struct UpstreamInfo {
238    /// Upstream instance
239    pub upstream_instance: Option<Arc<dyn UpstreamInstance>>,
240    /// Location instance
241    pub location_instance: Option<Arc<dyn LocationInstance>>,
242    /// The location (route) that directed the request to this upstream.
243    pub location: Arc<str>,
244    /// The name of the upstream server or group.
245    pub name: Arc<str>,
246    /// The address of the upstream server.
247    pub address: String,
248    /// Indicates if the connection to the upstream was reused.
249    pub reused: bool,
250    /// The number of requests currently being processed by the upstream.
251    pub processing_count: Option<i32>,
252    /// The current number of active connections to the upstream.
253    pub connected_count: Option<i32>,
254    /// The HTTP status code of upstream response.
255    pub status: Option<StatusCode>,
256    /// The number of retries for failed connections.
257    pub retries: u8,
258    /// Maximum number of retries for failed connections.
259    pub max_retries: Option<u8>,
260    /// The maximum total time window allowed for an operation and all of its subsequent retries.
261    ///
262    /// The timer starts from the beginning of the **initial attempt**. Once this time window
263    /// is exceeded, no more retries will be initiated, even if the maximum number of
264    /// retries (`max_retries`) has not been reached.
265    ///
266    /// If set to `None`, there is no time limit for the retry process.
267    pub max_retry_window: Option<Duration>,
268}
269
270/// State related to the current request being processed.
271#[derive(Default)]
272pub struct RequestState {
273    /// A unique identifier for the request.
274    pub request_id: Option<String>,
275    /// The HTTP status code of the response.
276    pub status: Option<StatusCode>,
277    /// The size of the request payload in bytes.
278    pub payload_size: usize,
279    /// A guard for rate limiting, if applicable.
280    pub guard: Option<Guard>,
281    /// The total number of requests currently being processed by the service.
282    pub processing_count: i32,
283    /// The total number of requests accepted by the service.
284    pub accepted_count: u64,
285    /// The number of requests currently being processed for this location.
286    pub location_processing_count: i32,
287    /// The total number of requests accepted for this location.
288    pub location_accepted_count: u64,
289}
290
291/// All cache-related configuration and statistics for a request.
292#[derive(Default)]
293pub struct CacheInfo {
294    /// The namespace for cache entries.
295    pub namespace: Option<String>,
296    /// The list of keys used to generate the final cache key.
297    pub keys: Option<Vec<String>>,
298    /// Whether to respect Cache-Control headers.
299    pub check_cache_control: bool,
300    /// The maximum time-to-live for cache entries.
301    pub max_ttl: Option<Duration>,
302    /// Request headers the origin's `Vary` response header may turn into
303    /// cache variants (lowercased); `None` honours every header it names.
304    pub vary_headers: Option<Arc<Vec<String>>>,
305    /// The number of cache read operations performed.
306    pub reading_count: Option<u32>,
307    /// The number of cache write operations performed.
308    pub writing_count: Option<u32>,
309}
310
311/// Optional features like tracing, plugins, and response modifications.
312#[derive(Default)]
313pub struct Features {
314    /// A map of custom variables for request processing.
315    pub variables: Option<AHashMap<String, String>>,
316    /// A list of plugin names and their processing times in milliseconds.
317    pub plugin_processing_times: Option<Vec<(Arc<str>, u32)>>,
318    /// Statistics about response compression.
319    pub compression_stat: Option<CompressionStat>,
320    /// A map of plugin names and their response body handlers.
321    pub modify_body_handlers:
322        Option<AHashMap<String, Box<dyn ModifyResponseBody>>>,
323    /// OpenTelemetry tracer for distributed tracing (available with the "tracing" feature).
324    #[cfg(feature = "tracing")]
325    pub otel_tracer: Option<OtelTracer>,
326    /// OpenTelemetry span for the upstream request (available with the "tracing" feature).
327    #[cfg(feature = "tracing")]
328    pub upstream_span: Option<BoxedSpan>,
329}
330
331#[derive(Default)]
332/// Statistics about response compression operations.
333pub struct CompressionStat {
334    /// The algorithm used for compression (e.g., "gzip", "br").
335    pub algorithm: String,
336    /// The size of the data before compression in bytes.
337    pub in_bytes: usize,
338    /// The size of the data after compression in bytes.
339    pub out_bytes: usize,
340    /// The time taken to perform the compression operation.
341    pub duration: Duration,
342}
343
344impl CompressionStat {
345    /// Calculates the compression ratio.
346    pub fn ratio(&self) -> f64 {
347        if self.out_bytes == 0 {
348            return 0.0;
349        }
350        (self.in_bytes as f64) / (self.out_bytes as f64)
351    }
352}
353
354/// A wrapper for OpenTelemetry tracing components.
355#[cfg(feature = "tracing")]
356pub struct OtelTracer {
357    /// The tracer instance.
358    pub tracer: BoxedTracer,
359    /// The main span for the incoming HTTP request.
360    pub http_request_span: BoxedSpan,
361}
362
363#[cfg(feature = "tracing")]
364impl OtelTracer {
365    /// Creates a new child span for an upstream request.
366    #[inline]
367    pub fn new_upstream_span(&self, name: &str) -> BoxedSpan {
368        self.tracer
369            .span_builder(name.to_string())
370            .with_kind(SpanKind::Client)
371            .start_with_context(
372                &self.tracer,
373                // Set the parent span context to link this upstream span with the main request span.
374                &Context::current().with_remote_span_context(
375                    self.http_request_span.span_context().clone(),
376                ),
377            )
378    }
379}
380
381/// A plugin paired with the name it was registered under in the location config.
382pub type NamedPlugin = (Arc<str>, Arc<dyn Plugin>);
383
384/// A `Ctx` value the access log can print. The `{:name}` in a log format
385/// parses into one of these once, when the format is read, so writing a
386/// log line dispatches on an enum instead of comparing the name against
387/// every key on every request.
388#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, strum::Display)]
389#[strum(serialize_all = "snake_case")]
390pub enum CtxLogField {
391    ConnectionId,
392    UpstreamReused,
393    UpstreamStatus,
394    UpstreamAddr,
395    Processing,
396    UpstreamConnected,
397    UpstreamConnectTime,
398    UpstreamConnectTimeHuman,
399    UpstreamProcessingTime,
400    UpstreamProcessingTimeHuman,
401    UpstreamResponseTime,
402    UpstreamResponseTimeHuman,
403    UpstreamTcpConnectTime,
404    UpstreamTcpConnectTimeHuman,
405    UpstreamTlsHandshakeTime,
406    UpstreamTlsHandshakeTimeHuman,
407    UpstreamConnectOffloadWaitTime,
408    UpstreamConnectOffloadWaitTimeHuman,
409    UpstreamConnectionTime,
410    UpstreamConnectionTimeHuman,
411    ConnectionTime,
412    ConnectionTimeHuman,
413    Location,
414    ConnectionReused,
415    TlsVersion,
416    TlsCipher,
417    Ja4,
418    #[strum(serialize = "ja4_r")]
419    Ja4R,
420    #[strum(serialize = "ja4_o")]
421    Ja4O,
422    #[strum(serialize = "ja4_ro")]
423    Ja4Ro,
424    TlsHandshakeTime,
425    TlsHandshakeTimeHuman,
426    CompressionTime,
427    CompressionTimeHuman,
428    CompressionRatio,
429    CacheLookupTime,
430    CacheLookupTimeHuman,
431    CacheLockTime,
432    CacheLockTimeHuman,
433    ServiceTime,
434    ServiceTimeHuman,
435}
436
437/// Represents the state of a request/response cycle, tracking various metrics and properties
438/// including connection details, caching information, and upstream server interactions.
439#[derive(Default)]
440pub struct Ctx {
441    /// Information about the client connection.
442    pub conn: ConnectionInfo,
443    /// Information about the upstream server.
444    pub upstream: UpstreamInfo,
445    /// Timing metrics for the request lifecycle.
446    pub timing: Timing,
447    /// State related to the current request.
448    pub state: RequestState,
449    /// Cache-related information. Boxed behind an `Option`: most requests
450    /// never touch the cache, and a `None` costs a pointer instead of the
451    /// whole struct on every `Ctx`.
452    pub cache: Option<Box<CacheInfo>>,
453    /// Optional features, boxed for the same reason.
454    pub features: Option<Box<Features>>,
455    /// Plugins for the current location, shared with the location's cache
456    /// of resolved plugins so a request only bumps a reference count.
457    pub plugins: Option<Arc<[NamedPlugin]>>,
458}
459
460/// Helper struct to store connection timing and TLS details
461#[derive(Debug, Default)]
462pub struct DigestDetail {
463    /// A guess at reuse: the connection was established more than 100 ms
464    /// before this request. It is only a guess - a slow client or a long TLS
465    /// handshake trips it on a brand-new connection - so the proxy takes the
466    /// exact keepalive signal pingora gives it for HTTP/1 and applies this
467    /// only to HTTP/2, where streams share one connection with no signal.
468    pub connection_reused: bool,
469    /// Age of the connection in milliseconds, wall clock.
470    pub connection_time: u64,
471    /// Timestamp when TCP connection was established
472    pub tcp_established: u64,
473    /// Timestamp when TLS handshake completed
474    pub tls_established: u64,
475    /// TCP connect time in milliseconds, measured by pingora on a monotonic
476    /// clock. Only a connection this side opened has one; an accepted
477    /// connection reports `None`.
478    pub tcp_connect: Option<u64>,
479    /// TLS handshake time in milliseconds, measured by pingora on a monotonic
480    /// clock around the handshake itself; `None` without TLS or when it was
481    /// not measured.
482    pub tls_handshake: Option<u64>,
483    /// Time the connect spent queued for an offload thread, in milliseconds.
484    /// `None` unless the connect was offloaded.
485    pub connect_offload_wait: Option<u64>,
486    /// TLS protocol version if using HTTPS
487    pub tls_version: Option<Cow<'static, str>>,
488    /// TLS cipher suite in use if using HTTPS
489    pub tls_cipher: Option<Cow<'static, str>>,
490}
491
492#[inline]
493pub(crate) fn timing_to_ms(timing: Option<&Option<TimingDigest>>) -> u64 {
494    match timing {
495        Some(Some(item)) => item
496            .established_ts
497            .duration_since(SystemTime::UNIX_EPOCH)
498            .unwrap_or_default()
499            .as_millis() as u64,
500        _ => 0,
501    }
502}
503
504/// Extracts timing and TLS information from connection digest.
505/// Used for metrics and logging connection details.
506#[inline]
507pub fn get_digest_detail(digest: &Digest) -> DigestDetail {
508    let tcp_established = timing_to_ms(digest.timing_digest.first());
509    let mut connection_time = 0;
510    let now = now_ms();
511    if tcp_established > 0 && tcp_established < now {
512        connection_time = now - tcp_established;
513    }
514    let connection_reused = connection_time > 100;
515    // The first entry is the transport layer, the last one the outermost
516    // layer - the TLS session when there is one, otherwise the transport
517    // layer again, which is why the handshake is only read under TLS.
518    let tcp_connect = establishment_ms(digest.timing_digest.first());
519    let connect_offload_wait = offload_wait_ms(digest.timing_digest.first());
520
521    let Some(ssl_digest) = &digest.ssl_digest else {
522        return DigestDetail {
523            connection_reused,
524            tcp_established,
525            connection_time,
526            tcp_connect,
527            connect_offload_wait,
528            ..Default::default()
529        };
530    };
531
532    DigestDetail {
533        connection_reused,
534        tcp_established,
535        connection_time,
536        tcp_connect,
537        connect_offload_wait,
538        tls_established: timing_to_ms(digest.timing_digest.last()),
539        tls_handshake: establishment_ms(digest.timing_digest.last()),
540        // Clone the Cow: Borrowed(&'static str) is allocation-free.
541        tls_version: Some(ssl_digest.version.clone()),
542        tls_cipher: Some(ssl_digest.cipher.clone()),
543    }
544}
545
546/// The layer's own establishment time, when pingora measured it.
547fn establishment_ms(timing: Option<&Option<TimingDigest>>) -> Option<u64> {
548    timing
549        .and_then(|item| item.as_ref())
550        .and_then(|item| item.establishment_duration)
551        .map(|duration| duration.as_millis() as u64)
552}
553
554/// How long the transport connect waited for an offload thread, when it was
555/// offloaded at all.
556fn offload_wait_ms(timing: Option<&Option<TimingDigest>>) -> Option<u64> {
557    timing
558        .and_then(|item| item.as_ref())
559        .and_then(|item| item.offload_wait_duration)
560        .map(|duration| duration.as_millis() as u64)
561}
562
563impl Ctx {
564    /// Creates a new Ctx instance with the current timestamp and default values.
565    ///
566    /// Returns a new Ctx struct initialized with the current timestamp and all other fields
567    /// set to their default values.
568    pub fn new() -> Self {
569        Self {
570            ..Default::default()
571        }
572    }
573
574    /// Adds a variable to the state's variables map with the given key and value.
575    ///
576    /// # Arguments
577    /// * `key` - The variable name.
578    /// * `value` - The value to store for this variable.
579    #[inline]
580    pub fn add_variable(&mut self, key: &str, value: &str) {
581        // Lazily initialize features and variables map.
582        let features = self.features.get_or_insert_default();
583        let variables = features.variables.get_or_insert_with(AHashMap::new);
584        variables.insert(key.to_string(), value.to_string());
585    }
586
587    /// Extends the variables map with the given key-value pairs.
588    ///
589    /// # Arguments
590    /// * `values` - A HashMap containing the key-value pairs to add.
591    #[inline]
592    pub fn extend_variables(&mut self, values: AHashMap<String, String>) {
593        let features = self.features.get_or_insert_default();
594        if let Some(variables) = features.variables.as_mut() {
595            variables.extend(values);
596        } else {
597            features.variables = Some(values);
598        }
599    }
600
601    /// Returns the value of a variable by key.
602    ///
603    /// # Arguments
604    /// * `key` - The key of the variable to retrieve.
605    ///
606    /// Returns: Option<&str> representing the value of the variable, or None if the variable does not exist.
607    #[inline]
608    pub fn get_variable(&self, key: &str) -> Option<&str> {
609        self.features
610            .as_ref()?
611            .variables
612            .as_ref()?
613            .get(key)
614            .map(|v| v.as_str())
615    }
616
617    /// Adds a modify body handler to the context.
618    ///
619    /// # Arguments
620    /// * `name` - The name of the handler.
621    /// * `handler` - The handler to add.
622    #[inline]
623    pub fn add_modify_body_handler(
624        &mut self,
625        name: &str,
626        handler: Box<dyn ModifyResponseBody>,
627    ) {
628        let features = self.features.get_or_insert_default();
629        let handlers = features
630            .modify_body_handlers
631            .get_or_insert_with(AHashMap::new);
632        handlers.insert(name.to_string(), handler);
633    }
634
635    /// Returns the modify body handler by name.
636    #[inline]
637    pub fn get_modify_body_handler(
638        &mut self,
639        name: &str,
640    ) -> Option<&mut Box<dyn ModifyResponseBody>> {
641        self.features
642            .as_mut()
643            .and_then(|f| f.modify_body_handlers.as_mut())
644            .and_then(|h| h.get_mut(name))
645    }
646
647    // A private helper function to filter out time values that are too large (over an hour),
648    // which might indicate an error or uninitialized state.
649    #[inline]
650    fn get_time_field(&self, field: Option<i32>) -> Option<u32> {
651        if let Some(value) = field
652            && value >= 0
653        {
654            return Some(value as u32);
655        }
656        None
657    }
658
659    /// Returns the upstream response time if it's less than one hour, otherwise None.
660    /// This helps filter out potentially invalid or stale timing data.
661    ///
662    /// Returns: Option<u64> representing milliseconds, or None if time exceeds 1 hour.
663    #[inline]
664    pub fn get_upstream_response_time(&self) -> Option<u32> {
665        self.get_time_field(self.timing.upstream_response)
666    }
667
668    /// Returns the upstream connect time if it's less than one hour, otherwise None.
669    /// This helps filter out potentially invalid or stale timing data.
670    ///
671    /// Returns: Option<u64> representing milliseconds, or None if time exceeds 1 hour.
672    #[inline]
673    pub fn get_upstream_connect_time(&self) -> Option<u32> {
674        self.get_time_field(self.timing.upstream_connect)
675    }
676
677    /// Returns the upstream processing time if it's less than one hour, otherwise None.
678    /// This helps filter out potentially invalid or stale timing data.
679    ///
680    /// Returns: Option<u64> representing milliseconds, or None if time exceeds 1 hour.
681    #[inline]
682    pub fn get_upstream_processing_time(&self) -> Option<u32> {
683        self.get_time_field(self.timing.upstream_processing)
684    }
685
686    /// Adds a plugin processing time to the context.
687    ///
688    /// # Arguments
689    /// * `name` - The name of the plugin.
690    /// * `time` - The time taken by the plugin in milliseconds.
691    #[inline]
692    pub fn add_plugin_processing_time(&mut self, name: &Arc<str>, time: u32) {
693        // Lazily initialize features and the processing times vector.
694        let features = self.features.get_or_insert_default();
695        let times = features
696            .plugin_processing_times
697            .get_or_insert_with(|| Vec::with_capacity(5));
698        if let Some(item) = times.iter_mut().find(|item| &item.0 == name) {
699            item.1 += time;
700        } else {
701            times.push((Arc::clone(name), time));
702        }
703    }
704
705    /// Appends a formatted value to the provided log buffer based on the given key.
706    /// Handles various metrics including connection info, timing data, and TLS details.
707    ///
708    /// Resolves `key` on every call; a caller that knows the key ahead of
709    /// time (the access log format) should parse it into a [`CtxLogField`]
710    /// once and use [`Ctx::append_log_field`].
711    ///
712    /// # Arguments
713    /// * `buf` - The BytesMut buffer to append the value to.
714    /// * `key` - The key identifying which state value to format and append.
715    #[inline]
716    pub fn append_log_value(&self, buf: &mut BytesMut, key: &str) {
717        // Unknown keys append nothing.
718        if let Ok(field) = key.parse::<CtxLogField>() {
719            self.append_log_field(buf, field);
720        }
721    }
722
723    /// Appends the value of `field` to the log buffer.
724    pub fn append_log_field(&self, buf: &mut BytesMut, field: CtxLogField) {
725        // A macro to simplify formatting and appending optional time values.
726        macro_rules! append_time {
727            // Append raw milliseconds.
728            ($val:expr) => {
729                if let Some(ms) = $val {
730                    buf.extend(itoa::Buffer::new().format(ms).as_bytes());
731                }
732            };
733            // Append human-readable formatted time.
734            ($val:expr, human) => {
735                if let Some(ms) = $val {
736                    format_duration(buf, ms as u64);
737                }
738            };
739        }
740
741        match field {
742            CtxLogField::ConnectionId => {
743                buf.extend(itoa::Buffer::new().format(self.conn.id).as_bytes());
744            },
745            CtxLogField::UpstreamReused => {
746                if self.upstream.reused {
747                    buf.extend(b"true");
748                } else {
749                    buf.extend(b"false");
750                }
751            },
752            CtxLogField::UpstreamStatus => {
753                if let Some(status) = &self.upstream.status {
754                    buf.extend_from_slice(status.as_str().as_bytes());
755                } else {
756                    buf.extend_from_slice(b"-");
757                }
758            },
759            CtxLogField::UpstreamAddr => {
760                buf.extend(self.upstream.address.as_bytes())
761            },
762            CtxLogField::Processing => buf.extend(
763                itoa::Buffer::new()
764                    .format(self.state.processing_count)
765                    .as_bytes(),
766            ),
767            CtxLogField::UpstreamConnected => {
768                if let Some(value) = self.upstream.connected_count {
769                    buf.extend(itoa::Buffer::new().format(value).as_bytes());
770                }
771            },
772
773            // Timing fields
774            CtxLogField::UpstreamConnectTime => {
775                append_time!(self.get_upstream_connect_time())
776            },
777            CtxLogField::UpstreamConnectTimeHuman => {
778                append_time!(self.get_upstream_connect_time(), human)
779            },
780
781            CtxLogField::UpstreamProcessingTime => {
782                append_time!(self.get_upstream_processing_time())
783            },
784            CtxLogField::UpstreamProcessingTimeHuman => {
785                append_time!(self.get_upstream_processing_time(), human)
786            },
787            CtxLogField::UpstreamResponseTime => {
788                append_time!(self.get_upstream_response_time())
789            },
790            CtxLogField::UpstreamResponseTimeHuman => {
791                append_time!(self.get_upstream_response_time(), human)
792            },
793            CtxLogField::UpstreamTcpConnectTime => {
794                append_time!(self.timing.upstream_tcp_connect)
795            },
796            CtxLogField::UpstreamTcpConnectTimeHuman => {
797                append_time!(self.timing.upstream_tcp_connect, human)
798            },
799            CtxLogField::UpstreamTlsHandshakeTime => {
800                append_time!(self.timing.upstream_tls_handshake)
801            },
802            CtxLogField::UpstreamTlsHandshakeTimeHuman => {
803                append_time!(self.timing.upstream_tls_handshake, human)
804            },
805            CtxLogField::UpstreamConnectOffloadWaitTime => {
806                append_time!(self.timing.upstream_connect_offload_wait)
807            },
808            CtxLogField::UpstreamConnectOffloadWaitTimeHuman => {
809                append_time!(self.timing.upstream_connect_offload_wait, human)
810            },
811            CtxLogField::UpstreamConnectionTime => {
812                append_time!(self.timing.upstream_connection_duration)
813            },
814            CtxLogField::UpstreamConnectionTimeHuman => {
815                append_time!(self.timing.upstream_connection_duration, human)
816            },
817            CtxLogField::ConnectionTime => {
818                append_time!(Some(self.timing.connection_duration))
819            },
820            CtxLogField::ConnectionTimeHuman => {
821                append_time!(Some(self.timing.connection_duration), human)
822            },
823
824            // Other fields
825            CtxLogField::Location => {
826                if !self.upstream.location.is_empty() {
827                    buf.extend(self.upstream.location.as_bytes())
828                }
829            },
830            CtxLogField::ConnectionReused => {
831                if self.conn.reused {
832                    buf.extend(b"true");
833                } else {
834                    buf.extend(b"false");
835                }
836            },
837            CtxLogField::TlsVersion => {
838                if let Some(value) = &self.conn.tls_version {
839                    buf.extend(value.as_bytes());
840                }
841            },
842            CtxLogField::TlsCipher => {
843                if let Some(value) = &self.conn.tls_cipher {
844                    buf.extend(value.as_bytes());
845                }
846            },
847            CtxLogField::Ja4 => {
848                if let Some(fingerprint) = &self.conn.ja4 {
849                    buf.extend(fingerprint.ja4().as_bytes());
850                }
851            },
852            CtxLogField::Ja4R => {
853                if let Some(fingerprint) = &self.conn.ja4 {
854                    buf.extend(fingerprint.ja4_r().as_bytes());
855                }
856            },
857            CtxLogField::Ja4O => {
858                if let Some(fingerprint) = &self.conn.ja4 {
859                    buf.extend(fingerprint.ja4_o().as_bytes());
860                }
861            },
862            CtxLogField::Ja4Ro => {
863                if let Some(fingerprint) = &self.conn.ja4 {
864                    buf.extend(fingerprint.ja4_ro().as_bytes());
865                }
866            },
867            CtxLogField::TlsHandshakeTime => {
868                append_time!(self.timing.tls_handshake)
869            },
870            CtxLogField::TlsHandshakeTimeHuman => {
871                append_time!(self.timing.tls_handshake, human)
872            },
873            CtxLogField::CompressionTime => {
874                if let Some(feature) = &self.features
875                    && let Some(value) = &feature.compression_stat
876                {
877                    append_time!(Some(value.duration.as_millis() as u64))
878                }
879            },
880            CtxLogField::CompressionTimeHuman => {
881                if let Some(feature) = &self.features
882                    && let Some(value) = &feature.compression_stat
883                {
884                    append_time!(Some(value.duration.as_millis() as u64), human)
885                }
886            },
887            CtxLogField::CompressionRatio => {
888                if let Some(feature) = &self.features
889                    && let Some(value) = &feature.compression_stat
890                {
891                    // One decimal place without allocating via `format!`.
892                    let tenths = (value.ratio() * 10.0).round() as u64;
893                    buf.extend(
894                        itoa::Buffer::new().format(tenths / 10).as_bytes(),
895                    );
896                    buf.extend_from_slice(b".");
897                    buf.extend(
898                        itoa::Buffer::new().format(tenths % 10).as_bytes(),
899                    );
900                }
901            },
902            CtxLogField::CacheLookupTime => {
903                append_time!(self.timing.cache_lookup)
904            },
905            CtxLogField::CacheLookupTimeHuman => {
906                append_time!(self.timing.cache_lookup, human)
907            },
908            CtxLogField::CacheLockTime => {
909                append_time!(self.timing.cache_lock)
910            },
911            CtxLogField::CacheLockTimeHuman => {
912                append_time!(self.timing.cache_lock, human)
913            },
914            CtxLogField::ServiceTime => {
915                append_time!(Some(self.timing.created_at.elapsed().as_millis()))
916            },
917            CtxLogField::ServiceTimeHuman => {
918                append_time!(
919                    Some(self.timing.created_at.elapsed().as_millis()),
920                    human
921                )
922            },
923        }
924    }
925
926    /// Generates a Server-Timing header value based on the context's timing metrics.
927    ///
928    /// The Server-Timing header allows servers to communicate performance metrics
929    /// about the request-response cycle to the client. This implementation includes
930    /// various timing metrics like connection time, processing time, and cache operations.
931    ///
932    /// Returns a String containing the formatted Server-Timing header value.
933    pub fn generate_server_timing(&self) -> String {
934        let mut timing_str = String::with_capacity(200);
935        // Flag to track if this is the first timing entry, to handle commas correctly.
936        let mut first = true;
937
938        // Macro to add a timing entry to the string.
939        macro_rules! add_timing {
940            ($name:expr, $dur:expr) => {
941                if !first {
942                    timing_str.push_str(", ");
943                }
944                // Ignore the write! result as it's unlikely to fail with a String.
945                let _ = write!(&mut timing_str, "{};dur={}", $name, $dur);
946                first = false;
947            };
948        }
949
950        // Aggregate and add upstream timings.
951        let mut upstream_time = 0;
952        if let Some(time) = self.get_upstream_connect_time() {
953            upstream_time += time;
954            add_timing!("upstream.connect", time);
955        }
956        if let Some(time) = self.get_upstream_processing_time() {
957            upstream_time += time;
958            add_timing!("upstream.processing", time);
959        }
960        if upstream_time > 0 {
961            add_timing!("upstream", upstream_time);
962        }
963
964        // Aggregate and add cache timings.
965        let mut cache_time = 0;
966        if let Some(time) = self.timing.cache_lookup {
967            cache_time += time;
968            add_timing!("cache.lookup", time);
969        }
970        if let Some(time) = self.timing.cache_lock {
971            cache_time += time;
972            add_timing!("cache.lock", time);
973        }
974        if cache_time > 0 {
975            add_timing!("cache", cache_time);
976        }
977
978        // Aggregate and add plugin timings.
979        if let Some(features) = &self.features
980            && let Some(times) = &features.plugin_processing_times
981        {
982            let mut plugin_time: u32 = 0;
983            for (name, time) in times {
984                if *time == 0 {
985                    continue;
986                }
987                plugin_time += time;
988                // Write directly into the shared buffer — avoid a per-plugin
989                // temporary `"plugin." + name` String.
990                if !first {
991                    timing_str.push_str(", ");
992                }
993                let _ = write!(&mut timing_str, "plugin.{name};dur={time}");
994                first = false;
995            }
996            if plugin_time > 0 {
997                add_timing!("plugin", plugin_time);
998            }
999        }
1000
1001        // Add the total service time, which is always present.
1002        let service_time = self.timing.created_at.elapsed().as_millis();
1003        // Add a separator if other timings were already added.
1004        if !first {
1005            timing_str.push_str(", ");
1006        }
1007        // Write the final timing directly.
1008        let _ = write!(&mut timing_str, "total;dur={}", service_time);
1009
1010        timing_str
1011    }
1012
1013    /// Pushes a single cache key component to the context.
1014    #[inline]
1015    pub fn push_cache_key(&mut self, key: String) {
1016        let cache_info = self.cache.get_or_insert_default();
1017        cache_info
1018            .keys
1019            .get_or_insert_with(|| Vec::with_capacity(2))
1020            .push(key);
1021    }
1022
1023    /// Extends the cache key components with a vector of keys.
1024    #[inline]
1025    pub fn extend_cache_keys(&mut self, keys: Vec<String>) {
1026        let cache_info = self.cache.get_or_insert_default();
1027        cache_info
1028            .keys
1029            .get_or_insert_with(|| Vec::with_capacity(keys.len() + 2))
1030            .extend(keys);
1031    }
1032    /// Updates the upstream timing from the digest.
1033    #[inline]
1034    pub fn update_upstream_timing_from_digest(
1035        &mut self,
1036        digest: &Digest,
1037        reused: bool,
1038    ) {
1039        let detail = get_digest_detail(digest);
1040        self.timing.upstream_connection_duration = Some(detail.connection_time);
1041        if reused {
1042            return;
1043        }
1044
1045        // pingora times each layer on a monotonic clock: the TCP connect on
1046        // the transport entry, the handshake on the TLS entry. Prefer those.
1047        if let Some(tcp_connect) = detail.tcp_connect {
1048            self.timing.upstream_tcp_connect = Some(tcp_connect as i32);
1049            self.timing.upstream_tls_handshake =
1050                detail.tls_handshake.map(|value| value as i32);
1051            self.timing.upstream_connect_offload_wait =
1052                detail.connect_offload_wait.map(|value| value as i32);
1053            return;
1054        }
1055
1056        // Fallback for a stream that carries no measurement: split pingap's
1057        // own end-to-end connect timer by the wall-clock gap between the
1058        // layers' timestamps. Coarser, and the TCP share also absorbs
1059        // whatever the connector did around the connect.
1060        let upstream_connect_time =
1061            self.timing.upstream_connect.unwrap_or_default();
1062        let mut upstream_tcp_connect = upstream_connect_time;
1063        if detail.tls_established > detail.tcp_established {
1064            let latency =
1065                (detail.tls_established - detail.tcp_established) as i32;
1066            upstream_tcp_connect -= latency;
1067            self.timing.upstream_tls_handshake = Some(latency);
1068        }
1069        if upstream_tcp_connect > 0 {
1070            self.timing.upstream_tcp_connect = Some(upstream_tcp_connect);
1071        }
1072    }
1073}
1074
1075/// Generates a cache key from the request method, URI and state context.
1076/// The key includes an optional namespace and other key components if configured in the context.
1077///
1078/// # Arguments
1079/// * `ctx` - The Ctx context containing cache configuration.
1080/// * `method` - The HTTP method as a string.
1081/// * `uri` - The request URI.
1082///
1083/// Returns: A CacheKey whose primary is the namespace, custom keys (if any),
1084/// method and URI concatenated, and whose `user_tag` is the namespace.
1085pub fn get_cache_key(ctx: &Ctx, method: &str, uri: &Uri) -> CacheKey {
1086    let Some(cache_info) = &ctx.cache else {
1087        // Return an empty key if cache is not configured for this context.
1088        return CacheKey::new("", "");
1089    };
1090    let namespace = cache_info.namespace.as_ref().map_or("", |v| v);
1091    // Size the buffer from the URI's parts and write the URI straight into
1092    // it, rather than rendering it to a String first and copying that.
1093    // Keep full-URI semantics so existing cache keys stay stable across upgrades.
1094    let uri_len = uri.scheme_str().map_or(0, |scheme| scheme.len() + 3)
1095        + uri
1096            .authority()
1097            .map_or(0, |authority| authority.as_str().len())
1098        + uri.path().len()
1099        + uri.query().map_or(0, |query| query.len() + 1);
1100    // pingora's CacheKey used to take the namespace as its own argument and
1101    // hashed `namespace ++ primary` as one unframed byte string. That argument
1102    // is gone, so the namespace is written straight in front of the primary
1103    // here: the hash comes out byte-identical and an on-disk cache filled by
1104    // an older pingap stays warm across the upgrade. The storage layer still
1105    // partitions by namespace, so it also travels in `user_tag`, which is
1106    // carried alongside the key but never hashed.
1107    let keys_len = cache_info
1108        .keys
1109        .as_ref()
1110        .map_or(0, |keys| keys.iter().map(|s| s.len() + 1).sum::<usize>());
1111    let mut key_buf = String::with_capacity(
1112        namespace.len() + keys_len + method.len() + 1 + uri_len,
1113    );
1114    key_buf.push_str(namespace);
1115    // Custom key components first, each followed by ':'.
1116    if let Some(keys) = &cache_info.keys {
1117        for k in keys {
1118            key_buf.push_str(k);
1119            key_buf.push(':');
1120        }
1121    }
1122    // Then "METHOD:URI".
1123    key_buf.push_str(method);
1124    key_buf.push(':');
1125    let _ = write!(&mut key_buf, "{uri}");
1126
1127    CacheKey::new(key_buf, namespace)
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132    use super::*;
1133    use bytes::Bytes;
1134    use bytes::BytesMut;
1135    use pingora::cache::key::CacheHashKey;
1136    use pingora::protocols::tls::SslDigest;
1137    use pingora::protocols::tls::SslDigestExtension;
1138    use pretty_assertions::assert_eq;
1139    use std::{sync::Arc, time::Duration};
1140
1141    #[test]
1142    fn test_ctx_new() {
1143        let ctx = Ctx::new();
1144        // Check that created_at is a recent timestamp.
1145        // It should be within the last 100ms.
1146        let elapsed_ms = ctx.timing.created_at.elapsed().as_millis();
1147        assert!(elapsed_ms < 100, "created_at should be a recent timestamp");
1148        // Check that other fields are correctly defaulted.
1149        assert!(ctx.cache.is_none());
1150        assert!(ctx.features.is_none());
1151        assert_eq!(ctx.conn.id, 0);
1152    }
1153
1154    /// Tests both adding and getting variables.
1155    #[test]
1156    fn test_add_and_get_variable() {
1157        let mut ctx = Ctx::new();
1158        assert!(
1159            ctx.get_variable("key1").is_none(),
1160            "Should be None before adding"
1161        );
1162
1163        ctx.add_variable("key1", "value1");
1164        ctx.add_variable("key2", "value2");
1165
1166        assert_eq!(ctx.get_variable("key1"), Some("value1"));
1167        assert_eq!(ctx.get_variable("key2"), Some("value2"));
1168        assert_eq!(ctx.get_variable("nonexistent"), None);
1169    }
1170
1171    /// Tests the helper functions for getting filtered time values.
1172    #[test]
1173    fn test_get_time_field() {
1174        let mut ctx = Ctx::new();
1175
1176        // Test with a valid time
1177        ctx.timing.upstream_response = Some(100);
1178        assert_eq!(ctx.get_upstream_response_time(), Some(100));
1179
1180        // Test with a time is negative
1181        ctx.timing.upstream_response = Some(-1);
1182        assert_eq!(
1183            ctx.get_upstream_response_time(),
1184            None,
1185            "Time exceeding one hour should be None"
1186        );
1187
1188        // Test with None
1189        ctx.timing.upstream_response = None;
1190        assert_eq!(ctx.get_upstream_response_time(), None);
1191    }
1192
1193    /// Every log field name round-trips through the enum, and the two
1194    /// entry points agree.
1195    #[test]
1196    fn test_ctx_log_field_names() {
1197        for (name, field) in [
1198            ("connection_id", CtxLogField::ConnectionId),
1199            (
1200                "upstream_tcp_connect_time_human",
1201                CtxLogField::UpstreamTcpConnectTimeHuman,
1202            ),
1203            (
1204                "upstream_connect_offload_wait_time",
1205                CtxLogField::UpstreamConnectOffloadWaitTime,
1206            ),
1207            ("tls_version", CtxLogField::TlsVersion),
1208            ("ja4", CtxLogField::Ja4),
1209            ("ja4_r", CtxLogField::Ja4R),
1210            ("ja4_o", CtxLogField::Ja4O),
1211            ("ja4_ro", CtxLogField::Ja4Ro),
1212            ("compression_ratio", CtxLogField::CompressionRatio),
1213            ("service_time_human", CtxLogField::ServiceTimeHuman),
1214        ] {
1215            assert_eq!(Ok(field), name.parse::<CtxLogField>(), "{name}");
1216            assert_eq!(name, field.to_string());
1217        }
1218        assert_eq!(true, "unknown_key".parse::<CtxLogField>().is_err());
1219
1220        let mut ctx = Ctx::new();
1221        ctx.conn.id = 7;
1222        let mut by_name = BytesMut::new();
1223        ctx.append_log_value(&mut by_name, "connection_id");
1224        let mut by_field = BytesMut::new();
1225        ctx.append_log_field(&mut by_field, CtxLogField::ConnectionId);
1226        assert_eq!(b"7", by_name.as_ref());
1227        assert_eq!(by_name, by_field);
1228    }
1229
1230    /// The JA4 fields print the connection's fingerprint in each form,
1231    /// and nothing when there is none.
1232    #[test]
1233    fn test_ja4_log_fields() {
1234        let mut ctx = Ctx::new();
1235        let value = |ctx: &Ctx, key: &str| {
1236            let mut buf = BytesMut::new();
1237            ctx.append_log_value(&mut buf, key);
1238            String::from_utf8(buf.to_vec()).unwrap()
1239        };
1240        assert_eq!("", value(&ctx, "ja4"));
1241        let fingerprint = Ja4Fingerprint::from_client_hello(
1242            &crate::ja4::testing::spec_example_body(),
1243        )
1244        .unwrap();
1245        ctx.conn.ja4 = Some(Arc::new(fingerprint.clone()));
1246        assert_eq!("t13d1516h2_8daaf6152771_e5627efa2ab1", value(&ctx, "ja4"));
1247        assert_eq!(fingerprint.ja4_r(), value(&ctx, "ja4_r"));
1248        assert_eq!(fingerprint.ja4_o(), value(&ctx, "ja4_o"));
1249        assert_eq!(fingerprint.ja4_ro(), value(&ctx, "ja4_ro"));
1250    }
1251
1252    /// Tests the `append_log_value` function with a wider range of keys and edge cases.
1253    #[test]
1254    fn test_append_log_value_coverage() {
1255        let mut ctx = Ctx::new();
1256        // Test an unknown key, should do nothing.
1257        let mut buf = BytesMut::new();
1258        ctx.append_log_value(&mut buf, "unknown_key");
1259        assert!(buf.is_empty(), "Unknown key should not append anything");
1260
1261        // Test boolean values
1262        buf = BytesMut::new();
1263        ctx.conn.reused = true;
1264        ctx.append_log_value(&mut buf, "connection_reused");
1265        assert_eq!(&buf[..], b"true");
1266
1267        // Test optional string values
1268        ctx.conn.tls_version = Some("TLSv1.3".into());
1269        buf = BytesMut::new();
1270        ctx.append_log_value(&mut buf, "tls_version");
1271        assert_eq!(&buf[..], b"TLSv1.3");
1272
1273        // Test service_time calculation
1274        std::thread::sleep(Duration::from_millis(11));
1275        buf = BytesMut::new();
1276        ctx.append_log_value(&mut buf, "service_time");
1277        let service_time: u64 =
1278            std::str::from_utf8(&buf[..]).unwrap().parse().unwrap();
1279        assert!(service_time >= 10, "Service time should be at least 10ms");
1280    }
1281
1282    /// Tests the `get_cache_key` function's logic more thoroughly.
1283    #[test]
1284    fn test_get_cache_key() {
1285        let method = "GET";
1286        let uri = Uri::from_static("https://example.com/path");
1287
1288        // Case 1: No cache info in context.
1289        let ctx_no_cache = Ctx::new();
1290        let key1 = get_cache_key(&ctx_no_cache, method, &uri);
1291        assert_eq!(key1.user_tag, "");
1292        assert_eq!(key1.primary_key_str(), Some(""));
1293
1294        // Case 2: Cache info with namespace but no keys.
1295        let mut ctx_with_ns = Ctx::new();
1296        ctx_with_ns.cache = Some(Box::new(CacheInfo {
1297            namespace: Some("my-ns".to_string()),
1298            ..Default::default()
1299        }));
1300        let key2 = get_cache_key(&ctx_with_ns, method, &uri);
1301        assert_eq!(key2.user_tag, "my-ns");
1302        assert_eq!(
1303            key2.primary_key_str(),
1304            Some("my-nsGET:https://example.com/path")
1305        );
1306        // The hex pingora 0.8.1 produced for namespace "my-ns" and primary
1307        // "GET:https://example.com/path". If this changes, every entry an
1308        // older pingap wrote to disk becomes unreachable after an upgrade.
1309        assert_eq!(key2.primary(), "3f45c68799da5997559d474ba4b5775c");
1310
1311        // Case 3: Cache info with namespace and multiple keys.
1312        let mut ctx_with_keys = Ctx::new();
1313        ctx_with_keys.cache = Some(Box::new(CacheInfo {
1314            namespace: Some("my-ns".to_string()),
1315            keys: Some(vec!["user-123".to_string(), "desktop".to_string()]),
1316            ..Default::default()
1317        }));
1318        let key3 = get_cache_key(&ctx_with_keys, method, &uri);
1319        assert_eq!(key3.user_tag, "my-ns");
1320        assert_eq!(
1321            key3.primary_key_str(),
1322            Some("my-nsuser-123:desktop:GET:https://example.com/path")
1323        );
1324    }
1325
1326    /// The original `test_generate_server_timing` is good, but this version
1327    /// is slightly more robust to minor timing variations.
1328    #[test]
1329    fn test_generate_server_timing() {
1330        let mut ctx = Ctx::new();
1331        ctx.timing.upstream_connect = Some(1);
1332        ctx.timing.upstream_processing = Some(2);
1333        ctx.timing.cache_lookup = Some(6);
1334        ctx.timing.cache_lock = Some(7);
1335        ctx.add_plugin_processing_time(&Arc::from("plugin1"), 100);
1336
1337        let timing_header = ctx.generate_server_timing();
1338
1339        // Check for the presence of each expected component.
1340        assert!(timing_header.contains("upstream.connect;dur=1"));
1341        assert!(timing_header.contains("upstream.processing;dur=2"));
1342        assert!(timing_header.contains("upstream;dur=3"));
1343        assert!(timing_header.contains("cache.lookup;dur=6"));
1344        assert!(timing_header.contains("cache.lock;dur=7"));
1345        assert!(timing_header.contains("cache;dur=13"));
1346        assert!(timing_header.contains("plugin.plugin1;dur=100"));
1347        assert!(timing_header.contains("plugin;dur=100"));
1348        assert!(timing_header.contains("total;dur="));
1349    }
1350
1351    #[test]
1352    fn test_format_duration() {
1353        let mut buf = BytesMut::new();
1354        format_duration(&mut buf, (3600 + 3500) * 1000);
1355        assert_eq!(b"1.9h", buf.as_ref());
1356
1357        buf = BytesMut::new();
1358        format_duration(&mut buf, (3600 + 1800) * 1000);
1359        assert_eq!(b"1.5h", buf.as_ref());
1360
1361        buf = BytesMut::new();
1362        format_duration(&mut buf, (3600 + 100) * 1000);
1363        assert_eq!(b"1h", buf.as_ref());
1364
1365        buf = BytesMut::new();
1366        format_duration(&mut buf, (60 + 50) * 1000);
1367        assert_eq!(b"1.8m", buf.as_ref());
1368
1369        buf = BytesMut::new();
1370        format_duration(&mut buf, (60 + 2) * 1000);
1371        assert_eq!(b"1m", buf.as_ref());
1372
1373        buf = BytesMut::new();
1374        format_duration(&mut buf, 1000);
1375        assert_eq!(b"1s", buf.as_ref());
1376
1377        buf = BytesMut::new();
1378        format_duration(&mut buf, 512);
1379        assert_eq!(b"512ms", buf.as_ref());
1380
1381        buf = BytesMut::new();
1382        format_duration(&mut buf, 1112);
1383        assert_eq!(b"1.1s", buf.as_ref());
1384    }
1385
1386    #[test]
1387    fn test_add_variable() {
1388        let mut ctx = Ctx::new();
1389        ctx.add_variable("key1", "value1");
1390        ctx.add_variable("key2", "value2");
1391        ctx.extend_variables(AHashMap::from([
1392            ("key3".to_string(), "value3".to_string()),
1393            ("key4".to_string(), "value4".to_string()),
1394        ]));
1395        let variables =
1396            ctx.features.as_ref().unwrap().variables.as_ref().unwrap();
1397        // NOTE: The current implementation in the main code doesn't add the '$' prefix automatically.
1398        // The test should reflect the actual implementation.
1399        assert_eq!(variables.get("key1"), Some(&"value1".to_string()));
1400        assert_eq!(variables.get("key2"), Some(&"value2".to_string()));
1401        assert_eq!(variables.get("key3"), Some(&"value3".to_string()));
1402        assert_eq!(variables.get("key4"), Some(&"value4".to_string()));
1403    }
1404
1405    #[test]
1406    fn test_cache_key() {
1407        let mut ctx = Ctx::new();
1408        ctx.push_cache_key("key1".to_string());
1409        ctx.extend_cache_keys(vec!["key2".to_string(), "key3".to_string()]);
1410        assert_eq!(
1411            vec!["key1".to_string(), "key2".to_string(), "key3".to_string()],
1412            ctx.cache.unwrap().keys.unwrap()
1413        );
1414
1415        let mut ctx = Ctx::new();
1416        ctx.cache.get_or_insert_default();
1417        let key = get_cache_key(
1418            &ctx,
1419            "GET",
1420            &Uri::from_static("https://example.com/path"),
1421        );
1422        assert_eq!(key.user_tag, "");
1423        assert_eq!(key.primary_key_str(), Some("GET:https://example.com/path"));
1424    }
1425
1426    #[test]
1427    fn test_state() {
1428        let mut ctx = Ctx::new();
1429
1430        let mut buf = BytesMut::new();
1431        ctx.conn.id = 10;
1432        ctx.append_log_value(&mut buf, "connection_id");
1433        assert_eq!(b"10", buf.as_ref());
1434
1435        buf = BytesMut::new();
1436        ctx.append_log_value(&mut buf, "upstream_reused");
1437        assert_eq!(b"false", buf.as_ref());
1438
1439        buf = BytesMut::new();
1440        ctx.upstream.reused = true;
1441        ctx.append_log_value(&mut buf, "upstream_reused");
1442        assert_eq!(b"true", buf.as_ref());
1443
1444        buf = BytesMut::new();
1445        ctx.upstream.address = "192.168.1.1:80".to_string();
1446        ctx.append_log_value(&mut buf, "upstream_addr");
1447        assert_eq!(b"192.168.1.1:80", buf.as_ref());
1448
1449        buf = BytesMut::new();
1450        ctx.upstream.status = Some(StatusCode::CREATED);
1451        ctx.append_log_value(&mut buf, "upstream_status");
1452        assert_eq!(b"201", buf.as_ref());
1453
1454        buf = BytesMut::new();
1455        ctx.state.processing_count = 10;
1456        ctx.append_log_value(&mut buf, "processing");
1457        assert_eq!(b"10", buf.as_ref());
1458
1459        buf = BytesMut::new();
1460        ctx.timing.upstream_connect = Some(1);
1461        ctx.append_log_value(&mut buf, "upstream_connect_time");
1462        assert_eq!(b"1", buf.as_ref());
1463
1464        buf = BytesMut::new();
1465        ctx.append_log_value(&mut buf, "upstream_connect_time_human");
1466        assert_eq!(b"1ms", buf.as_ref());
1467
1468        buf = BytesMut::new();
1469        ctx.upstream.connected_count = Some(30);
1470        ctx.append_log_value(&mut buf, "upstream_connected");
1471        assert_eq!(b"30", buf.as_ref());
1472
1473        buf = BytesMut::new();
1474        ctx.timing.upstream_processing = Some(2);
1475        ctx.append_log_value(&mut buf, "upstream_processing_time");
1476        assert_eq!(b"2", buf.as_ref());
1477
1478        buf = BytesMut::new();
1479        ctx.append_log_value(&mut buf, "upstream_processing_time_human");
1480        assert_eq!(b"2ms", buf.as_ref());
1481
1482        buf = BytesMut::new();
1483        ctx.timing.upstream_response = Some(3);
1484        ctx.append_log_value(&mut buf, "upstream_response_time");
1485        assert_eq!(b"3", buf.as_ref());
1486
1487        buf = BytesMut::new();
1488        ctx.append_log_value(&mut buf, "upstream_response_time_human");
1489        assert_eq!(b"3ms", buf.as_ref());
1490
1491        buf = BytesMut::new();
1492        ctx.timing.upstream_tcp_connect = Some(100);
1493        ctx.append_log_value(&mut buf, "upstream_tcp_connect_time");
1494        assert_eq!(b"100", buf.as_ref());
1495
1496        buf = BytesMut::new();
1497        ctx.append_log_value(&mut buf, "upstream_tcp_connect_time_human");
1498        assert_eq!(b"100ms", buf.as_ref());
1499
1500        buf = BytesMut::new();
1501        ctx.timing.upstream_tls_handshake = Some(110);
1502        ctx.append_log_value(&mut buf, "upstream_tls_handshake_time");
1503        assert_eq!(b"110", buf.as_ref());
1504
1505        buf = BytesMut::new();
1506        ctx.timing.upstream_connect_offload_wait = Some(3);
1507        ctx.append_log_value(&mut buf, "upstream_connect_offload_wait_time");
1508        assert_eq!(b"3", buf.as_ref());
1509        buf = BytesMut::new();
1510        ctx.append_log_value(
1511            &mut buf,
1512            "upstream_connect_offload_wait_time_human",
1513        );
1514        assert_eq!(b"3ms", buf.as_ref());
1515
1516        buf = BytesMut::new();
1517        ctx.append_log_value(&mut buf, "upstream_tls_handshake_time_human");
1518        assert_eq!(b"110ms", buf.as_ref());
1519
1520        buf = BytesMut::new();
1521        ctx.timing.upstream_connection_duration = Some(120);
1522        ctx.append_log_value(&mut buf, "upstream_connection_time");
1523        assert_eq!(b"120", buf.as_ref());
1524
1525        buf = BytesMut::new();
1526        ctx.append_log_value(&mut buf, "upstream_connection_time_human");
1527        assert_eq!(b"120ms", buf.as_ref());
1528
1529        buf = BytesMut::new();
1530        ctx.upstream.location = "pingap".to_string().into();
1531        ctx.append_log_value(&mut buf, "location");
1532        assert_eq!(b"pingap", buf.as_ref());
1533
1534        buf = BytesMut::new();
1535        ctx.timing.connection_duration = 4;
1536        ctx.append_log_value(&mut buf, "connection_time");
1537        assert_eq!(b"4", buf.as_ref());
1538
1539        buf = BytesMut::new();
1540        ctx.append_log_value(&mut buf, "connection_time_human");
1541        assert_eq!(b"4ms", buf.as_ref());
1542
1543        buf = BytesMut::new();
1544        ctx.conn.reused = false;
1545        ctx.append_log_value(&mut buf, "connection_reused");
1546        assert_eq!(b"false", buf.as_ref());
1547
1548        buf = BytesMut::new();
1549        ctx.conn.reused = true;
1550        ctx.append_log_value(&mut buf, "connection_reused");
1551        assert_eq!(b"true", buf.as_ref());
1552
1553        buf = BytesMut::new();
1554        ctx.conn.tls_version = Some("TLSv1.3".into());
1555        ctx.append_log_value(&mut buf, "tls_version");
1556        assert_eq!(b"TLSv1.3", buf.as_ref());
1557
1558        buf = BytesMut::new();
1559        ctx.conn.tls_cipher =
1560            Some("ECDHE_ECDSA_WITH_AES_128_GCM_SHA256".into());
1561        ctx.append_log_value(&mut buf, "tls_cipher");
1562        assert_eq!(b"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", buf.as_ref());
1563
1564        buf = BytesMut::new();
1565        ctx.timing.tls_handshake = Some(101);
1566        ctx.append_log_value(&mut buf, "tls_handshake_time");
1567        assert_eq!(b"101", buf.as_ref());
1568
1569        buf = BytesMut::new();
1570        ctx.append_log_value(&mut buf, "tls_handshake_time_human");
1571        assert_eq!(b"101ms", buf.as_ref());
1572
1573        {
1574            let features = ctx.features.get_or_insert_default();
1575            features.compression_stat = Some(CompressionStat {
1576                in_bytes: 1024,
1577                out_bytes: 500,
1578                duration: Duration::from_millis(5),
1579                ..Default::default()
1580            })
1581        }
1582
1583        buf = BytesMut::new();
1584        ctx.append_log_value(&mut buf, "compression_time");
1585        assert_eq!(b"5", buf.as_ref());
1586
1587        buf = BytesMut::new();
1588        ctx.append_log_value(&mut buf, "compression_time_human");
1589        assert_eq!(b"5ms", buf.as_ref());
1590
1591        buf = BytesMut::new();
1592        ctx.append_log_value(&mut buf, "compression_ratio");
1593        assert_eq!(b"2.0", buf.as_ref());
1594
1595        buf = BytesMut::new();
1596        ctx.timing.cache_lookup = Some(6);
1597        ctx.append_log_value(&mut buf, "cache_lookup_time");
1598        assert_eq!(b"6", buf.as_ref());
1599
1600        buf = BytesMut::new();
1601        ctx.append_log_value(&mut buf, "cache_lookup_time_human");
1602        assert_eq!(b"6ms", buf.as_ref());
1603
1604        buf = BytesMut::new();
1605        ctx.timing.cache_lock = Some(7);
1606        ctx.append_log_value(&mut buf, "cache_lock_time");
1607        assert_eq!(b"7", buf.as_ref());
1608
1609        buf = BytesMut::new();
1610        ctx.append_log_value(&mut buf, "cache_lock_time_human");
1611        assert_eq!(b"7ms", buf.as_ref());
1612    }
1613
1614    #[test]
1615    fn test_add_plugin_processing_time() {
1616        let mut ctx = Ctx::new();
1617        ctx.add_plugin_processing_time(&Arc::from("plugin1"), 100);
1618        ctx.add_plugin_processing_time(&Arc::from("plugin2"), 200);
1619        assert_eq!(
1620            ctx.features.unwrap().plugin_processing_times,
1621            Some(vec![
1622                (Arc::from("plugin1"), 100),
1623                (Arc::from("plugin2"), 200)
1624            ])
1625        );
1626    }
1627
1628    #[test]
1629    fn test_get_digest_detail() {
1630        let mut digest = Digest::default();
1631        let detail = get_digest_detail(&digest);
1632        assert_eq!(detail.connection_reused, false);
1633        assert_eq!(detail.connection_time, 0);
1634        assert_eq!(detail.tcp_established, 0);
1635        assert_eq!(detail.tls_established, 0);
1636        assert_eq!(detail.tls_version, None);
1637        assert_eq!(detail.tls_cipher, None);
1638
1639        digest.timing_digest.push(Some(TimingDigest {
1640            established_ts: SystemTime::UNIX_EPOCH
1641                .checked_add(Duration::from_secs(5))
1642                .unwrap(),
1643            ..Default::default()
1644        }));
1645        digest.timing_digest.push(Some(TimingDigest {
1646            established_ts: SystemTime::UNIX_EPOCH
1647                .checked_add(Duration::from_secs(3))
1648                .unwrap(),
1649            ..Default::default()
1650        }));
1651        digest.ssl_digest = Some(Arc::new(SslDigest {
1652            version: "1.3".into(),
1653            cipher: "123".into(),
1654            organization: Some("cloudflare".to_string()),
1655            serial_number: Some(
1656                "0x00000000000000000000000000000abc".to_string(),
1657            ),
1658            cert_digest: vec![],
1659            extension: SslDigestExtension::default(),
1660        }));
1661        let detail = get_digest_detail(&digest);
1662        assert_eq!(detail.connection_reused, true);
1663        assert_eq!(detail.tcp_established, 5000);
1664        assert_eq!(detail.tls_established, 3000);
1665        assert_eq!(detail.tls_version.as_deref(), Some("1.3"));
1666        assert_eq!(detail.tls_cipher.as_deref(), Some("123"));
1667        // Nothing measured on these entries.
1668        assert_eq!(detail.tcp_connect, None);
1669        assert_eq!(detail.tls_handshake, None);
1670
1671        // pingora's per-layer measurements come through as such.
1672        digest.timing_digest = vec![
1673            Some(TimingDigest {
1674                establishment_duration: Some(Duration::from_millis(12)),
1675                ..Default::default()
1676            }),
1677            Some(TimingDigest {
1678                establishment_duration: Some(Duration::from_millis(34)),
1679                ..Default::default()
1680            }),
1681        ];
1682        let detail = get_digest_detail(&digest);
1683        assert_eq!(detail.tcp_connect, Some(12));
1684        assert_eq!(detail.tls_handshake, Some(34));
1685        // Not offloaded: no queueing time to report.
1686        assert_eq!(detail.connect_offload_wait, None);
1687
1688        // An offloaded connect also carries the time it waited for a thread.
1689        digest.timing_digest[0] = Some(TimingDigest {
1690            establishment_duration: Some(Duration::from_millis(12)),
1691            offload_wait_duration: Some(Duration::from_millis(2)),
1692            ..Default::default()
1693        });
1694        let detail = get_digest_detail(&digest);
1695        assert_eq!(detail.connect_offload_wait, Some(2));
1696
1697        // Without TLS the last entry is the transport again: no handshake.
1698        digest.ssl_digest = None;
1699        digest.timing_digest.truncate(1);
1700        let detail = get_digest_detail(&digest);
1701        assert_eq!(detail.tcp_connect, Some(12));
1702        assert_eq!(detail.tls_handshake, None);
1703    }
1704
1705    #[test]
1706    fn test_update_upstream_timing_from_digest() {
1707        let measured = |tcp: u64, tls: u64| Digest {
1708            timing_digest: vec![
1709                Some(TimingDigest {
1710                    establishment_duration: Some(Duration::from_millis(tcp)),
1711                    ..Default::default()
1712                }),
1713                Some(TimingDigest {
1714                    establishment_duration: Some(Duration::from_millis(tls)),
1715                    ..Default::default()
1716                }),
1717            ],
1718            ssl_digest: Some(Arc::new(SslDigest {
1719                version: "1.3".into(),
1720                cipher: "123".into(),
1721                organization: None,
1722                serial_number: None,
1723                cert_digest: vec![],
1724                extension: SslDigestExtension::default(),
1725            })),
1726            ..Default::default()
1727        };
1728
1729        // Measured layers are taken as they are, independent of pingap's
1730        // own end-to-end timer.
1731        let mut ctx = Ctx::new();
1732        ctx.timing.upstream_connect = Some(100);
1733        ctx.update_upstream_timing_from_digest(&measured(12, 34), false);
1734        assert_eq!(Some(12), ctx.timing.upstream_tcp_connect);
1735        assert_eq!(Some(34), ctx.timing.upstream_tls_handshake);
1736        assert_eq!(None, ctx.timing.upstream_connect_offload_wait);
1737
1738        // The offload queueing time rides along when the connect was
1739        // offloaded.
1740        let mut ctx = Ctx::new();
1741        let mut offloaded = measured(12, 34);
1742        offloaded.timing_digest[0] = Some(TimingDigest {
1743            establishment_duration: Some(Duration::from_millis(12)),
1744            offload_wait_duration: Some(Duration::from_millis(2)),
1745            ..Default::default()
1746        });
1747        ctx.update_upstream_timing_from_digest(&offloaded, false);
1748        assert_eq!(Some(2), ctx.timing.upstream_connect_offload_wait);
1749
1750        // A reused connection carries no connect cost for this request.
1751        let mut ctx = Ctx::new();
1752        ctx.timing.upstream_connect = Some(100);
1753        ctx.update_upstream_timing_from_digest(&measured(12, 34), true);
1754        assert_eq!(None, ctx.timing.upstream_tcp_connect);
1755        assert_eq!(None, ctx.timing.upstream_tls_handshake);
1756
1757        // No measurement: fall back to splitting the end-to-end timer by
1758        // the layers' wall-clock timestamps.
1759        let mut ctx = Ctx::new();
1760        ctx.timing.upstream_connect = Some(100);
1761        let mut unmeasured = measured(0, 0);
1762        unmeasured.timing_digest = vec![
1763            Some(TimingDigest {
1764                established_ts: SystemTime::UNIX_EPOCH
1765                    .checked_add(Duration::from_millis(1_000))
1766                    .unwrap(),
1767                ..Default::default()
1768            }),
1769            Some(TimingDigest {
1770                established_ts: SystemTime::UNIX_EPOCH
1771                    .checked_add(Duration::from_millis(1_030))
1772                    .unwrap(),
1773                ..Default::default()
1774            }),
1775        ];
1776        ctx.update_upstream_timing_from_digest(&unmeasured, false);
1777        assert_eq!(Some(70), ctx.timing.upstream_tcp_connect);
1778        assert_eq!(Some(30), ctx.timing.upstream_tls_handshake);
1779    }
1780
1781    #[test]
1782    fn test_modify_body_handler() {
1783        let mut ctx = Ctx::default();
1784
1785        struct TestHandler {}
1786        impl ModifyResponseBody for TestHandler {
1787            fn handle(
1788                &mut self,
1789                _session: &Session,
1790                body: &mut Option<bytes::Bytes>,
1791                _end_of_stream: bool,
1792            ) -> pingora::Result<()> {
1793                *body = Some(Bytes::from("test"));
1794                Ok(())
1795            }
1796        }
1797
1798        ctx.add_modify_body_handler("test", Box::new(TestHandler {}));
1799        assert_eq!(true, ctx.get_modify_body_handler("test").is_some());
1800    }
1801}