File Coverage

blib/lib/OpenTracing/Implementation/DataDog/Client.pm
Criterion Covered Total %
statement 109 111 98.2
branch 22 24 91.6
condition 6 8 75.0
subroutine 31 32 96.8
pod 2 3 66.6
total 170 178 95.5


line stmt bran cond sub pod time code
1             package OpenTracing::Implementation::DataDog::Client;
2              
3             =head1 NAME
4              
5             OpenTracing::Implementation::DataDog::Client - A Client that sends off the spans
6              
7             =head1 SYNOPSIS
8              
9             use alias OpenTracing::Implementation::DataDog::Client;
10            
11             my $datadog_client = ->new(
12             http_user_agent => LWP::UserAgent->new();
13             host => 'localhost',
14             port => '8126',
15             path => 'v0.3/traces',
16             ); # these are defaults
17              
18             and later:
19              
20             $datadog_client->send_span( $span );
21              
22             =cut
23              
24              
25              
26             =head1 DESCRIPTION
27              
28             The main responsabillity of this C<Client> is to provide the C<send_span>
29             method, that will send the data to the local running DataDog agent.
30              
31             It does this by calling L<to_struct> that massages the generic OpenTracing data,
32             like C<baggage_items> from L<SpanContext> and C<tags> from C<Span>, together
33             with the DataDog specific data like C<resource_name>.
34              
35             This structure will be send of as a JSON string to the local installed DataDog
36             agent.
37              
38             =cut
39              
40              
41              
42             our $VERSION = 'v0.47.0';
43              
44 14     14   1005866 use English;
  14         52073  
  14         85  
45              
46 14     14   9458 use Moo;
  14         44429  
  14         81  
47 14     14   21843 use Sub::HandlesVia;
  14         86454  
  14         167  
48             # XXX Order matters: Sub::HandlesVia::Manual::WithMoo - Potential load order
49 14     14   2443816 use MooX::Attribute::ENV;
  14         69090  
  14         132  
50 14     14   9215 use MooX::ProtectedAttributes;
  14         10282  
  14         105  
51 14     14   3957 use MooX::Should;
  14         22508  
  14         148  
52              
53 14     14   1586 use Carp;
  14         57  
  14         732  
54 14     14   6792 use HTTP::Request ();
  14         287927  
  14         443  
55 14     14   6773 use JSON::MaybeXS qw(JSON);
  14         77539  
  14         883  
56 14     14   10835 use LWP::UserAgent;
  14         413370  
  14         673  
57 14     14   3060 use PerlX::Maybe qw/maybe provided/;
  14         15176  
  14         129  
58 14     14   9209 use Regexp::Common qw/URI/;
  14         38988  
  14         82  
59 14     14   363875 use Types::Standard qw/ArrayRef Bool Enum HasMethods Maybe Str/;
  14         48  
  14         179  
60 14     14   42141 use Types::Common::Numeric qw/IntRange/;
  14         346226  
  14         148  
61              
62 14         1123 use OpenTracing::Implementation::DataDog::Utils qw(
63             nano_seconds
64 14     14   18459 );
  14         66  
65              
66 14     14   138 use constant MAX_SPANS => 20_000; # this is just an arbitrary, hardcoded number
  14         33  
  14         24354  
