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.
- checksums.yaml +7 -0
- data/.github/workflows/gem-push.yml +34 -0
- data/.gitignore +40 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +17 -0
- data/CONTRIBUTING.md +68 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +11 -0
- data/README.md +161 -0
- data/Rakefile +15 -0
- data/application_insights.gemspec +30 -0
- data/lib/application_insights/channel/asynchronous_queue.rb +58 -0
- data/lib/application_insights/channel/asynchronous_sender.rb +133 -0
- data/lib/application_insights/channel/contracts/application.rb +14 -0
- data/lib/application_insights/channel/contracts/cloud.rb +14 -0
- data/lib/application_insights/channel/contracts/data.rb +14 -0
- data/lib/application_insights/channel/contracts/data_point.rb +24 -0
- data/lib/application_insights/channel/contracts/data_point_type.rb +7 -0
- data/lib/application_insights/channel/contracts/dependency_kind.rb +9 -0
- data/lib/application_insights/channel/contracts/dependency_source_type.rb +9 -0
- data/lib/application_insights/channel/contracts/device.rb +28 -0
- data/lib/application_insights/channel/contracts/envelope.rb +40 -0
- data/lib/application_insights/channel/contracts/event_data.rb +28 -0
- data/lib/application_insights/channel/contracts/exception_data.rb +37 -0
- data/lib/application_insights/channel/contracts/exception_details.rb +28 -0
- data/lib/application_insights/channel/contracts/internal.rb +14 -0
- data/lib/application_insights/channel/contracts/json_serializable.rb +59 -0
- data/lib/application_insights/channel/contracts/location.rb +16 -0
- data/lib/application_insights/channel/contracts/message_data.rb +24 -0
- data/lib/application_insights/channel/contracts/metric_data.rb +27 -0
- data/lib/application_insights/channel/contracts/operation.rb +19 -0
- data/lib/application_insights/channel/contracts/page_view_data.rb +30 -0
- data/lib/application_insights/channel/contracts/remote_dependency_data.rb +56 -0
- data/lib/application_insights/channel/contracts/request_data.rb +36 -0
- data/lib/application_insights/channel/contracts/session.rb +15 -0
- data/lib/application_insights/channel/contracts/severity_level.rb +13 -0
- data/lib/application_insights/channel/contracts/stack_frame.rb +17 -0
- data/lib/application_insights/channel/contracts/user.rb +19 -0
- data/lib/application_insights/channel/event.rb +68 -0
- data/lib/application_insights/channel/queue_base.rb +73 -0
- data/lib/application_insights/channel/sender_base.rb +88 -0
- data/lib/application_insights/channel/synchronous_queue.rb +45 -0
- data/lib/application_insights/channel/synchronous_sender.rb +17 -0
- data/lib/application_insights/channel/telemetry_channel.rb +131 -0
- data/lib/application_insights/channel/telemetry_context.rb +85 -0
- data/lib/application_insights/rack/track_request.rb +158 -0
- data/lib/application_insights/telemetry_client.rb +229 -0
- data/lib/application_insights/unhandled_exception.rb +49 -0
- data/lib/application_insights/version.rb +3 -0
- data/lib/application_insights.rb +9 -0
- data/test/application_insights/channel/contracts/test_application.rb +44 -0
- data/test/application_insights/channel/contracts/test_cloud.rb +44 -0
- data/test/application_insights/channel/contracts/test_data.rb +44 -0
- data/test/application_insights/channel/contracts/test_data_point.rb +109 -0
- data/test/application_insights/channel/contracts/test_device.rb +200 -0
- data/test/application_insights/channel/contracts/test_envelope.rb +209 -0
- data/test/application_insights/channel/contracts/test_event_data.rb +62 -0
- data/test/application_insights/channel/contracts/test_exception_data.rb +111 -0
- data/test/application_insights/channel/contracts/test_exception_details.rb +106 -0
- data/test/application_insights/channel/contracts/test_internal.rb +44 -0
- data/test/application_insights/channel/contracts/test_location.rb +70 -0
- data/test/application_insights/channel/contracts/test_message_data.rb +66 -0
- data/test/application_insights/channel/contracts/test_metric_data.rb +50 -0
- data/test/application_insights/channel/contracts/test_operation.rb +109 -0
- data/test/application_insights/channel/contracts/test_page_view_data.rb +88 -0
- data/test/application_insights/channel/contracts/test_remote_dependency_data.rb +209 -0
- data/test/application_insights/channel/contracts/test_request_data.rb +153 -0
- data/test/application_insights/channel/contracts/test_session.rb +57 -0
- data/test/application_insights/channel/contracts/test_stack_frame.rb +83 -0
- data/test/application_insights/channel/contracts/test_user.rb +96 -0
- data/test/application_insights/channel/test_asynchronous_queue.rb +47 -0
- data/test/application_insights/channel/test_asynchronous_sender.rb +81 -0
- data/test/application_insights/channel/test_event.rb +53 -0
- data/test/application_insights/channel/test_queue_base.rb +89 -0
- data/test/application_insights/channel/test_sender_base.rb +94 -0
- data/test/application_insights/channel/test_synchronous_queue.rb +28 -0
- data/test/application_insights/channel/test_synchronous_sender.rb +11 -0
- data/test/application_insights/channel/test_telemetry_channel.rb +167 -0
- data/test/application_insights/channel/test_telemetry_context.rb +83 -0
- data/test/application_insights/mock_sender.rb +37 -0
- data/test/application_insights/rack/test_track_request.rb +182 -0
- data/test/application_insights/test_logger.rb +10 -0
- data/test/application_insights/test_telemetry_client.rb +138 -0
- data/test/application_insights/test_unhandled_exception.rb +23 -0
- data/test/application_insights.rb +8 -0
- 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
|