fluent-plugin-application-insights 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f77dfcd3f36125b8dc0a3fd720db0c4303274c54
4
+ data.tar.gz: 3129c6bc54acac8cbfb71fe821e5e3675f35728a
5
+ SHA512:
6
+ metadata.gz: da8c5a97877de1e48c507c586fbb352dfd7aa9a0e092edb4a9d883a1f07d861fa64e953c6761030bbb0a5b6ffa58230755194ade69443eb6b6798700dc3d4318
7
+ data.tar.gz: 49d85bf6fc2822ebf417ca85536d501be120f887e517dd3aff44fc1d454a6abe70042059db5010bfda1ae7a7711f3d6aeff3f2cd5a66d2fb30bd0174fd777bb7
data/.gitignore ADDED
@@ -0,0 +1,50 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ Gemfile.lock
46
+ .ruby-version
47
+ .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.0
4
+ - 2.2.0
5
+ - 2.3.3
6
+ - 2.4.0
7
+ - ruby-head
8
+
9
+ cache: bundler
10
+
11
+ before_install:
12
+ - gem install bundler
13
+
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: ruby-head
17
+
18
+ install:
19
+ - bundle install
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation. All rights reserved.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # fluent-plugin-application-insights
2
+ This is the [Fluentd](https://fluentd.org/) output plugin for [Azure Application Insights](https://docs.microsoft.com/azure/application-insights/)
3
+
4
+ Application Insights is an extensible Application Performance Management (APM) service for web developers on multiple platforms.
5
+ Use it to monitor your live web application. It will automatically detect performance anomalies. It includes powerful analytics
6
+ tools to help you diagnose issues and to understand what users actually do with your app.
7
+ It's designed to help you continuously improve performance and usability.
8
+
9
+ ## Installation
10
+
11
+ ```
12
+ $ gem install fluent-plugin-application-insights
13
+ ```
14
+
15
+ ## Configuration
16
+
17
+ To send data to Application Insights, add the following piece to your fluentd configuration file:
18
+
19
+ ```
20
+ <match **>
21
+ @type application_insights
22
+ instrumentation_key <your instrumentation key>
23
+ </match>
24
+ ```
25
+
26
+ Here is the configuration options for this plugin:
27
+
28
+ * `instrumentation_key` - Required, the Application Insights instrumentation key
29
+ * `send_buffer_size` - The batch size to send data to Application Insights service (default `1000`). Setting this to a large size will usually result in better output throughput.
30
+ * `standard_schema` - The parameter indicating whether the record is in standard schema. i.e., the format that is recognized by Application Insights backend (default `false`).
31
+ If the record is not in standard schema, it will be tracked as Application Insights trace telemetry. Otherwise, the record is just forwarded to the backend. See [Standard Schema](#standard-schema) for more info.
32
+ * `message_property` - The property name for the trace message (default `message`).
33
+ * `time_property` - The property name for the timestamp (default `nil`). Fluentd input plugin will assign a timestamp for each emitted record, and this timestamp is used as the telemetry creation time by default. Set the `time_property` if you want to use the value of this property instead of the one assigned by the input plugin.
34
+ * `severity_property` - The property name for severity level (default `severity`). If the severity property doesn't exist, the record will be treated as information level. See [Severity Level](https://docs.microsoft.com/azure/application-insights/application-insights-data-model-trace-telemetry#severity-level) for more info.
35
+ * `severity_level_verbose` - The value of severity property that maps to Application Insights' verbose severity level (default `verbose`).
36
+ * `severity_level_information` - The value of severity property that maps to Application Insights' information severity level (default `information`).
37
+ * `severity_level_warning` - The value of severity property that maps to Application Insights' warning severity level (default `warning`).
38
+ * `severity_level_error` - The value of severity property that maps to Application Insights' error severity level (default `error`).
39
+ * `severity_level_critical` - The value of severity property that maps to Application Insights' critical severity level (default `critical`).
40
+ * `context_tag_sources` - The dictionary that instructs the Application Insights plugin to set Application Insights context tags using record properties. In this dictionary keys are Application Insights context tags to set, and values are names of properties to use as source of data. For example:
41
+ ```
42
+ context_tag_sources {
43
+ "ai.cloud.role": "kubernetes_container_name",
44
+ "ai.cloud.roleInstance": "kubernetes_pod_name"
45
+ }
46
+ ```
47
+ Here is the list of all [context tag keys](https://github.com/Microsoft/ApplicationInsights-dotnet/blob/develop/Schema/PublicSchema/ContextTagKeys.bond)
48
+
49
+ ## Standard Schema
50
+
51
+ The standard schema for Application Insights telemetry is defined [here](https://github.com/Microsoft/ApplicationInsights-Home/tree/master/EndpointSpecs/Schemas/Bond).
52
+
53
+ Below is an example of a Request telemetry in standard schema format. `name`, `time`, `data`, `data.baseType` and `data.baseData` are required properties. Different telemetry types will have different properties associated with the `baseData` object.
54
+
55
+ ```
56
+ {
57
+ "name": "Microsoft.ApplicationInsights.Request",
58
+ "time": "2018-02-28T00:24:00.5676240Z",
59
+ "tags":{
60
+ "ai.cloud.role": "webfront",
61
+ "ai.cloud.roleInstance":"85a1e424491d07b6c1ed032f"
62
+ },
63
+ "data": {
64
+ "baseType": "RequestData",
65
+ "baseData": {
66
+ "ver":2,
67
+ "id":"|85a1e424-491d07b6c1ed032f.",
68
+ "name":"PUT Flights/StartNewFlightAsync",
69
+ "duration":"00:00:01.0782934",
70
+ "success":true,
71
+ "responseCode":"204",
72
+ "url":"http://localhost:5023/api/flights
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## Development
78
+ ### Build Gem
79
+ Run ```gem build fluent-plugin-application-insights.gemspec```.
80
+
81
+ ### Run Test
82
+ Make sure you have bundler installed, you can install it by ```sudo gem install bundler```. And run ```bundler install``` once to install all dependencies.
83
+
84
+ Run ```rake test```.
85
+
86
+ ### Release
87
+ If you are the current maintainer of this plugin:
88
+ 1. Ensure all tests passed
89
+ 2. Bump up version in ```fluent-plugin-application-insights.gemspec```
90
+ 3. Build the gem, install it locally and test it
91
+ 4. Create a PR with whatever changes needed before releasing, e.g., version bump up, documentation
92
+ 5. Tag and push: ```git tag vx.xx.xx; git push --tags```
93
+ 6. Create a github release with the pushed tag
94
+ 7. Push to rubygems.org: ```gem push fluent-plugin-application-insights-<version>.gem```
95
+
96
+ ## Contributing
97
+
98
+ This project welcomes contributions and suggestions. Most contributions require you to
99
+ agree to a Contributor License Agreement (CLA) declaring that you have the right to,
100
+ and actually do, grant us the rights to use your contribution. For details, visit
101
+ https://cla.microsoft.com.
102
+
103
+ When you submit a pull request, a CLA-bot will automatically determine whether you need
104
+ to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the
105
+ instructions provided by the bot. You will only need to do this once across all repositories using our CLA.
106
+
107
+ This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
108
+ For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
109
+ or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require "bundler"
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs.push("lib", "test")
8
+ t.test_files = FileList["test/**/test_*.rb"]
9
+ t.verbose = true
10
+ t.warning = true
11
+ end
12
+
13
+ task default: [:test]
@@ -0,0 +1,34 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "fluent-plugin-application-insights"
6
+ spec.version = "0.1.0"
7
+ spec.authors = ["Microsoft Corporation"]
8
+ spec.email = ["ctdiagcore@microsoft.com"]
9
+
10
+ spec.summary = "This is the fluentd output plugin for Azure Application Insights."
11
+ spec.description = <<-eos
12
+ Fluentd output plugin for Azure Application Insights.
13
+ Application Insights is an extensible Application Performance Management (APM) service for web developers on multiple platforms.
14
+ Use it to monitor your live web application. It will automatically detect performance anomalies. It includes powerful analytics
15
+ tools to help you diagnose issues and to understand what users actually do with your app.
16
+ It's designed to help you continuously improve performance and usability.
17
+ eos
18
+ spec.homepage = "https://github.com/Microsoft/fluent-plugin-application-insights"
19
+ spec.license = "MIT"
20
+
21
+ test_files, files = `git ls-files -z`.split("\x0").partition do |f|
22
+ f.match(%r{^(test|spec|features)/})
23
+ end
24
+ spec.files = files.push('lib/fluent/plugin/out_application_insights.rb')
25
+ spec.executables = files.grep(%r{^bin/}) { |f| File.basename(f) }
26
+ spec.test_files = test_files
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.14"
30
+ spec.add_development_dependency "rake", "~> 12.0"
31
+ spec.add_development_dependency "test-unit", "~> 3.0"
32
+ spec.add_runtime_dependency "fluentd", [">= 0.14.10", "< 2"]
33
+ spec.add_runtime_dependency "application_insights", "~> 0.5.5"
34
+ end
@@ -0,0 +1,201 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+
4
+ require 'json'
5
+ require 'fluent/plugin/output'
6
+ require 'application_insights'
7
+
8
+ module Fluent::Plugin
9
+ class ApplicationInsightsOutput < Output
10
+ Fluent::Plugin.register_output("application_insights", self)
11
+
12
+ attr_accessor :tc
13
+
14
+ # The Application Insights instrumentation key
15
+ config_param :instrumentation_key, :string
16
+ # The batch size to send data to Application Insights service.
17
+ config_param :send_buffer_size, :integer, default: 1000
18
+ # The parameter indication whether the record is in standard schema. i.e., the format that is recognized by Application Insights backend.
19
+ config_param :standard_schema, :bool, default: false
20
+ # The property name for the message. It will be ignored if the record is in standard schema.
21
+ config_param :message_property, :string, default: 'message'
22
+ # The property name for the timestamp. It will be ignored if the record is in standard schema.
23
+ config_param :time_property, :string, default: nil
24
+ # The property name for severity level. It will be ignored if the record is in standard schema.
25
+ config_param :severity_property, :string, default: 'severity'
26
+ # The value of severity property that maps to Application Insights' verbose severity level.
27
+ config_param :severity_level_verbose, :string, default: 'verbose'
28
+ # The value of severity property that maps to Application Insights' information severity level.
29
+ config_param :severity_level_information, :string, default: 'information'
30
+ # The value of severity property that maps to Application Insights' warning severity level.
31
+ config_param :severity_level_warning, :string, default: 'warning'
32
+ # The value of severity property that maps to Application Insights' error severity level.
33
+ config_param :severity_level_error, :string, default: 'error'
34
+ # The value of severity property that maps to Application Insights' critical severity level.
35
+ config_param :severity_level_critical, :string, default: 'critical'
36
+ # The dictionary that instructs the Application Insights plugin to set Application Insights context tags using record properties.
37
+ # In this dictionary keys are Application Insights context tags to set, and values are names of properties to use as source of data.
38
+ config_param :context_tag_sources, :hash, default: {}, value_type: :string
39
+
40
+ TELEMETRY_TYPES = ["RequestData", "RemoteDependencyData", "MessageData", "ExceptionData", "EventData", "MetricData", "PageViewData", "AvailabilityData"]
41
+
42
+ def configure(conf)
43
+ super
44
+
45
+ @severity_level_mapping = {}
46
+ @severity_level_mapping[@severity_level_verbose.downcase] = Channel::Contracts::SeverityLevel::VERBOSE
47
+ @severity_level_mapping[@severity_level_information.downcase] = Channel::Contracts::SeverityLevel::INFORMATION
48
+ @severity_level_mapping[@severity_level_warning.downcase] = Channel::Contracts::SeverityLevel::WARNING
49
+ @severity_level_mapping[@severity_level_error.downcase] = Channel::Contracts::SeverityLevel::ERROR
50
+ @severity_level_mapping[@severity_level_critical.downcase] = Channel::Contracts::SeverityLevel::CRITICAL
51
+
52
+ context_tag_keys = []
53
+ context_tag_keys.concat Channel::Contracts::Application.json_mappings.values
54
+ context_tag_keys.concat Channel::Contracts::Cloud.json_mappings.values
55
+ context_tag_keys.concat Channel::Contracts::Device.json_mappings.values
56
+ context_tag_keys.concat Channel::Contracts::Internal.json_mappings.values
57
+ context_tag_keys.concat Channel::Contracts::Location.json_mappings.values
58
+ context_tag_keys.concat Channel::Contracts::Operation.json_mappings.values
59
+ context_tag_keys.concat Channel::Contracts::Session.json_mappings.values
60
+ context_tag_keys.concat Channel::Contracts::User.json_mappings.values
61
+
62
+ context_tag_sources.keys.each do |tag|
63
+ raise ArgumentError.new("Context tag '#{tag}' is invalid!") unless context_tag_keys.include?(tag)
64
+ end
65
+ end
66
+
67
+ def start
68
+ super
69
+
70
+ sender = Channel::AsynchronousSender.new
71
+ queue = Channel::AsynchronousQueue.new sender
72
+ channel = Channel::TelemetryChannel.new nil, queue
73
+ @tc = TelemetryClient.new @instrumentation_key, channel
74
+ @tc.channel.queue.max_queue_length = @send_buffer_size
75
+ @tc.channel.sender.send_buffer_size = @send_buffer_size
76
+ end
77
+
78
+ def shutdown
79
+ super
80
+
81
+ # Draining the events in the queue.
82
+ # We need to make sure the work thread has finished. Otherwise, it's possible that the queue is empty, but the http request to send the data is not finished.
83
+ # However, a drawback of waiting for the work thread to finish is that even if the events have been drained, it will still poll the queue for some time (default is 3 seconds, set by sender.send_time).
84
+ # This can be improved if the SDK exposes another variable indicating whether the work thread is sending data or just polling the queue.
85
+ while !@tc.channel.queue.empty? || @tc.channel.sender.work_thread != nil
86
+ # It's possible that the work thread has already exited but there are still items in the queue.
87
+ # https://github.com/Microsoft/ApplicationInsights-Ruby/blob/master/lib/application_insights/channel/asynchronous_sender.rb#L115
88
+ # Trigger flush to make the work thread working again in this case.
89
+ if @tc.channel.sender.work_thread == nil && !@tc.channel.queue.empty?
90
+ @tc.flush
91
+ end
92
+
93
+ sleep(1)
94
+ end
95
+ end
96
+
97
+ def process(tag, es)
98
+ es.each do |time, record|
99
+ # Convert the fluentd EventTime object to ruby Time object
100
+ time_ruby = Time.at(time.sec, time.nsec / 1000).utc
101
+ if @standard_schema
102
+ process_standard_schema_log record, time_ruby
103
+ else
104
+ process_non_standard_schema_log record, time_ruby
105
+ end
106
+ end
107
+ end
108
+
109
+ def process_standard_schema_log(record, time)
110
+ if record["name"] && record["data"] && record["data"].is_a?(Hash) && record["data"]["baseType"] && record["data"]["baseData"]
111
+ base_type = record["data"]["baseType"]
112
+
113
+ if TELEMETRY_TYPES.include? base_type
114
+ # If the record is processed by json parser plugin, e.g., in_http use it by default, the time property will be removed. Add it back in this case.
115
+ record["time"] ||= time.iso8601(7)
116
+ record["iKey"] = @instrumentation_key
117
+ set_context_standard_schema record
118
+
119
+ envelope = Channel::Contracts::Envelope.new
120
+ Channel::Contracts::Envelope.json_mappings.each do |attr, name|
121
+ envelope.send(:"#{attr}=", record[name]) if record[name]
122
+ end
123
+
124
+ @tc.channel.queue.push(envelope)
125
+ else
126
+ log.warn "Unknown telemetry type #{base_type}. Event will be treated as as non standard schema event."
127
+ process_non_standard_schema_log record, time
128
+ end
129
+ else
130
+ log.warn "The event does not meet the standard schema of Application Insights output. Missing name, data, baseType or baseData property. Event will be treated as as non standard schema event."
131
+ process_non_standard_schema_log record, time
132
+ end
133
+ end
134
+
135
+ def set_context_standard_schema(record)
136
+ return if @context_tag_sources.length == 0
137
+
138
+ record["tags"] = record["tags"] || {}
139
+ @context_tag_sources.each do |context_tag, source_property|
140
+ context_value = record.delete source_property
141
+ record["tags"][context_tag] = context_value if context_value
142
+ end
143
+ end
144
+
145
+ def process_non_standard_schema_log(record, time)
146
+ time = record.delete(@time_property) || time
147
+ context = get_context_non_standard_schema(record)
148
+ message = record.delete @message_property
149
+ severity_level_value = record.delete @severity_property
150
+ severity_level = severity_level_value ? @severity_level_mapping[severity_level_value.to_s.downcase] : nil
151
+ props = stringify_properties(record)
152
+
153
+ data = Channel::Contracts::MessageData.new(
154
+ :message => message || 'Null',
155
+ :severity_level => severity_level || Channel::Contracts::SeverityLevel::INFORMATION,
156
+ :properties => props || {}
157
+ )
158
+
159
+ @tc.channel.write(data, context, time)
160
+ end
161
+
162
+ def get_context_non_standard_schema(record)
163
+ context = Channel::TelemetryContext.new
164
+ context.instrumentation_key = @instrumentation_key
165
+ return context if @context_tag_sources.length == 0
166
+
167
+ @context_tag_sources.each do |context_tag, source_property|
168
+ if record[source_property]
169
+ set_context_tag context, context_tag, record[source_property]
170
+ end
171
+ end
172
+
173
+ return context
174
+ end
175
+
176
+ def set_context_tag(context, tag_name, tag_value)
177
+ context_set = [context.application, context.cloud, context.device, context.location, context.operation, context.session, context.user]
178
+ context_set.each do |c|
179
+ c.class.json_mappings.each do |attr, name|
180
+ if (name == tag_name)
181
+ c.send(:"#{attr}=", tag_value)
182
+ return
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ def stringify_properties(record)
189
+ # If the property value is a json object or array, e.g., {"prop": {"inner_prop": value}}, it needs to be serialized.
190
+ # Otherwise, the property will become {"prop": "[object Object]"} in the final telemetry.
191
+ # The stringified property can be queried as described here: https://docs.loganalytics.io/docs/Language-Reference/Scalar-functions/parse_json()
192
+ record.each do |key, value|
193
+ if value.is_a?(Hash) || value.is_a?(Array)
194
+ record[key] = JSON.generate(value)
195
+ end
196
+ end
197
+ record
198
+ end
199
+
200
+ end
201
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.expand_path("../../", __FILE__))
2
+ require "test-unit"
3
+ require "fluent/test"
4
+ require "fluent/test/driver/output"
5
+ require "fluent/test/helpers"
6
+
7
+ Test::Unit::TestCase.include(Fluent::Test::Helpers)
8
+ Test::Unit::TestCase.extend(Fluent::Test::Helpers)
@@ -0,0 +1,37 @@
1
+ require "application_insights"
2
+
3
+ class MockSender < Channel::SenderBase
4
+ def initialize
5
+ end
6
+
7
+ def start
8
+ end
9
+ end
10
+
11
+ class MockQueue
12
+ attr_accessor :sender
13
+ attr_accessor :queue
14
+
15
+ def initialize(sender)
16
+ @sender = sender
17
+ @queue = []
18
+ end
19
+
20
+ def push(item)
21
+ return unless item
22
+ @queue << item
23
+ end
24
+
25
+ def empty?
26
+ @queue.empty?
27
+ end
28
+
29
+ def [](index)
30
+ @queue[index]
31
+ end
32
+
33
+ def flush
34
+ @queue = []
35
+ end
36
+ end
37
+
@@ -0,0 +1,427 @@
1
+ require "helper"
2
+ require "fluent/plugin/out_application_insights.rb"
3
+ require_relative "mock_client.rb"
4
+
5
+ class ApplicationInsightsOutputTest < Test::Unit::TestCase
6
+ setup do
7
+ Fluent::Test.setup
8
+ end
9
+
10
+ CONFIG = %[
11
+ instrumentation_key ikey
12
+ ]
13
+
14
+ def create_driver(conf = CONFIG)
15
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::ApplicationInsightsOutput).configure(conf)
16
+ driver.instance.start
17
+
18
+ sender = MockSender.new
19
+ queue = MockQueue.new sender
20
+ channel = ApplicationInsights::Channel::TelemetryChannel.new nil, queue
21
+ driver.instance.tc = ApplicationInsights::TelemetryClient.new "iKey", channel
22
+
23
+ return driver
24
+ end
25
+
26
+ sub_test_case 'configure' do
27
+ test 'invalid context tag key' do
28
+ config = %[
29
+ instrumentation_key ikey
30
+ context_tag_sources invalid_tag_name:kubernetes_container_name
31
+ ]
32
+ assert_raise ArgumentError.new("Context tag 'invalid_tag_name' is invalid!") do
33
+ create_driver config
34
+ end
35
+ end
36
+ end
37
+
38
+ sub_test_case 'process standard schema event' do
39
+ setup do
40
+ config = %[
41
+ instrumentation_key ikey
42
+ standard_schema true
43
+ ]
44
+
45
+ @d = create_driver config
46
+ end
47
+
48
+ test 'ikey and timestamps are added if empty' do
49
+ time = event_time("2011-01-02 13:14:15 UTC")
50
+ @d.run(default_tag: 'test', shutdown: false) do
51
+ @d.feed(time, {"name" => "telemetry name", "data" => { "baseType" => "RequestData", "baseData" => {} }})
52
+ end
53
+
54
+ envelope = @d.instance.tc.channel.queue[0]
55
+ assert_equal "ikey", envelope.i_key
56
+ assert_equal "2011-01-02T13:14:15.0000000Z", envelope.time
57
+ end
58
+
59
+ test 'event missing required properties is treated as non standard schema' do
60
+ time = event_time("2011-01-02 13:14:15 UTC")
61
+ @d.run(default_tag: 'test', shutdown: false) do
62
+ @d.feed(time, {"data" => {"baseType" => "RequestData", "baseData" => "data"}})
63
+ @d.feed(time, {"name" => "telemetry name"})
64
+ @d.feed(time, {"name" => "telemetry name", "data" => 2})
65
+ @d.feed(time, {"name" => "telemetry name", "data" => {}})
66
+ @d.feed(time, {"name" => "telemetry name", "data" => {"someprop" => "value"}})
67
+ @d.feed(time, {"name" => "telemetry name", "data" => {"baseType" => "type"}})
68
+ @d.feed(time, {"name" => "telemetry name", "data" => {"baseData" => "data"}})
69
+ end
70
+
71
+ logs = @d.instance.log.out.logs
72
+ assert_equal 7, logs.length
73
+ assert_true logs.all?{ |log| log.include?("The event does not meet the standard schema of Application Insights output. Missing name, data, baseType or baseData property.") }
74
+ end
75
+
76
+ test 'event with unknown data type is treated as non standard schema' do
77
+ time = event_time("2011-01-02 13:14:15 UTC")
78
+ @d.run(default_tag: 'test', shutdown: false) do
79
+ @d.feed(time, {"name" => "telemetry name", "data" => {"baseType" => "unknown", "baseData" => {}}})
80
+ end
81
+
82
+ logs = @d.instance.log.out.logs
83
+ assert_true logs.all?{ |log| log.include?("Unknown telemetry type unknown") }
84
+ end
85
+
86
+ end
87
+
88
+ sub_test_case 'set context on standard schema event' do
89
+ test 'context tag sources is empty' do
90
+ config = %[
91
+ instrumentation_key ikey
92
+ standard_schema true
93
+ context_tag_sources {}
94
+ ]
95
+ d = create_driver config
96
+
97
+ time = event_time("2011-01-02 13:14:15 UTC")
98
+ d.run(default_tag: 'test', shutdown: false) do
99
+ d.feed(time, {
100
+ "name" => "telemetry name",
101
+ "data" => { "baseType" => "RequestData", "baseData" => {} },
102
+ "kubernetes_container_name" => "frontend"
103
+ })
104
+ end
105
+
106
+ envelope = d.instance.tc.channel.queue[0]
107
+ assert_true envelope.tags.length == 0
108
+ end
109
+
110
+ test 'context tag sources does not exist on record' do
111
+ config = %[
112
+ instrumentation_key ikey
113
+ standard_schema true
114
+ context_tag_sources ai.cloud.role:kubernetes_container_name
115
+ ]
116
+ d = create_driver config
117
+
118
+ time = event_time("2011-01-02 13:14:15 UTC")
119
+ d.run(default_tag: 'test', shutdown: false) do
120
+ d.feed(time, {
121
+ "name" => "telemetry name",
122
+ "data" => { "baseType" => "RequestData", "baseData" => {} }
123
+ })
124
+ end
125
+
126
+ envelope = d.instance.tc.channel.queue[0]
127
+ assert_not_nil envelope.tags
128
+ assert_nil envelope.tags["ai.cloud.role"]
129
+ end
130
+
131
+ test 'context is updated according to context tag keys' do
132
+ config = %[
133
+ instrumentation_key ikey
134
+ standard_schema true
135
+ context_tag_sources ai.cloud.role:kubernetes_container_name
136
+ ]
137
+ d = create_driver config
138
+
139
+ time = event_time("2011-01-02 13:14:15 UTC")
140
+ d.run(default_tag: 'test', shutdown: false) do
141
+ d.feed(time, {
142
+ "name" => "telemetry name",
143
+ "data" => { "baseType" => "RequestData", "baseData" => {} },
144
+ "kubernetes_container_name" => "frontend",
145
+ "other_prop" => "prop value"
146
+ })
147
+ end
148
+
149
+ envelope = d.instance.tc.channel.queue[0]
150
+ assert_not_nil envelope.tags
151
+ assert_equal "frontend", envelope.tags["ai.cloud.role"]
152
+ # Extra property that is not part of Envelope (kubernetes_container_name, other_prop) is ignored
153
+ assert_nil envelope.data["baseData"]["properties"]
154
+ end
155
+
156
+ test 'multiple context tag keys' do
157
+ config = %[
158
+ instrumentation_key ikey
159
+ standard_schema true
160
+ context_tag_sources {
161
+ "ai.cloud.role": "kubernetes_container_name",
162
+ "ai.cloud.roleInstance": "kubernetes_container_id"
163
+ }
164
+ ]
165
+ d = create_driver config
166
+
167
+ time = event_time("2011-01-02 13:14:15 UTC")
168
+ d.run(default_tag: 'test', shutdown: false) do
169
+ d.feed(time, {
170
+ "name" => "telemetry name",
171
+ "data" => { "baseType" => "RequestData", "baseData" => {} },
172
+ "kubernetes_container_name" => "frontend",
173
+ "kubernetes_container_id" => "c42c557e1615511dd3a3cb1d6e8f14984464bb0f"
174
+ })
175
+ end
176
+
177
+ envelope = d.instance.tc.channel.queue[0]
178
+ assert_not_nil envelope.tags
179
+ assert_equal "frontend", envelope.tags["ai.cloud.role"]
180
+ assert_equal "c42c557e1615511dd3a3cb1d6e8f14984464bb0f", envelope.tags["ai.cloud.roleInstance"]
181
+ end
182
+ end
183
+
184
+ sub_test_case 'process non standard schema event' do
185
+ test 'empty message' do
186
+ d = create_driver
187
+
188
+ time = event_time("2011-01-02 13:14:15 UTC")
189
+ d.run(default_tag: 'test', shutdown: false) do
190
+ d.feed(time, {"prop" => "value"})
191
+ end
192
+
193
+ assert_equal 1, d.instance.tc.channel.queue.queue.length
194
+ envelope = d.instance.tc.channel.queue[0]
195
+ assert_equal "Null", envelope.data.base_data.message
196
+ assert_equal envelope.data.base_data.properties, {"prop" => "value"}
197
+ end
198
+
199
+ test 'custom timestamp take precedence over fluentd timestamp' do
200
+ config = %[
201
+ instrumentation_key ikey
202
+ time_property time
203
+ ]
204
+ d = create_driver config
205
+
206
+ time = event_time("2011-01-02 13:14:15 UTC")
207
+ d.run(default_tag: 'test', shutdown: false) do
208
+ d.feed(time, {"time" => "2010-10-01"})
209
+ end
210
+
211
+ envelope = d.instance.tc.channel.queue[0]
212
+ assert_equal "2010-10-01", envelope.time
213
+ end
214
+
215
+ test 'custom timestamp format is not ensured to be valid' do
216
+ config = %[
217
+ instrumentation_key ikey
218
+ time_property time
219
+ ]
220
+ d = create_driver config
221
+
222
+ time = event_time("2011-01-02 13:14:15 UTC")
223
+ d.run(default_tag: 'test', shutdown: false) do
224
+ d.feed(time, {"time" => "custom time"})
225
+ end
226
+
227
+ envelope = d.instance.tc.channel.queue[0]
228
+ assert_equal "custom time", envelope.time
229
+ end
230
+
231
+ test 'timestamp is in iso8601 format' do
232
+ d = create_driver
233
+
234
+ time = event_time("2011-01-02 13:14:15 UTC")
235
+ d.run(default_tag: 'test', shutdown: false) do
236
+ d.feed(time, {"message" => "log message"})
237
+ end
238
+
239
+ envelope = d.instance.tc.channel.queue[0]
240
+ assert_equal "2011-01-02T13:14:15.0000000Z", envelope.time
241
+ end
242
+
243
+ test 'custom message property' do
244
+ config = %[
245
+ instrumentation_key ikey
246
+ message_property custom_message_property
247
+ ]
248
+ d = create_driver config
249
+
250
+ time = event_time("2011-01-02 13:14:15 UTC")
251
+ d.run(default_tag: 'test', shutdown: false) do
252
+ d.feed(time, {"custom_message_property" => "custom message", "message" => "my message"})
253
+ end
254
+
255
+ envelope = d.instance.tc.channel.queue[0]
256
+ assert_equal "custom message", envelope.data.base_data.message
257
+ end
258
+
259
+ test 'custom severity level mapping' do
260
+ config = %[
261
+ instrumentation_key ikey
262
+ severity_property custom_severity_property
263
+ severity_level_error 100
264
+ ]
265
+ d = create_driver config
266
+
267
+ time = event_time("2011-01-02 13:14:15 UTC")
268
+ d.run(default_tag: 'test', shutdown: false) do
269
+ d.feed(time, {"custom_severity_property" => 100, "message" => "my message"})
270
+ end
271
+
272
+ envelope = d.instance.tc.channel.queue[0]
273
+ assert_equal ApplicationInsights::Channel::Contracts::SeverityLevel::ERROR, envelope.data.base_data.severity_level
274
+ end
275
+
276
+ test 'properties are stringified' do
277
+ d = create_driver
278
+
279
+ time = event_time("2011-01-02 13:14:15 UTC")
280
+ d.run(default_tag: 'test', shutdown: false) do
281
+ d.feed(time, {"prop" => {"inner_prop1" => "value1", "inner_prop2" => "value2"}})
282
+ end
283
+
284
+ envelope = d.instance.tc.channel.queue[0]
285
+ assert_equal 1, envelope.data.base_data.properties.length
286
+ assert_equal "{\"inner_prop1\":\"value1\",\"inner_prop2\":\"value2\"}", envelope.data.base_data.properties["prop"]
287
+ end
288
+ end
289
+
290
+ sub_test_case 'set context on non standard schema event' do
291
+ test 'context tag sources is empty' do
292
+ config = %[
293
+ instrumentation_key ikey
294
+ context_tag_sources {}
295
+ ]
296
+ d = create_driver config
297
+
298
+ time = event_time("2011-01-02 13:14:15 UTC")
299
+ d.run(default_tag: 'test', shutdown: false) do
300
+ d.feed(time, {
301
+ "message" => "my message",
302
+ "kubernetes_container_name" => "frontend"
303
+ })
304
+ end
305
+
306
+ envelope = d.instance.tc.channel.queue[0]
307
+
308
+ # The only tag is "ai.internal.sdkVersion", which is irrelevant to contaxt_tag_sources
309
+ assert_true envelope.tags.length == 1
310
+ assert_equal "ai.internal.sdkVersion", envelope.tags.keys[0]
311
+ assert_equal "ikey", envelope.i_key
312
+ end
313
+
314
+ test 'context tag sources does not exist on record' do
315
+ config = %[
316
+ instrumentation_key ikey
317
+ context_tag_sources ai.cloud.role:kubernetes_container_name
318
+ ]
319
+ d = create_driver config
320
+
321
+ time = event_time("2011-01-02 13:14:15 UTC")
322
+ d.run(default_tag: 'test', shutdown: false) do
323
+ d.feed(time, {
324
+ "message" => "my message"
325
+ })
326
+ end
327
+
328
+ envelope = d.instance.tc.channel.queue[0]
329
+ assert_not_nil envelope.tags
330
+ assert_nil envelope.tags["ai.cloud.role"]
331
+ end
332
+
333
+ test 'context is updated according to context tag keys' do
334
+ config = %[
335
+ instrumentation_key ikey
336
+ context_tag_sources ai.cloud.role:kubernetes_container_name
337
+ ]
338
+ d = create_driver config
339
+
340
+ time = event_time("2011-01-02 13:14:15 UTC")
341
+ d.run(default_tag: 'test', shutdown: false) do
342
+ d.feed(time, {
343
+ "message" => "my message",
344
+ "kubernetes_container_name" => "frontend",
345
+ "other_prop" => "prop value"
346
+ })
347
+ end
348
+
349
+ envelope = d.instance.tc.channel.queue[0]
350
+ assert_not_nil envelope.tags
351
+ assert_equal "frontend", envelope.tags["ai.cloud.role"]
352
+ assert_not_nil envelope.data.base_data.properties["kubernetes_container_name"]
353
+ assert_not_nil envelope.data.base_data.properties["other_prop"]
354
+ end
355
+
356
+ test 'multiple context tag keys' do
357
+ config = %[
358
+ instrumentation_key ikey
359
+ context_tag_sources {
360
+ "ai.cloud.role": "kubernetes_container_name",
361
+ "ai.cloud.roleInstance": "kubernetes_container_id"
362
+ }
363
+ ]
364
+ d = create_driver config
365
+
366
+ time = event_time("2011-01-02 13:14:15 UTC")
367
+ d.run(default_tag: 'test', shutdown: false) do
368
+ d.feed(time, {
369
+ "message" => "my message",
370
+ "kubernetes_container_name" => "frontend",
371
+ "kubernetes_container_id" => "c42c557e1615511dd3a3cb1d6e8f14984464bb0f"
372
+ })
373
+ end
374
+
375
+ envelope = d.instance.tc.channel.queue[0]
376
+ assert_not_nil envelope.tags
377
+ assert_true envelope.tags.length == 3
378
+ assert_equal "frontend", envelope.tags["ai.cloud.role"]
379
+ assert_equal "c42c557e1615511dd3a3cb1d6e8f14984464bb0f", envelope.tags["ai.cloud.roleInstance"]
380
+ end
381
+ end
382
+
383
+ sub_test_case 'stringify_properties' do
384
+ test 'simple data type are not stringified' do
385
+ plugin = create_driver.instance
386
+
387
+ record = {prop1: 1, prop2: true, prop3: "value"}
388
+ actual = plugin.stringify_properties(record)
389
+ expected = {prop1: 1, prop2: true, prop3: "value"}
390
+ assert_equal expected, actual
391
+ end
392
+
393
+ test 'json and array property values are stringified' do
394
+ plugin = create_driver.instance
395
+
396
+ record = {prop1: 1, prop2: [1, 2, 3], prop3: {inner_prop: "value"}}
397
+ actual = plugin.stringify_properties(record)
398
+ expected = {prop1: 1, prop2: "[1,2,3]", prop3: "{\"inner_prop\":\"value\"}"}
399
+ assert_equal expected, actual
400
+ end
401
+
402
+ test 'stringify complicated property value' do
403
+ plugin = create_driver.instance
404
+
405
+ record = {
406
+ arr: [1, [2, [3, {inner: "value"}]]],
407
+ obj: {
408
+ arr: [1, {inarr: "inarr"}],
409
+ inobj: {
410
+ ininobj: {
411
+ prop: "value"
412
+ },
413
+ num: 3.14
414
+ }
415
+ }
416
+ }
417
+
418
+ actual = plugin.stringify_properties(record)
419
+ expected = {
420
+ :arr=> "[1,[2,[3,{\"inner\":\"value\"}]]]",
421
+ :obj=> "{\"arr\":[1,{\"inarr\":\"inarr\"}],\"inobj\":{\"ininobj\":{\"prop\":\"value\"},\"num\":3.14}}"
422
+ }
423
+ assert_equal expected, actual
424
+ end
425
+ end
426
+
427
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-application-insights
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Microsoft Corporation
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-05-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '12.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '12.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: test-unit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: fluentd
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.14.10
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '2'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 0.14.10
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '2'
75
+ - !ruby/object:Gem::Dependency
76
+ name: application_insights
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.5.5
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 0.5.5
89
+ description: |2
90
+ Fluentd output plugin for Azure Application Insights.
91
+ Application Insights is an extensible Application Performance Management (APM) service for web developers on multiple platforms.
92
+ Use it to monitor your live web application. It will automatically detect performance anomalies. It includes powerful analytics
93
+ tools to help you diagnose issues and to understand what users actually do with your app.
94
+ It's designed to help you continuously improve performance and usability.
95
+ email:
96
+ - ctdiagcore@microsoft.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - ".gitignore"
102
+ - ".travis.yml"
103
+ - Gemfile
104
+ - LICENSE
105
+ - README.md
106
+ - Rakefile
107
+ - fluent-plugin-application-insights.gemspec
108
+ - lib/fluent/plugin/out_application_insights.rb
109
+ - test/helper.rb
110
+ - test/plugin/mock_client.rb
111
+ - test/plugin/test_out_application_insights.rb
112
+ homepage: https://github.com/Microsoft/fluent-plugin-application-insights
113
+ licenses:
114
+ - MIT
115
+ metadata: {}
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 2.5.2
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: This is the fluentd output plugin for Azure Application Insights.
136
+ test_files:
137
+ - test/helper.rb
138
+ - test/plugin/mock_client.rb
139
+ - test/plugin/test_out_application_insights.rb