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