67              
68              
69              
70             =head1 OPTIONAL ATTRIBUTES
71              
72             The attributes below can be set during instantiation, but none are required and
73             have sensible defaults, that may actually play nice with known DataDog
74             environment variables
75              
76             =cut
77              
78              
79              
80             =head2 C<http_user_agent>
81              
82             A HTTP User Agent that connects to the locally running DataDog agent. This will
83             default to a L<LWP::UserAgent>, but any User Agent will suffice, as long as it
84             has a required delegate method C<request>, that takes a L<HTTP::Request> object
85             and returns a L<HTTP::Response> compliant response object.
86              
87             =cut
88              
89             has http_user_agent => (
90             is => 'lazy',
91             should => HasMethods[qw/request/],
92             handles => { _send_http_request => 'request' },
93             );
94              
95             sub _build_http_user_agent {
96 0     0   0 return LWP::UserAgent->new( )
97             }
98              
99              
100              
101             =head2 C<scheme>
102              
103             The scheme being used, should be either C<http> or C<https>,
104             defaults to C<http>
105              
106             =cut
107              
108             has scheme => (
109             is => 'ro',
110             should => Enum[qw/http https/],
111             default => 'http',
112             );
113              
114              
115              
116             =head2 C<host>
117              
118             The host-name where the DataDog agent is running, which defaults to
119             C<localhost> or the value of C<DD_AGENT_HOST> environment variable if set.
120              
121             =cut
122              
123             has host => (
124             is => 'ro',
125             env_key => 'DD_AGENT_HOST',
126             default => 'localhost',
127             );
128              
129              
130              
131             =head2 C<port>
132              
133             The port-number the DataDog agent is listening at, which defaults to C<8126> or
134             the value of the C<DD_TRACE_AGENT_PORT> environment variable if set.
135              
136             =cut
137              
138             has port => (
139             is => 'ro',
140             env_key => 'DD_TRACE_AGENT_PORT',
141             default => '8126',
142             );
143              
144              
145              
146             =head2 C<path>
147              
148             The path the DataDog agent is expecting requests to come in, which defaults to
149             C<v0.3/traces>.
150              
151             =cut
152              
153             has path => (
154             is => 'ro',
155             default => 'v0.3/traces',
156             );
157             #
158             # maybe a 'version number' would be a better option ?
159              
160              
161              
162             =head2 C<agent_url>
163              
164             The complete URL the DataDog agent is listening at, and defaults to the value of
165             the C<DD_TRACE_AGENT_URL> environment variable if set. If this is set, it takes
166             precedence over any of the other settings.
167              
168             =cut
169              
170             has agent_url => (
171             is => 'ro',
172             env_key => 'DD_TRACE_AGENT_URL',
173             should => Maybe[Str->where( sub { _is_uri($_) } )],
174             );
175              
176             =pod
177              
178             NOTE: DataDog Agents can also listen to a UNiX socket, and one is suggested that
179             there is a C<unix:> URL. Fist of all, that is false, the C<unix:> scheme is just
180             non existent. It should be C<file:> instead. Secondly, this L<Client> just does
181             not support it, only C<http:> or C<https:>
182              
183             =cut
184              
185              
186              
187             has uri => (
188             is => 'lazy',
189             init_arg => undef,
190             );
191              
192             sub _build_uri {
193 9     9   3822 my $self = shift;
194            
195             return
196 9   66     137 $self->agent_url
197             //
198             "$self->{ scheme }://$self->{ host }:$self->{ port }/$self->{ path }"
199             }
200             #
201             # URI::Template is a nicer solution for this and more dynamic
202              
203              
204              
205             protected_has _default_http_headers => (
206             is => 'lazy',
207             isa => ArrayRef,
208             init_arg => undef,
209             handles_via => 'Array',
210             handles => {
211             _default_http_headers_list => 'all',
212             },
213             );
214              
215             sub _build__default_http_headers {
216             return [
217 4     4   98 'Content-Type' => 'application/json; charset=UTF-8',
218             'Datadog-Meta-Lang' => 'perl',
219             'Datadog-Meta-Lang-Interpreter' => $EXECUTABLE_NAME,
220             'Datadog-Meta-Lang-Version' => $PERL_VERSION->stringify,
221             'Datadog-Meta-Tracer-Version' => $VERSION,
222             ]
223             }
224              
225              
226              
227             has _json_encoder => (
228             is => 'lazy',
229             init_arg => undef,
230             handles => { _json_encode => 'encode' },
231             );
232              
233             sub _build__json_encoder {
234 4     4   144 JSON()->new->utf8->canonical->pretty->allow_bignum
235             }
236             #
237             # I just love readable and consistant JSONs
238              
239              
240              
241             =head2 C<span_buffer_threshold>
242              
243             This sets the size limit of the span buffer. When this number is reached, this
244             C<Client> will send off the buffered spans using the internal C<user_agent>.
245              
246             This number can be set on instantiation, or will take it from the
247             C<DD_TRACE_PARTIAL_FLUSH_MIN_SPANS> environment variable. If nothing is set, it
248             defaults to 100.
249              
250             The number can not be set to anything higher than 20_000.
251              
252             If this number is C<0> (zero), spans will be sent with each call to
253             C<send_span>.
254              
255             =cut
256              
257             has span_buffer_threshold => (
258             is => 'rw',
259             isa => IntRange[ 0, MAX_SPANS ],
260             env_key => 'DD_TRACE_PARTIAL_FLUSH_MIN_SPANS',
261             default => 100,
262             );
263              
264              
265              
266             protected_has _span_buffer => (
267             is => 'rw',
268             isa => ArrayRef,
269             init_args => undef,
270             default => sub { [] },
271             handles_via => 'Array',
272             handles => {
273             _buffer_span => 'push',
274             _span_buffer_size => 'count',
275             _buffered_spans => 'all',
276             _empty_span_buffer => 'clear',
277             },
278             );
279              
280              
281              
282             protected_has _client_halted => (
283             is => 'rw',
284             isa => Bool,
285             reader => '_has_client_halted',
286             default => 0,
287             handles_via => 'Bool',
288             handles => {
289             _halt_client => 'set'
290             },
291             );
292              
293              
294              
295             =head1 DELEGATED INSTANCE METHODS
296              
297             The following method(s) are required by the L<DataDog::Tracer|
298             OpenTracing::Implementation::DataDog::Tracer>:
299              
300             =cut
301              
302              
303              
304             =head2 C<send_span>
305              
306             This method gets called by the L<DataDog::Tracer|
307             OpenTracing::Implementation::DataDog::Tracer> to send a L<Span> with its
308             specific L<DataDog::SpanContext|OpenTracing::Implementation::DataDog::SpanContext>.
309              
310             This will typically get called during C<on_finish>.
311              
312             =head3 Required Positional Arguments
313              
314             =over
315              
316             =item C<$span>
317              
318             An L<OpenTracing Span|OpenTracing::Interface::Span> compliant object, that will
319             be serialised (using L<to_struct> and converted to JSON).
320              
321             =back
322              
323             =head3 Returns
324              
325             =over
326              
327             =item C<undef>
328              
329             in case something went wrong during the HTTP-request or the client has been
330             halted in any previous call.
331              
332             =item a positive int
333              
334             indicating the number of collected spans, in case this client has only buffered
335             the span.
336              
337             =item a negative int
338              
339             indicating the number of flushed spans, in case the client has succesfully
340             flushed the spans collected in the buffer.
341              
342             =back
343              
344             =cut
345              
346             sub send_span {
347 12     12 1 10603 my $self = shift;
348 12         24 my $span = shift;
349            
350             return
351 12 100       63 if $self->_has_client_halted();
352             # do not add more spans to the buffer
353            
354 11         50 my $new_span_buffer_size = $self->_buffer_span($span);
355            
356 11 50 50     113 return $new_span_buffer_size
357             unless ( $new_span_buffer_size // 0 ) > 0;
358             # this should be the number of spans in the buffer, should not be undef or 0
359            
360 11 100       32 return $new_span_buffer_size
361             unless $self->_should_flush_span_buffer();
362            
363 4         282 my $flushed = $self->_flush_span_buffer();
364            
365             return
366 4 100       20 unless defined $flushed;
367            
368 3         15 return -$flushed
369            
370             }
371              
372              
373              
374             =head1 INSTANCE METHODS
375              
376             =cut
377              
378              
379              
380             =head2 C<to_struct>
381              
382             Gather required data from a single span and its context, tags and baggage items.
383              
384             =head3 Required Positional Arguments
385              
386             =over
387              
388             =item C<$span>
389              
390             =back
391              
392             =head3 Returns
393              
394             a hashreference with the following keys:
395              
396             =over
397              
398             =item C<trace_id>
399              
400             =item C<span_id>
401              
402             =item C<resource>
403              
404             =item C<service>
405              
406             =item C<type> (optional)
407              
408             =item C<env> (optional)
409              
410             =item C<hostname> (optional)
411              
412             =item C<name>
413              
414             =item C<start>
415              
416             =item C<duration>
417              
418             =item C<parent_id> (optional)
419              
420             =item C<error>
421              
422             =item C<meta> (optional)
423              
424             =item C<metrics>
425              
426             =back
427              
428             =head3 Notes
429              
430             This data structure is specific for sending it through the DataDog agent and
431             therefore can not be a intance method of the DataDog::Span object.
432              
433             =cut
434              
435             sub to_struct {
436 14     14 1 101 my $self = shift;
437 14         31 my $span = shift;
438            
439 14         42 my $context = $span->get_context();
440            
441 14         299 my %meta_data = (
442             _fixup_span_tags( $span->get_tags ),
443             $context->get_baggage_items,
444             );
445            
446             # fix issue with meta-data, values must be string!
447             %meta_data =
448 14 100       1992 map { $_ => "$meta_data{$_}" } keys %meta_data
  22         117  
449             if %meta_data;
450            
451 14         273 my $data = {
452             trace_id => $context->trace_id,
453             span_id => $context->span_id,
454             resource => $context->get_resource_name,
455             service => $context->get_service_name,
456            
457             maybe
458             type => $context->get_service_type,
459            
460             maybe
461             env => $context->get_environment,
462            
463             maybe
464             hostname => $context->get_hostname,
465            
466             maybe
467             version => $context->get_version,
468            
469             name => $span->get_operation_name,
470             start => nano_seconds( $span->start_time() ),
471             duration => nano_seconds( $span->duration() ),
472            
473             maybe
474             parent_id => $span->get_parent_span_id(),
475            
476             provided _is_with_errors( $span ),
477             error => 1,
478            
479             provided %meta_data,
480             meta => { %meta_data },
481            
482             # metrics => ... ,
483             };
484            
485             # TODO: use Hash::Ordered, so we can control what will be the first item in
486             # the long string of JSON text. But this needs investigation on how
487             # this behaves with JSON
488            
489 14         1965 return $data
490             }
491              
492              
493              
494             =head1 ENVIRONMENT VARIABLES
495              
496             For configuring DataDog Tracing there is support for the folllowing environment
497             variables:
498              
499              
500              
501             =head2 C<DD_AGENT_HOST>
502              
503             Hostname for where to send traces to. If using a containerized environment,
504             configure this to be the host IP.
505              
506             B<default:> C<localhost>
507              
508              
509              
510             =head2 C<DD_TRACE_AGENT_PORT>
511              
512             The port number the Agent is listening on for configured host. If the Agent
513             configuration sets receiver_port or C<DD_APM_RECEIVER_PORT> to something other
514             than the default B<8126>, then C<DD_TRACE_AGENT_PORT> or C<DD_TRACE_AGENT_URL>
515             must match it.
516              
517             B<default:> C<8126>
518              
519              
520             =head2 C<DD_TRACE_AGENT_URL>
521              
522             The URL to send traces to. If the Agent configuration sets receiver_port or
523             C<DD_APM_RECEIVER_PORT> to something other than the default B<8126>, then
524             C<DD_TRACE_AGENT_PORT> or C<DD_TRACE_AGENT_URL> must match it. The URL value can
525             start with C<http://> to connect B<using HTTP> or with C<unix://> to use a
526             B<Unix Domain Socket>.
527              
528             When set this takes precedence over C<DD_AGENT_HOST> and C<DD_TRACE_AGENT_PORT>.
529              
530             B<CAVEATE: > the C<unix:> scheme is non-exisitent, and is not supported with the
531             L<DataDog::Client|OpenTracing::Implementation::DataDog::Client>.
532              
533              
534              
535             =head2 C<DD_TRACE_PARTIAL_FLUSH_MIN_SPANS>
536              
537             Set a number of partial spans to flush on. Useful to reduce memory overhead when
538             dealing with heavy traffic or long running traces.
539              
540             B<default:> 100
541              
542              
543              
544             =head1 SEE ALSO
545              
546             =over
547              
548             =item L<OpenTracing::Implementation::DataDog>
549              
550             Sending traces to DataDog using Agent.
551              
552             =item L<DataDog Docs API Tracing|https://docs.datadoghq.com/api/v1/tracing/>
553              
554             The DataDog B<Agent API> Documentation.
555              
556             =item L<LWP::UserAgent>
557              
558             Web user agent class
559              
560             =item L<JSON::Maybe::XS>
561              
562             Use L<Cpanel::JSON::XS> with a fallback to L<JSON::XS> and L<JSON::PP>
563              
564             =item L<HTTP::Request>
565              
566             HTTP style request message
567              
568             =item L<HTTP::Response>
569              
570             HTTP style response message
571              
572             =back
573              
574              
575              
576             =head1 AUTHOR
577              
578             Theo van Hoesel <tvanhoesel@perceptyx.com>
579              
580              
581              
582             =head1 COPYRIGHT AND LICENSE
583              
584             'OpenTracing::Implementation::DataDog'
585             is Copyright (C) 2019 .. 2021, Perceptyx Inc
586              
587             This library is free software; you can redistribute it and/or modify it under
588             the terms of the Artistic License 2.0.
589              
590             This package is distributed in the hope that it will be useful, but it is
591             provided "as is" and without any express or implied warranties.
592              
593             For details, see the full text of the license in the file LICENSE.
594              
595              
596             =cut
597              
598              
599             # _fixup_span_tags
600             #
601             # rename and or remove key value pairs from standard OpenTracing to what
602             # DataDog expects to be send
603             #
604             sub _fixup_span_tags {
605 14     14   2060 my %tags = @_;
606            
607 14         38 my $error = delete $tags { error };
608            
609 14 100       46 $tags { 'error.type' } = delete $tags{ 'error.kind' }
610             if $error;
611            
612 14 100       45 $tags { 'error.message' } = delete $tags{ 'message' }
613             if $error;
614            
615 14         329 return %tags;
616             }
617              
618              
619              
620             # _flush_span_buffer
621             #
622             # Flushes the spans in the span buffer and send them off to the DataDog agent
623             # over HTTP.
624             #
625             # Returns the number off flushed spans or `undef` in case of an error.
626             #
627             sub _flush_span_buffer {
628 6     6   51 my $self = shift;
629            
630 6         36 my @structs = map {$self->to_struct($_) } $self->_buffered_spans();
  13         78  
631            
632 6 100       45 my $resp = $self->_http_post_struct_as_json( [ \@structs ] )
633             or return;
634            
635 4         33 $self->_empty_span_buffer();
636            
637 4         1715 return scalar @structs;
638             }
639              
640              
641              
642             # checks if there is an exisiting 'error' tag
643             #
644             sub _is_with_errors {
645 14     14   1700 my $span = shift;
646             return exists { $span->get_tags() }->{ error }
647 14         284 }
648              
649              
650              
651             # _is_uri
652             #
653             # Returns true if the given string matches an http(s) url
654             #
655             sub _is_uri {
656 4     4   44 return $RE{URI}{HTTP}{-scheme => 'https?'}->matches(shift)
657             # scheme must be specified, defaults to 'http:'
658             }
659              
660              
661              
662             # _http_headers_with_trace_count
663             #
664             # Returns a list of HTTP Headers needed for DataDog
665             #
666             # This feature was originally added, so the Trace-Count could dynamically set
667             # per request. That was a design flaw, and now the count is hardcoded to '1',
668             # until we figured out how to send multiple spans.
669             #
670             sub _http_headers_with_trace_count {
671 6     6   13 my $self = shift;
672 6         14 my $count = shift;
673            
674             return (
675 6         31 $self->_default_http_headers_list,
676            
677             maybe
678             'X-Datadog-Trace-Count' => $count,
679             )
680             }
681              
682              
683              
684             # _http_post_struct_as_json
685             #
686             # Takes a given data structure and sends an HTTP POST request to the tracing
687             # agent.
688             #
689             # It is the caller's responsibility to generate the correct data structure!
690             #
691             # Maybe returns an HTTP::Response object, which may indicate a failure.
692             #
693             sub _http_post_struct_as_json {
694 7     7   1069 my $self = shift;
695 7         14 my $struct = shift;
696            
697             return
698 7 100       38 if $self->_has_client_halted();
699             # this shouldn't be needed, but will happen on DEMOLISH & spans in buffer
700              
701 6         133 my $encoded_data = $self->_json_encode($struct);
702 0         0 do { warn "$encoded_data\n" }
703 6 50       818 if $ENV{OPENTRACING_DEBUG};
704            
705 6         15 my @headers = $self->_http_headers_with_trace_count( scalar @{$struct->[0]} );
  6         29  
706 6         334 my $rqst = HTTP::Request->new( 'POST', $self->uri, \@headers, $encoded_data );
707            
708 6         36262 my $resp = $self->_send_http_request( $rqst );
709 6 100       307 if ( $resp->is_error ) {
710             #
711             # not interested in what the error actually has been, no matter what it
712             # was, this client will be halted, be it an error in the data send (XXX)
713             # or a problem with the recipient tracing agent.
714             #
715 1         15 $self->_halt_client();
716 1         40 warn sprintf "DataDog::Client being halted due to an error [%s]\n",
717             $resp->status_line;
718 1         107 return;
719             }
720            
721 5         120 return $resp;
722             }
723              
724              
725              
726             # _last_buffered_span
727             #
728             # Returns the last span added to the buffer.
729             #
730             # nothing special, but just easier to read the code where it is used
731             #
732             sub _last_buffered_span {
733 11     11   23 my $self = shift;
734            
735 11         288 return $self->_span_buffer->[-1]
736             }
737              
738              
739              
740             # _should_flush_span_buffer
741             #
742             # Returns a 'Boolean'
743             #
744             # For obvious reasons, it should be flushed if the limit has been reached.
745             # But another reason is when the root-span has been just added. It is the first
746             # span being created, but it is therefor the last one being closed and send.
747             #
748             sub _should_flush_span_buffer {
749 11     11   23 my $self = shift;
750            
751             return (
752 11   100     33 $self->_last_buffered_span()->is_root_span
753             or
754             $self->_span_buffer_threshold_reached()
755             );
756             }
757              
758              
759              
760             # _span_buffer_threshold_reached
761             #
762             # Returns a 'Boolean', being 'true' once the limit has been reached
763             #
764             sub _span_buffer_threshold_reached {
765 10     10   2568 my $self = shift;
766            
767 10         39 return $self->_span_buffer_size >= $self->span_buffer_threshold
768             }
769              
770              
771              
772             # DEMOLISH
773             #
774             # This should not happen, but just in case something went completely wrong, this
775             # will try to flush the buffered spans as a last resort.
776             #
777             sub DEMOLISH {
778 9     9 0 36858 my ($self) = @_;
779            
780 9 100       81 $self->_flush_span_buffer() if $self->_span_buffer_size(); # send leftovers
781            
782 9         230 return;
783             }
784              
785             1;