azure_application_insights 0.5.7

Sign up to get free protection for your applications and to get access to all the features.
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