1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
use std::collections::HashMap;

pub trait TxTraceExt {
    fn into_trace_id(self) -> opentelemetry::trace::TraceId;
    fn into_trace_context(self) -> opentelemetry::Context;
    fn as_traceparent(&self) -> String;
}

impl TxTraceExt for crate::TxId {
    // in order to convert a TxId (a 48 bytes cuid) into a TraceId (16 bytes), we remove the first byte,
    // (always 'c') and get the next 16 bytes, which are random enough to be used as a trace id.
    // this is a typical cuid: "c-lct0q6ma-0004-rb04-h6en1roa"
    //
    // - first letter is always the same
    // - next 7-8 byte are random a timestamp. There's more entropy in the least significative bytes
    // - next 4 bytes are a counter since the server started
    // - next 4 bytes are a system fingerprint, invariant for the same server instance
    // - least significative 8 bytes. Totally random.
    //
    // We want the most entropic slice of 16 bytes that's deterministicly determined
    fn into_trace_id(self) -> opentelemetry::trace::TraceId {
        let mut buffer = [0; 16];
        let str = self.to_string();
        let tx_id_bytes = str.as_bytes();
        let len = tx_id_bytes.len();

        // bytes [len-20  to len-12): least significative 4 bytes of the timestamp + 4 bytes counter
        for (i, source_idx) in (len - 20..len - 12).enumerate() {
            buffer[i] = tx_id_bytes[source_idx];
        }
        // bytes [len-8 to len):  the random blocks
        for (i, source_idx) in (len - 8..len).enumerate() {
            buffer[i + 8] = tx_id_bytes[source_idx];
        }

        opentelemetry::trace::TraceId::from_bytes(buffer)
    }
    // This is a bit of a hack, but it's the only way to have a default trace span for a whole
    // transaction when no traceparent is propagated from the client.
    //
    // This is done  so we can capture traces happening accross the different queries in a
    // transaction. Otherwise, if a traceparent is not propagated from the client, each query in
    // the transaction will run within a span that has already been generated at the begining of the
    // transaction, and held active in the actor in charge of running the queries. Thus, making
    // impossible to capture traces happening in the individual queries, as they won't be aware of
    // the transaction they are part of.
    //
    // By generating this "fake" traceparent based on the transaction id, we can have a common
    // trace_id for all transaction operations.
    fn into_trace_context(self) -> opentelemetry::Context {
        let extractor: HashMap<String, String> =
            HashMap::from_iter(vec![("traceparent".to_string(), self.as_traceparent())]);
        opentelemetry::global::get_text_map_propagator(|propagator| propagator.extract(&extractor))
    }

    fn as_traceparent(&self) -> String {
        let trace_id = self.clone().into_trace_id();
        format!("00-{trace_id}-0000000000000001-01")
    }
}

// tests for txid into traits
#[cfg(test)]
mod test {
    use super::*;
    use crate::TxId;

    #[test]
    fn test_txid_into_traceid() {
        let fixture = vec![
            ("clct0q6ma0000rb04768tiqbj", "71366d6130303030373638746971626a"),
            // counter changed, trace id changed:
            ("clct0q6ma0002rb04cpa6zkmx", "71366d6130303032637061367a6b6d78"),
            // fingerprint changed, trace id did not change, as that chunk is ignored:
            ("clct0q6ma00020000cpa6zkmx", "71366d6130303032637061367a6b6d78"),
            // first 5 bytes changed, trace id did not change, as that chunk is ignored:
            ("00000q6ma00020000cpa6zkmx", "71366d6130303032637061367a6b6d78"),
            // 6 th byte changed, trace id changed, as that chunk is part of the lsb of the timestamp
            ("0000006ma00020000cpa6zkmx", "30366d6130303032637061367a6b6d78"),
        ];

        for (txid, expected_trace_id) in fixture {
            let txid: TxId = txid.into();
            let trace_id: opentelemetry::trace::TraceId = txid.into_trace_id();
            assert_eq!(trace_id.to_string(), expected_trace_id);
        }
    }
}