azure_application_insights 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/gem-push.yml +34 -0
  3. data/.gitignore +40 -0
  4. data/.travis.yml +22 -0
  5. data/CHANGELOG.md +17 -0
  6. data/CONTRIBUTING.md +68 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +11 -0
  9. data/README.md +161 -0
  10. data/Rakefile +15 -0
  11. data/application_insights.gemspec +30 -0
  12. data/lib/application_insights/channel/asynchronous_queue.rb +58 -0
  13. data/lib/application_insights/channel/asynchronous_sender.rb +133 -0
  14. data/lib/application_insights/channel/contracts/application.rb +14 -0
  15. data/lib/application_insights/channel/contracts/cloud.rb +14 -0
  16. data/lib/application_insights/channel/contracts/data.rb +14 -0
  17. data/lib/application_insights/channel/contracts/data_point.rb +24 -0
  18. data/lib/application_insights/channel/contracts/data_point_type.rb +7 -0
  19. data/lib/application_insights/channel/contracts/dependency_kind.rb +9 -0
  20. data/lib/application_insights/channel/contracts/dependency_source_type.rb +9 -0
  21. data/lib/application_insights/channel/contracts/device.rb +28 -0
  22. data/lib/application_insights/channel/contracts/envelope.rb +40 -0
  23. data/lib/application_insights/channel/contracts/event_data.rb +28 -0
  24. data/lib/application_insights/channel/contracts/exception_data.rb +37 -0
  25. data/lib/application_insights/channel/contracts/exception_details.rb +28 -0
  26. data/lib/application_insights/channel/contracts/internal.rb +14 -0
  27. data/lib/application_insights/channel/contracts/json_serializable.rb +59 -0
  28. data/lib/application_insights/channel/contracts/location.rb +16 -0
  29. data/lib/application_insights/channel/contracts/message_data.rb +24 -0
  30. data/lib/application_insights/channel/contracts/metric_data.rb +27 -0
  31. data/lib/application_insights/channel/contracts/operation.rb +19 -0
  32. data/lib/application_insights/channel/contracts/page_view_data.rb +30 -0
  33. data/lib/application_insights/channel/contracts/remote_dependency_data.rb +56 -0
  34. data/lib/application_insights/channel/contracts/request_data.rb +36 -0
  35. data/lib/application_insights/channel/contracts/session.rb +15 -0
  36. data/lib/application_insights/channel/contracts/severity_level.rb +13 -0
  37. data/lib/application_insights/channel/contracts/stack_frame.rb +17 -0
  38. data/lib/application_insights/channel/contracts/user.rb +19 -0
  39. data/lib/application_insights/channel/event.rb +68 -0
  40. data/lib/application_insights/channel/queue_base.rb +73 -0
  41. data/lib/application_insights/channel/sender_base.rb +88 -0
  42. data/lib/application_insights/channel/synchronous_queue.rb +45 -0
  43. data/lib/application_insights/channel/synchronous_sender.rb +17 -0
  44. data/lib/application_insights/channel/telemetry_channel.rb +131 -0
  45. data/lib/application_insights/channel/telemetry_context.rb +85 -0
  46. data/lib/application_insights/rack/track_request.rb +158 -0
  47. data/lib/application_insights/telemetry_client.rb +229 -0
  48. data/lib/application_insights/unhandled_exception.rb +49 -0
  49. data/lib/application_insights/version.rb +3 -0
  50. data/lib/application_insights.rb +9 -0
  51. data/test/application_insights/channel/contracts/test_application.rb +44 -0
  52. data/test/application_insights/channel/contracts/test_cloud.rb +44 -0
  53. data/test/application_insights/channel/contracts/test_data.rb +44 -0
  54. data/test/application_insights/channel/contracts/test_data_point.rb +109 -0
  55. data/test/application_insights/channel/contracts/test_device.rb +200 -0
  56. data/test/application_insights/channel/contracts/test_envelope.rb +209 -0
  57. data/test/application_insights/channel/contracts/test_event_data.rb +62 -0
  58. data/test/application_insights/channel/contracts/test_exception_data.rb +111 -0
  59. data/test/application_insights/channel/contracts/test_exception_details.rb +106 -0
  60. data/test/application_insights/channel/contracts/test_internal.rb +44 -0
  61. data/test/application_insights/channel/contracts/test_location.rb +70 -0
  62. data/test/application_insights/channel/contracts/test_message_data.rb +66 -0
  63. data/test/application_insights/channel/contracts/test_metric_data.rb +50 -0
  64. data/test/application_insights/channel/contracts/test_operation.rb +109 -0
  65. data/test/application_insights/channel/contracts/test_page_view_data.rb +88 -0
  66. data/test/application_insights/channel/contracts/test_remote_dependency_data.rb +209 -0
  67. data/test/application_insights/channel/contracts/test_request_data.rb +153 -0
  68. data/test/application_insights/channel/contracts/test_session.rb +57 -0
  69. data/test/application_insights/channel/contracts/test_stack_frame.rb +83 -0
  70. data/test/application_insights/channel/contracts/test_user.rb +96 -0
  71. data/test/application_insights/channel/test_asynchronous_queue.rb +47 -0
  72. data/test/application_insights/channel/test_asynchronous_sender.rb +81 -0
  73. data/test/application_insights/channel/test_event.rb +53 -0
  74. data/test/application_insights/channel/test_queue_base.rb +89 -0
  75. data/test/application_insights/channel/test_sender_base.rb +94 -0
  76. data/test/application_insights/channel/test_synchronous_queue.rb +28 -0
  77. data/test/application_insights/channel/test_synchronous_sender.rb +11 -0
  78. data/test/application_insights/channel/test_telemetry_channel.rb +167 -0
  79. data/test/application_insights/channel/test_telemetry_context.rb +83 -0
  80. data/test/application_insights/mock_sender.rb +37 -0
  81. data/test/application_insights/rack/test_track_request.rb +182 -0
  82. data/test/application_insights/test_logger.rb +10 -0
  83. data/test/application_insights/test_telemetry_client.rb +138 -0
  84. data/test/application_insights/test_unhandled_exception.rb +23 -0
  85. data/test/application_insights.rb +8 -0
  86. metadata +247 -0
