Skip to main content

opentracingrust/tracers/
file.rs

1use std::io;
2use std::io::Write;
3use std::time::UNIX_EPOCH;
4
5use crossbeam_channel::unbounded;
6use rand::random;
7
8use super::super::ImplContextBox;
9use super::super::Result;
10
11use super::super::FinishedSpan;
12use super::super::LogValue;
13use super::super::Span;
14use super::super::SpanContext;
15use super::super::SpanReceiver;
16use super::super::SpanReference;
17use super::super::SpanReferenceAware;
18use super::super::SpanSender;
19use super::super::StartOptions;
20use super::super::TagValue;
21
22use super::super::ExtractFormat;
23use super::super::InjectFormat;
24use super::super::Tracer;
25use super::super::TracerInterface;
26
27
28const BAGGAGE_KEY_PREFIX: &str = "Baggage-";
29const SPAN_ID_KEY: &str = "SpanID";
30const TRACE_ID_KEY: &str = "TraceID";
31
32
33/// A tracer that writes spans to an `std::io::Write`.
34///
35/// Useful for local testing, experiments, tests.
36/// **NOT suited for production use!**
37///
38/// Intended to write spans to stderr but can also be used to write spans to stdout or files.
39///
40/// # Examples
41///
42/// ```
43/// extern crate opentracingrust;
44///
45/// use std::io;
46/// use std::time::Duration;
47///
48/// use opentracingrust::FinishedSpan;
49/// use opentracingrust::tracers::FileTracer;
50/// use opentracingrust::utils::GlobalTracer;
51/// use opentracingrust::utils::ReporterThread;
52///
53///
54/// fn main() {
55///     let (tracer, receiver) = FileTracer::new();
56///     GlobalTracer::init(tracer);
57///
58///     let reporter = ReporterThread::new_with_duration(
59///         receiver, Duration::from_millis(50), |span| {
60///             let mut stderr = io::stderr();
61///             FileTracer::write_trace(span, &mut stderr).unwrap();
62///         }
63///     );
64///
65///     // ... snip ...
66/// }
67/// ```
68pub struct FileTracer {
69    sender: SpanSender
70}
71
72impl TracerInterface for FileTracer {
73    /// Extract a span context from a text map or HTTP headers.
74    ///
75    /// Note that the binary extraction format is not supported by `FileTracer`.
76    fn extract(&self, fmt: ExtractFormat) -> Result<Option<SpanContext>> {
77        match fmt {
78            ExtractFormat::HttpHeaders(carrier) => {
79                // Decode trace and span IDs.
80                let trace_id = carrier.get(TRACE_ID_KEY);
81                if trace_id.is_none() {
82                    return Ok(None);
83                }
84                let trace_id = trace_id.unwrap().parse::<u64>()?;
85
86                let span_id = carrier.get(SPAN_ID_KEY);
87                if span_id.is_none() {
88                    return Ok(None);
89                }
90                let span_id = span_id.unwrap().parse::<u64>()?;
91
92                // Create a mutable context to load baggage items.
93                let mut context = SpanContext::new(ImplContextBox::new(
94                    FileTracerContext {
95                        trace_id,
96                        span_id
97                    }
98                ));
99
100                // Decode baggage items.
101                for (key, value) in carrier.items() {
102                    if key.starts_with(BAGGAGE_KEY_PREFIX) {
103                        context.set_baggage_item(key.clone(), value.clone());
104                    }
105                }
106                Ok(Some(context))
107            },
108            _ => panic!("Unsupported extraction format")
109        }
110    }
111
112    /// Inject the span context into a text map or HTTP headers.
113    ///
114    /// Note that the binary injection format is not supported by `FileTracer`.
115    fn inject(&self, context: &SpanContext, fmt: InjectFormat) -> Result<()> {
116        let span_context = context;
117        let context = span_context.impl_context::<FileTracerContext>();
118        let context = context.expect(
119            "Unsupported span, was it created by FileTracer?"
120        );
121        match fmt {
122            InjectFormat::HttpHeaders(carrier) |
123            InjectFormat::TextMap(carrier) => {
124                carrier.set(TRACE_ID_KEY, &context.trace_id.to_string());
125                carrier.set(SPAN_ID_KEY, &context.span_id.to_string());
126                for (key, value) in span_context.baggage_items() {
127                    let key = format!("{}{}", BAGGAGE_KEY_PREFIX, key);
128                    carrier.set(&key, value);
129                }
130                Ok(())
131            },
132            _ => panic!("Unsupported injection format")
133        }
134    }
135
136    fn span(&self, name: &str, options: StartOptions) -> Span {
137        let trace_id = random::<u64>();
138        let span_id = random::<u64>();
139        let context = SpanContext::new(ImplContextBox::new(FileTracerContext {
140            trace_id,
141            span_id
142        }));
143        Span::new(name, context, options, self.sender.clone())
144    }
145}
146
147impl FileTracer {
148    /// Instantiate a new file tracer.
149    pub fn new() -> (Tracer, SpanReceiver) {
150        let (sender, receiver) = unbounded();
151        let tracer = FileTracer { sender };
152        (Tracer::new(tracer), receiver)
153    }
154
155    /// Function to write a `FinishedSpan` to a stream.
156    ///
157    /// Used to send `FinishedSpan`s to an `std::io::Write` stream.
158    pub fn write_trace<W: Write>(
159        span: FinishedSpan, file: &mut W
160    ) -> io::Result<()> {
161        let context = span.context().impl_context::<FileTracerContext>();
162        let context = context.expect(
163            "Unsupported span, was it created by FileTracer?"
164        );
165        let mut buffer = String::new();
166        buffer.push_str(&format!("==>> Trace ID: {}\n", context.trace_id));
167        buffer.push_str(&format!("===> Span ID: {}\n", context.span_id));
168
169        let finish = span.finish_time();
170        let start = span.start_time();
171        let duration = finish.duration_since(*start).unwrap();
172        let secs = duration.as_secs() as f64;
173        let delta = secs + duration.subsec_nanos() as f64 * 1e-9;
174        buffer.push_str(&format!("===> Span Duration: {}\n", delta));
175
176        buffer.push_str("===> References: [\n");
177        for reference in span.references() {
178            let ref_id = match reference {
179                &SpanReference::ChildOf(ref parent) |
180                &SpanReference::FollowsFrom(ref parent) => {
181                    let context = parent.impl_context::<FileTracerContext>();
182                    let context = context.expect(
183                        "Unsupported span context, was it created by FileTracer?"
184                    );
185                    context.span_id
186                }
187            };
188            let ref_type = match reference {
189                SpanReference::ChildOf(_) => "Child of span ID",
190                SpanReference::FollowsFrom(_) => "Follows from span ID"
191            };
192            buffer.push_str(&format!("===>   * {}: {}\n", ref_type, ref_id));
193        }
194        buffer.push_str("===> ]\n");
195
196        buffer.push_str("===> Baggage items: [\n");
197        for (key, value) in span.context().baggage_items() {
198            buffer.push_str(&format!("===>   * {}: {}\n", key, value));
199        }
200        buffer.push_str("===> ]\n");
201
202        let mut tags: Vec<(&String, &TagValue)> = span.tags().iter().collect();
203        tags.sort_by_key(|&(k, _)| k);
204        buffer.push_str("===> Tags: [\n");
205        for (tag, value) in tags {
206            let value = match value {
207                TagValue::Boolean(v) => v.to_string(),
208                TagValue::Float(v) => v.to_string(),
209                TagValue::Integer(v) => v.to_string(),
210                TagValue::String(ref v) => v.clone(),
211            };
212            buffer.push_str(&format!("===>   * {}: {}\n", tag, value));
213        }
214        buffer.push_str("===> ]\n");
215
216        buffer.push_str("===> Logs: [\n");
217        for log in span.logs().iter() {
218            let timestamp = log.timestamp().unwrap()
219                .duration_since(UNIX_EPOCH).unwrap()
220                .as_secs();
221            buffer.push_str(&format!("===>   - {}:\n", timestamp));
222
223            let mut fields: Vec<(&String, &LogValue)> = log.iter().collect();
224            fields.sort_by_key(|&(k, _)| k);
225            for (key, value) in fields {
226                let value = match value {
227                    LogValue::Boolean(v) => v.to_string(),
228                    LogValue::Float(v) => v.to_string(),
229                    LogValue::Integer(v) => v.to_string(),
230                    LogValue::String(ref v) => v.clone(),
231                };
232                buffer.push_str(&format!("===>     * {}: {}\n", key, value));
233            }
234        }
235        buffer.push_str("===> ]\n");
236        file.write_all(buffer.as_bytes())
237    }
238}
239
240
241/// Inner `SpanContext` for `FileTracer`.
242#[derive(Clone, Debug)]
243struct FileTracerContext {
244    trace_id: u64,
245    span_id: u64
246}
247
248impl SpanReferenceAware for FileTracerContext {
249    fn reference_span(&mut self, reference: &SpanReference) {
250        match reference {
251            &SpanReference::ChildOf(ref parent) |
252            &SpanReference::FollowsFrom(ref parent) => {
253                let context = parent.impl_context::<FileTracerContext>();
254                let context = context.expect(
255                    "Unsupported span context, was it created by FileTracer?"
256                );
257                self.trace_id = context.trace_id;
258            }
259        }
260    }
261}
262
263
264#[cfg(test)]
265mod tests {
266    use super::super::super::ImplContextBox;
267    use super::super::super::SpanContext;
268    use super::super::super::SpanReceiver;
269    use super::super::super::Tracer;
270
271    use super::FileTracer;
272    use super::FileTracerContext;
273
274    fn make_context(trace_id: u64, span_id: u64) -> SpanContext {
275        SpanContext::new(ImplContextBox::new(FileTracerContext {
276            trace_id,
277            span_id
278        }))
279    }
280
281    fn make_tracer() -> (Tracer, SpanReceiver) {
282        FileTracer::new()
283    }
284
285
286    mod span {
287        use std::time::UNIX_EPOCH;
288        use std::time::Duration;
289
290        use super::super::super::super::Log;
291
292        use super::super::FileTracer;
293        use super::super::FileTracerContext;
294        use super::make_context;
295        use super::make_tracer;
296
297
298        mod extract {
299            use std::collections::HashMap;
300            use std::io;
301
302            use super::super::super::super::super::Error;
303            use super::super::super::super::super::ExtractFormat;
304
305            use super::FileTracerContext;
306            use super::make_tracer;
307
308
309            mod invalid {
310                use std::collections::HashMap;
311
312                use super::Error;
313                use super::ExtractFormat;
314                use super::make_tracer;
315
316                #[test]
317                fn fails_if_invalid_span_id() {
318                    let (tracer, _) = make_tracer();
319                    let mut map: HashMap<String, String> = HashMap::new();
320                    map.insert(String::from("TraceID"), String::from("123"));
321                    map.insert(String::from("SpanID"), String::from("abc"));
322                    let context = tracer.extract(
323                        ExtractFormat::HttpHeaders(Box::new(&map))
324                    );
325                    match context {
326                        Err(Error::ParseIntError(_)) => {},
327                        Err(err) => panic!("Unexpected error: {:?}", err),
328                        Ok(success) => panic!("Unexpected ok: {:?}", success)
329                    }
330                }
331
332                #[test]
333                fn fails_if_invalid_trace_id() {
334                    let (tracer, _) = make_tracer();
335                    let mut map: HashMap<String, String> = HashMap::new();
336                    map.insert(String::from("TraceID"), String::from("abc"));
337                    let context = tracer.extract(
338                        ExtractFormat::HttpHeaders(Box::new(&map))
339                    );
340                    match context {
341                        Err(Error::ParseIntError(_)) => {},
342                        Err(err) => panic!("Unexpected error: {:?}", err),
343                        Ok(success) => panic!("Unexpected ok: {:?}", success)
344                    }
345                }
346
347                #[test]
348                fn returns_none_without_trace_id() {
349                    let (tracer, _) = make_tracer();
350                    let map: HashMap<String, String> = HashMap::new();
351                    let context = tracer.extract(
352                        ExtractFormat::HttpHeaders(Box::new(&map))
353                    );
354                    match context {
355                        Err(err) => panic!("Unexpected error: {:?}", err),
356                        Ok(Some(success)) => panic!(
357                            "Unexpected some: {:?}", success
358                        ),
359                        Ok(None) => {}
360                    }
361                }
362
363                #[test]
364                fn returns_none_without_span_id() {
365                    let (tracer, _) = make_tracer();
366                    let mut map: HashMap<String, String> = HashMap::new();
367                    map.insert(String::from("TraceID"), String::from("123"));
368                    let context = tracer.extract(
369                        ExtractFormat::HttpHeaders(Box::new(&map))
370                    );
371                    match context {
372                        Err(err) => panic!("Unexpected error: {:?}", err),
373                        Ok(Some(success)) => panic!(
374                            "Unexpected some: {:?}", success
375                        ),
376                        Ok(None) => {}
377                    }
378                }
379            }
380
381            #[test]
382            #[should_panic(expected = "Unsupported extraction format")]
383            fn binary_not_supported() {
384                let (tracer, _) = make_tracer();
385                let mut stdin = io::stdin();
386                tracer.extract(
387                    ExtractFormat::Binary(Box::new(&mut stdin))
388                ).unwrap();
389            }
390
391            #[test]
392            fn http_headers() {
393                let (tracer, _) = make_tracer();
394                let mut map: HashMap<String, String> = HashMap::new();
395                map.insert(String::from("TraceID"), String::from("1234"));
396                map.insert(String::from("SpanID"), String::from("5678"));
397                map.insert(String::from("Baggage-Item1"), String::from("ab"));
398                map.insert(String::from("Baggage-Item2"), String::from("cd"));
399
400                let context = tracer.extract(
401                    ExtractFormat::HttpHeaders(Box::new(&map))
402                ).unwrap().unwrap();
403                let inner = context.impl_context::<FileTracerContext>();
404                let inner = inner.unwrap();
405
406                assert_eq!(1234, inner.trace_id);
407                assert_eq!(5678, inner.span_id);
408                assert_eq!(
409                    "ab",
410                    context.get_baggage_item("Baggage-Item1").unwrap()
411                );
412                assert_eq!(
413                    "cd",
414                    context.get_baggage_item("Baggage-Item2").unwrap()
415                );
416            }
417        }
418
419
420        mod inject {
421            use std::collections::HashMap;
422            use std::io;
423
424            use super::super::super::super::super::InjectFormat;
425            use super::make_context;
426            use super::make_tracer;
427
428
429            #[test]
430            #[should_panic(expected = "Unsupported injection format")]
431            fn binary_not_supported() {
432                let (tracer, _) = make_tracer();
433                let context = make_context(1234, 1234);
434                let mut stdout = io::stdout();
435                tracer.inject(
436                    &context,
437                    InjectFormat::Binary(Box::new(&mut stdout))
438                ).unwrap();
439            }
440
441            #[test]
442            fn http_headers() {
443                let (tracer, _) = make_tracer();
444                let mut context = make_context(1234, 5678);
445                let mut map: HashMap<String, String> = HashMap::new();
446                context.set_baggage_item(String::from("Item1"), String::from("ab"));
447                context.set_baggage_item(String::from("Item2"), String::from("cd"));
448                tracer.inject(
449                    &context,
450                    InjectFormat::HttpHeaders(Box::new(&mut map))
451                ).unwrap();
452
453                assert_eq!("1234", map.get("TraceID").unwrap());
454                assert_eq!("5678", map.get("SpanID").unwrap());
455                assert_eq!("ab", map.get("Baggage-Item1").unwrap());
456                assert_eq!("cd", map.get("Baggage-Item2").unwrap());
457            }
458
459            #[test]
460            fn text_map() {
461                let (tracer, _) = make_tracer();
462                let mut context = make_context(1234, 5678);
463                let mut map: HashMap<String, String> = HashMap::new();
464                context.set_baggage_item(String::from("Item1"), String::from("ab"));
465                context.set_baggage_item(String::from("Item2"), String::from("cd"));
466                tracer.inject(
467                    &context,
468                    InjectFormat::TextMap(Box::new(&mut map))
469                ).unwrap();
470
471                assert_eq!("1234", map.get("TraceID").unwrap());
472                assert_eq!("5678", map.get("SpanID").unwrap());
473                assert_eq!("ab", map.get("Baggage-Item1").unwrap());
474                assert_eq!("cd", map.get("Baggage-Item2").unwrap());
475            }
476        }
477
478
479        #[test]
480        fn create() {
481            let (tracer, _) = make_tracer();
482            let span = tracer.span("test1");
483            let context = span.context().impl_context::<FileTracerContext>();
484            context.unwrap();
485        }
486
487        #[test]
488        fn write() {
489            let (tracer, receiver) = make_tracer();
490            let mut span = tracer.span("test1");
491            span.child_of(make_context(123456, 123));
492            span.follows(make_context(123456, 456));
493            span.set_baggage_item("TestKey", "Test Value");
494            span.tag("test.bool", true);
495            span.tag("test.float", 0.5);
496            span.tag("test.int", 5);
497            span.tag("test.string", "hello");
498
499            span.log(Log::new()
500                .log("bool", false)
501                .log("float", 0.66)
502                .at(UNIX_EPOCH + Duration::from_secs(123456))
503            );
504            span.log(Log::new()
505                .log("int", 66)
506                .log("string", "message")
507                .at(UNIX_EPOCH + Duration::from_secs(654321))
508            );
509            span.finish().unwrap();
510
511            let mut buffer = Vec::new();
512            let span = receiver.recv().unwrap();
513            FileTracer::write_trace::<Vec<u8>>(span, &mut buffer).unwrap();
514
515            let buffer = String::from_utf8(buffer).unwrap();
516            let mut buffer = buffer.split('\n');
517            assert_eq!(buffer.next().unwrap(), "==>> Trace ID: 123456");
518
519            let buffer: Vec<&str> = buffer.skip(2).collect();
520            assert_eq!(buffer, [
521                "===> References: [",
522                "===>   * Child of span ID: 123",
523                "===>   * Follows from span ID: 456",
524                "===> ]",
525                "===> Baggage items: [",
526                "===>   * TestKey: Test Value",
527                "===> ]",
528                "===> Tags: [",
529                "===>   * test.bool: true",
530                "===>   * test.float: 0.5",
531                "===>   * test.int: 5",
532                "===>   * test.string: hello",
533                "===> ]",
534                "===> Logs: [",
535                "===>   - 123456:",
536                "===>     * bool: false",
537                "===>     * float: 0.66",
538                "===>   - 654321:",
539                "===>     * int: 66",
540                "===>     * string: message",
541                "===> ]",
542                ""
543            ]);
544        }
545    }
546}