@@ -0,0 +1,73 @@
1
+ require 'thread'
2
+
3
+ module ApplicationInsights
4
+ module Channel
5
+ # The base class for all types of queues for use in conjunction with an
6
+ # implementation of {SenderBase}. The queue will notify the sender that it
7
+ # needs to pick up items when it reaches {#max_queue_length}, or when the
8
+ # consumer calls {#flush}.
9
+ class QueueBase
10
+ # Initializes a new instance of the class.
11
+ # @param [SenderBase] sender the sender object that will be used in
12
+ # conjunction with this queue.
13
+ def initialize(sender)
14
+ @queue = Queue.new
15
+ @max_queue_length = 500
16
+ self.sender = sender
17
+ end
18
+
19
+ # The maximum number of items that will be held by the queue before the
20
+ # queue will call the {#flush} method.
21
+ # @return [Fixnum] the maximum queue size. (defaults to: 500)
22
+ attr_accessor :max_queue_length
23
+
24
+ # The sender that is associated with this queue that this queue will use to
25
+ # send data to the service.
26
+ # @return [SenderBase] the sender object.
27
+ attr_reader :sender
28
+
29
+ # Change the sender that is associated with this queue.
30
+ # @param [SenderBase] sender the sender object.
31
+ # @return [SenderBase] the sender object.
32
+ def sender=(sender)
33
+ @sender = sender
34
+ @sender.queue = self if sender
35
+ @sender
36
+ end
37
+
38
+ # Adds the passed in item object to the queue and calls {#flush} if the
39
+ # size of the queue is larger than {#max_queue_length}. This method does
40
+ # nothing if the passed in item is nil.
41
+ # @param [Contracts::Envelope] item the telemetry envelope object to send
42
+ # to the service.
43
+ def push(item)
44
+ return unless item
45
+
46
+ @queue.push(item)
47
+
48
+ flush if @queue.length >= @max_queue_length
49
+ end
50
+
51
+ # Pops a single item from the queue and returns it. If the queue is empty,
52
+ # this method will return nil.
53
+ # @return [Contracts::Envelope] a telemetry envelope object or nil if the
54
+ # queue is empty.
55
+ def pop
56
+ return @queue.pop(true)
57
+ rescue ThreadError
58
+ return nil
59
+ end
60
+
61
+ # Flushes the current queue by notifying the {#sender}. This method needs
62
+ # to be overridden by a concrete implementations of the queue class.
63
+ def flush
64
+ end
65
+
66
+ # Indicates whether the queue is empty.
67
+ # @return [Boolean] true if the queue is empty
68
+ def empty?
69
+ @queue.empty?
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,88 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'openssl'
4
+ require 'stringio'
5
+ require 'zlib'
6
+ require 'logger'
7
+
8
+ module ApplicationInsights
9
+ module Channel
10
+ # The base class for all types of senders for use in conjunction with an
11
+ # implementation of {QueueBase}. The queue will notify the sender that it
12
+ # needs to pick up items. The concrete sender implementation will listen to
13
+ # these notifications and will pull items from the queue using
14
+ # {QueueBase#pop} getting at most {#send_buffer_size} items.
15
+ # It will then call {#send} using the list of items pulled from the queue.
16
+ class SenderBase
17
+ # Initializes a new instance of the class.
18
+ # @param [String] service_endpoint_uri the address of the service to send
19
+ # telemetry data to.
20
+ def initialize(service_endpoint_uri)
21
+ @service_endpoint_uri = service_endpoint_uri
22
+ @queue = nil
23
+ @send_buffer_size = 100
24
+ @logger = Logger.new(STDOUT)
25
+ end
26
+
27
+ # The service endpoint URI where this sender will send data to.
28
+ # @return [String] the service endpoint URI.
29
+ attr_accessor :service_endpoint_uri
30
+
31
+ # The queue that this sender is draining. While {SenderBase} doesn't
32
+ # implement any means of doing so, derivations of this class do.
33
+ # @return [QueueBase] the queue instance that this sender is draining.
34
+ attr_accessor :queue
35
+
36
+ # The buffer size for a single batch of telemetry. This is the maximum number
37
+ # of items in a single service request that this sender is going to send.
38
+ # @return [Fixnum] the maximum number of items in a telemetry batch.
39
+ attr_accessor :send_buffer_size
40
+
41
+ # The logger for the sender.
42
+ attr_accessor :logger
43
+
44
+ # Immediately sends the data passed in to {#service_endpoint_uri}. If the
45
+ # service request fails, the passed in items are pushed back to the {#queue}.
46
+ # @param [Array<Contracts::Envelope>] data_to_send an array of
47
+ # {Contracts::Envelope} objects to send to the service.
48
+ def send(data_to_send)
49
+ uri = URI(@service_endpoint_uri)
50
+ headers = {
51
+ 'Accept' => 'application/json',
52
+ 'Content-Type' => 'application/json; charset=utf-8',
53
+ 'Content-Encoding' => 'gzip'
54
+ }
55
+ request = Net::HTTP::Post.new(uri.path, headers)
56
+
57
+ # Use JSON.generate instead of to_json, otherwise it will
58
+ # default to ActiveSupport::JSON.encode for Rails app
59
+ json = JSON.generate(data_to_send)
60
+ compressed_data = compress(json)
61
+ request.body = compressed_data
62
+
63
+ http = Net::HTTP.new uri.hostname, uri.port
64
+ if uri.scheme.downcase == 'https'
65
+ http.use_ssl = true
66
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
67
+ end
68
+
69
+ response = http.request(request)
70
+ http.finish if http.started?
71
+
72
+ if !response.kind_of? Net::HTTPSuccess
73
+ @logger.warn('application_insights') { "Failed to send data: #{response.message}" }
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def compress(string)
80
+ wio = StringIO.new("w")
81
+ w_gz = Zlib::GzipWriter.new wio, nil, nil
82
+ w_gz.write(string)
83
+ w_gz.close
84
+ wio.string
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,45 @@
1
+ require_relative 'queue_base'
2
+
3
+ module ApplicationInsights
4
+ module Channel
5
+ # A synchronous queue for use in conjunction with the {SynchronousSender}.
6
+ # The queue will call {SenderBase#send} when it reaches {#max_queue_length},
7
+ # or when the consumer calls {#flush}.
8
+ #
9
+ # @example
10
+ # require 'application_insights'
11
+ # require 'thread'
12
+ # queue = ApplicationInsights::Channel::SynchronousQueue.new nil
13
+ # queue.max_queue_length = 1
14
+ # queue.push 1
15
+ class SynchronousQueue < QueueBase
16
+ # Initializes a new instance of the class.
17
+ # @param [SenderBase] sender the sender object that will be used in
18
+ # conjunction with this queue.
19
+ def initialize(sender)
20
+ super sender
21
+ end
22
+
23
+ # Flushes the current queue by by calling {#sender}'s
24
+ # {SenderBase#send} method.
25
+ def flush
26
+ local_sender = @sender
27
+ return unless local_sender
28
+
29
+ while true
30
+ # get at most send_buffer_size items and send them
31
+ data = []
32
+ while data.length < local_sender.send_buffer_size
33
+ item = pop()
34
+ break if not item
35
+ data.push item
36
+ end
37
+
38
+ break if data.length == 0
39
+
40
+ local_sender.send(data)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'sender_base'
2
+
3
+ module ApplicationInsights
4
+ module Channel
5
+ # A synchronous sender that works in conjunction with the {SynchronousQueue}.
6
+ # The queue will call {#send} on the current instance with the data to send.
7
+ class SynchronousSender < SenderBase
8
+ SERVICE_ENDPOINT_URI = 'https://dc.services.visualstudio.com/v2/track'
9
+ # Initializes a new instance of the class.
10
+ # @param [String] service_endpoint_uri the address of the service to send
11
+ # telemetry data to.
12
+ def initialize(service_endpoint_uri = SERVICE_ENDPOINT_URI)
13
+ super service_endpoint_uri
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,131 @@
1
+ require 'time'
2
+ require_relative 'asynchronous_queue'
3
+ require_relative 'asynchronous_sender'
4
+ require_relative 'telemetry_context'
5
+ require_relative 'synchronous_queue'
6
+ require_relative 'synchronous_sender'
7
+ require_relative 'contracts/envelope'
8
+ require_relative 'contracts/data'
9
+ require_relative 'contracts/internal'
10
+ require_relative '../../application_insights/version'
11
+
12
+ module ApplicationInsights
13
+ module Channel
14
+ # The telemetry channel is responsible for constructing a
15
+ # {Contracts::Envelope} object from the passed in data and specified
16
+ # telemetry context.
17
+ #
18
+ # @example
19
+ # require 'application_insights'
20
+ # channel = ApplicationInsights::Channel::TelemetryChannel.new
21
+ # event = ApplicationInsights::Channel::Contracts::EventData.new name: 'My event'
22
+ # channel.write event
23
+ class TelemetryChannel
24
+ # Initializes a new instance of the class.
25
+ # @param [TelemetryContext] context the telemetry context to use when
26
+ # sending telemetry data.
27
+ # @param [QueueBase] queue the queue to enqueue the resulting
28
+ # {Contracts::Envelope} to.
29
+ def initialize(context=nil, queue=nil)
30
+ @context = context || TelemetryContext.new
31
+ @queue = queue || SynchronousQueue.new(SynchronousSender.new)
32
+ end
33
+
34
+ # The context associated with this channel. All {Contracts::Envelope}
35
+ # objects created by this channel will use this value if it's present or if
36
+ # none is specified as part of the {#write} call.
37
+ # @return [TelemetryContext] the context instance
38
+ # (defaults to: TelemetryContext.new)
39
+ attr_reader :context
40
+
41
+ # The queue associated with this channel. All {Contracts::Envelope} objects
42
+ # created by this channel will be pushed to this queue.
43
+ # @return [QueueBase] the queue instance (defaults to: SynchronousQueue.new)
44
+ attr_reader :queue
45
+
46
+ # The sender associated with this channel. This instance will be used to
47
+ # transmit telemetry to the service.
48
+ # @return [SenderBase] the sender instance (defaults to: SynchronousSender.new)
49
+ def sender
50
+ @queue.sender
51
+ end
52
+
53
+ # Flushes the enqueued data by calling {QueueBase#flush}.
54
+ def flush
55
+ @queue.flush
56
+ end
57
+
58
+ # Enqueues the passed in data to the {#queue}. If the caller specifies a
59
+ # context as well, it will take precedence over the instance in {#context}.
60
+ # @param [Object] data the telemetry data to send. This will be wrapped in
61
+ # an {Contracts::Envelope} before being enqueued to the {#queue}.
62
+ # @param [TelemetryContext] context the override context to use when
63
+ # constructing the {Contracts::Envelope}.
64
+ # @param [Time|String] time the timestamp of the telemetry used to construct the
65
+ # {Contracts::Envelope}.
66
+ def write(data, context=nil, time=nil)
67
+ local_context = context || @context
68
+ raise ArgumentError, 'Context was required but not provided' unless local_context
69
+
70
+ if time && time.is_a?(String)
71
+ local_time = time
72
+ elsif time && time.is_a?(Time)
73
+ local_time = time.iso8601(7)
74
+ else
75
+ local_time = Time.now.iso8601(7)
76
+ end
77
+
78
+ data_type = data.class.name.gsub(/^.*::/, '')
79
+ set_properties data, local_context
80
+ data_attributes = {
81
+ :base_type => data_type,
82
+ :base_data => data
83
+ }
84
+ envelope_attributes = {
85
+ :name => 'Microsoft.ApplicationInsights.' + data_type[0..-5],
86
+ :time => local_time,
87
+ :i_key => local_context.instrumentation_key,
88
+ :tags => get_tags(local_context),
89
+ :data => Contracts::Data.new(data_attributes)
90
+ }
91
+ envelope = Contracts::Envelope.new envelope_attributes
92
+ @queue.push(envelope)
93
+ end
94
+
95
+ private
96
+
97
+ def get_tags(context)
98
+ hash = {}
99
+ internal_context_attributes = {
100
+ :sdk_version => 'rb:' + ApplicationInsights::VERSION
101
+ }
102
+ internal_context = Contracts::Internal.new internal_context_attributes
103
+
104
+ [internal_context,
105
+ context.application,
106
+ context.cloud,
107
+ context.device,
108
+ context.user,
109
+ context.session,
110
+ context.location,
111
+ context.operation].each { |c| hash.merge!(c.to_h) if c }
112
+
113
+ hash.delete_if { |k, v| v.nil? }
114
+
115
+ hash
116
+ end
117
+
118
+ def set_properties(data, context)
119
+ if context.properties
120
+ properties = data.properties || {}
121
+ context.properties.each do |key, value|
122
+ unless properties.key?(key)
123
+ properties[key] = value
124
+ end
125
+ end
126
+ data.properties = properties
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,85 @@
1
+ require_relative 'contracts/application'
2
+ require_relative 'contracts/cloud'
3
+ require_relative 'contracts/device'
4
+ require_relative 'contracts/user'
5
+ require_relative 'contracts/session'
6
+ require_relative 'contracts/operation'
7
+ require_relative 'contracts/location'
8
+
9
+ module ApplicationInsights
10
+ module Channel
11
+ # Represents the context for sending telemetry to the
12
+ # Application Insights service.
13
+ #
14
+ # @example
15
+ # require 'application_insights'
16
+ # context = ApplicationInsights::Channel::TelemetryContext.new
17
+ # context.instrumentation_key = '<YOUR INSTRUMENTATION KEY GOES HERE>'
18
+ # context.application.id = 'My application'
19
+ # context.application.ver = '1.2.3'
20
+ # context.device.id = 'My current device'
21
+ # context.device.oem_name = 'Asus'
22
+ # context.device.model = 'X31A'
23
+ # context.device.type = "Other"
24
+ # context.user.id = 'santa@northpole.net'
25
+ class TelemetryContext
26
+ # Initializes a new instance of the class.
27
+ def initialize
28
+ @instrumentation_key = nil
29
+ @application = Contracts::Application.new
30
+ @cloud = Contracts::Cloud.new
31
+ @device = Contracts::Device.new
32
+ @user = Contracts::User.new
33
+ @session = Contracts::Session.new
34
+ @operation = Contracts::Operation.new
35
+ @location = Contracts::Location.new
36
+ @properties = {}
37
+ end
38
+
39
+ # The instrumentation key that is used to identify which
40
+ # Application Insights application this data is for.
41
+ # @return [String] the instrumentation key.
42
+ attr_accessor :instrumentation_key
43
+
44
+ # The application context. This contains properties of the
45
+ # application you are running.
46
+ # @return [Contracts::Application] the context object.
47
+ attr_accessor :application
48
+
49
+ # The cloud context. This contains properties of the
50
+ # cloud role you are generating telemetry for.
51
+ # @return [Contracts::Cloud] the context object.
52
+ attr_accessor :cloud
53
+
54
+ # The device context. This contains properties of the
55
+ # device you are running on.
56
+ # @return [Contracts::Device] the context object.
57
+ attr_accessor :device
58
+
59
+ # The user context. This contains properties of the
60
+ # user you are generating telemetry for.
61
+ # @return [Contracts::User] the context object.
62
+ attr_accessor :user
63
+
64
+ # The session context. This contains properties of the
65
+ # session you are generating telemetry for.
66
+ # @return [Contracts::Session] the context object.
67
+ attr_accessor :session
68
+
69
+ # The operation context. This contains properties of the
70
+ # operation you are generating telemetry for.
71
+ # @return [Contracts::Operation] the context object.
72
+ attr_accessor :operation
73
+
74
+ # The location context. This contains properties of the
75
+ # location you are generating telemetry from.
76
+ # @return [Contracts::Location] the context object.
77
+ attr_accessor :location
78
+
79
+ # The property context. This contains free-form properties
80
+ # that you can add to your telemetry.
81
+ # @return [Hash<String, String>] the context object.
82
+ attr_accessor :properties
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,158 @@
1
+ require 'rack'
2
+ require 'securerandom'
3
+ require_relative '../channel/contracts/request_data'
4
+ require_relative '../telemetry_client'
5
+
6
+ module ApplicationInsights
7
+ module Rack
8
+ # Track every request and sends the request data to Application Insights.
9
+ class TrackRequest
10
+ # Initializes a new instance of the class.
11
+ # @param [Object] app the inner rack application.
12
+ # @param [String] instrumentation_key to identify which Application Insights
13
+ # application this data is for.
14
+ # @param [Fixnum] buffer_size the buffer size and the buffered requests would
15
+ # send to Application Insights when buffer is full.
16
+ # @param [Fixnum] send_interval the frequency (in seconds) to check buffer
17
+ # and send buffered requests to Application Insights if any.
18
+ def initialize(app, instrumentation_key, buffer_size = 500, send_interval = 60)
19
+ @app = app
20
+ @instrumentation_key = instrumentation_key
21
+ @buffer_size = buffer_size
22
+ @send_interval = send_interval
23
+
24
+ @sender = Channel::AsynchronousSender.new
25
+ @sender.send_interval = @send_interval
26
+ queue = Channel::AsynchronousQueue.new @sender
27
+ queue.max_queue_length = @buffer_size
28
+ @channel = Channel::TelemetryChannel.new nil, queue
29
+
30
+ @client = TelemetryClient.new @instrumentation_key, @channel
31
+ end
32
+
33
+ # Track requests and send data to Application Insights asynchronously.
34
+ # @param [Hash] env the rack environment.
35
+ def call(env)
36
+ # Build a request ID, incorporating one from our request if one exists.
37
+ request_id = request_id_header(env['HTTP_REQUEST_ID'])
38
+ env['ApplicationInsights.request.id'] = request_id
39
+
40
+ start = Time.now
41
+ begin
42
+ status, headers, response = @app.call(env)
43
+ rescue Exception => ex
44
+ status = 500
45
+ exception = ex
46
+ end
47
+ stop = Time.now
48
+
49
+ start_time = start.iso8601(7)
50
+ duration = format_request_duration(stop - start)
51
+ success = status.to_i < 400
52
+
53
+ request = ::Rack::Request.new env
54
+ options = options_hash(request)
55
+
56
+ data = request_data(request_id, start_time, duration, status, success, options)
57
+ context = telemetry_context(request_id, env['HTTP_REQUEST_ID'])
58
+
59
+ @client.channel.write data, context, start_time
60
+
61
+ if exception
62
+ @client.track_exception exception, handled_at: 'Unhandled'
63
+ raise exception
64
+ end
65
+
66
+ [status, headers, response]
67
+ end
68
+
69
+ private
70
+
71
+ def sender=(sender)
72
+ if sender.is_a? Channel::AsynchronousSender
73
+ @sender = sender
74
+ @client.channel.queue.sender = @sender
75
+ end
76
+ end
77
+
78
+ def client
79
+ @client
80
+ end
81
+
82
+ def format_request_duration(duration_seconds)
83
+ if duration_seconds >= 86400
84
+ # just return 1 day when it takes more than 1 day which should not happen for requests.
85
+ return "%02d.%02d:%02d:%02d.%07d" % [1, 0, 0, 0, 0]
86
+ end
87
+
88
+ Time.at(duration_seconds).gmtime.strftime("00.%H:%M:%S.%7N")
89
+ end
90
+
91
+ def request_id_header(request_id)
92
+ valid_request_id_header = valid_request_id(request_id)
93
+
94
+ length = valid_request_id_header ? 5 : 10
95
+ id = SecureRandom.base64(length)
96
+
97
+ if valid_request_id_header
98
+ request_id_has_end = %w[. _].include?(request_id[-1])
99
+ request_id << '.' unless request_id_has_end
100
+
101
+ return "#{request_id}#{id}_"
102
+ end
103
+
104
+ "|#{id}."
105
+ end
106
+
107
+ def valid_request_id(request_id)
108
+ request_id && request_id[0] == '|'
109
+ end
110
+
111
+ def operation_id(id)
112
+ # Returns the root ID from the '|' to the first '.' if any.
113
+ root_start = id[0] == '|' ? 1 : 0
114
+
115
+ root_end = id.index('.')
116
+ root_end = root_end ? root_end - 1 : id.length - root_start
117
+
118
+ id[root_start..root_end]
119
+ end
120
+
121
+ def options_hash(request)
122
+ {
123
+ name: "#{request.request_method} #{request.path}",
124
+ http_method: request.request_method,
125
+ url: request.url,
126
+ properties: {
127
+ params: request.params.to_json,
128
+ requestBody: request.body.to_json
129
+ }
130
+ }
131
+ end
132
+
133
+ def request_data(request_id, start_time, duration, status, success, options)
134
+ Channel::Contracts::RequestData.new(
135
+ :id => request_id || 'Null',
136
+ :start_time => start_time || Time.now.iso8601(7),
137
+ :duration => duration || '0:00:00:00.0000000',
138
+ :response_code => status || 200,
139
+ :success => success == nil ? true : success,
140
+ :name => options[:name],
141
+ :http_method => options[:http_method],
142
+ :url => options[:url],
143
+ :properties => options[:properties] || {},
144
+ :measurements => options[:measurements] || {}
145
+ )
146
+ end
147
+
148
+ def telemetry_context(request_id, request_id_header)
149
+ context = Channel::TelemetryContext.new
150
+ context.instrumentation_key = @instrumentation_key
151
+ context.operation.id = operation_id(request_id)
152
+ context.operation.parent_id = request_id_header
153
+
154
+ context
155
+ end
156
+ end
157
+ end
158
+ end