fluent-plugin-azure-logs-ingestion 0.1.0

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.
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require_relative 'support/fake_azure_server'
5
+ require_relative 'support/helpers'
6
+ require 'fluent/test/driver/output'
7
+ require 'fluent/plugin/out_azure_logs_ingestion'
8
+ require 'stringio'
9
+ require 'zlib'
10
+
11
+ class AzureLogsIngestionWriteTest < Test::Unit::TestCase
12
+ def create_driver(conf)
13
+ Fluent::Test::Driver::Output.new(Fluent::Plugin::AzureLogsIngestionOutput).configure(conf)
14
+ end
15
+
16
+ test 'writes a chunk with service principal auth' do
17
+ server = FakeAzureServer.new do |request|
18
+ case request.path
19
+ when %r{/tenant/oauth2/v2.0/token}
20
+ [200, { 'Content-Type' => 'application/json' }, { access_token: 'token-1', expires_in: '3600' }.to_json]
21
+ when %r{/dataCollectionRules/.+/streams/Custom-MyTable\?api-version=2023-01-01}
22
+ [200, { 'Content-Type' => 'application/json' }, '{}']
23
+ else
24
+ [404, {}, 'not found']
25
+ end
26
+ end.start
27
+
28
+ driver = create_driver(<<~CONFIG)
29
+ endpoint #{server.url}
30
+ authority_host #{server.url}
31
+ dcr_immutable_id dcr-000a00a000a00000a000000aa000a0aa
32
+ stream_name Custom-MyTable
33
+ tenant_id tenant
34
+ client_id client
35
+ client_secret secret
36
+ <buffer>
37
+ @type memory
38
+ </buffer>
39
+ CONFIG
40
+
41
+ driver.instance.start
42
+ driver.instance.write(FakeChunk.new([
43
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 0, 0)), { 'message' => 'hello' }]
44
+ ]))
45
+
46
+ ingestion_request = server.requests.last
47
+ assert_equal 'Bearer token-1', ingestion_request.headers['authorization']
48
+ assert_match(/"message":"hello"/, ingestion_request.body)
49
+ assert_not_match(/"TimeGenerated":/, ingestion_request.body)
50
+ ensure
51
+ driver&.instance&.shutdown
52
+ driver&.instance&.close
53
+ server&.stop
54
+ end
55
+
56
+ test 'raises unrecoverable error for 400 response' do
57
+ server = FakeAzureServer.new do |request|
58
+ case request.path
59
+ when %r{/tenant/oauth2/v2.0/token}
60
+ [200, { 'Content-Type' => 'application/json' }, { access_token: 'token-1', expires_in: '3600' }.to_json]
61
+ else
62
+ [400, { 'Content-Type' => 'application/json' }, '{"error":"bad request"}']
63
+ end
64
+ end.start
65
+
66
+ driver = create_driver(<<~CONFIG)
67
+ endpoint #{server.url}
68
+ authority_host #{server.url}
69
+ dcr_immutable_id dcr-000a00a000a00000a000000aa000a0aa
70
+ stream_name Custom-MyTable
71
+ tenant_id tenant
72
+ client_id client
73
+ client_secret secret
74
+ <buffer>
75
+ @type memory
76
+ </buffer>
77
+ CONFIG
78
+
79
+ driver.instance.start
80
+ error = assert_raise(Fluent::UnrecoverableError) do
81
+ driver.instance.write(FakeChunk.new([
82
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 0, 0)), { 'message' => 'hello' }]
83
+ ]))
84
+ end
85
+
86
+ assert_match(/400/, error.message)
87
+ ensure
88
+ driver&.instance&.shutdown
89
+ driver&.instance&.close
90
+ server&.stop
91
+ end
92
+
93
+ test 'sends gzip payload when enabled' do
94
+ server = FakeAzureServer.new do |request|
95
+ case request.path
96
+ when %r{/tenant/oauth2/v2.0/token}
97
+ [200, { 'Content-Type' => 'application/json' }, { access_token: 'token-1', expires_in: '3600' }.to_json]
98
+ when %r{/dataCollectionRules/.+/streams/Custom-MyTable\?api-version=2023-01-01}
99
+ [200, { 'Content-Type' => 'application/json' }, '{}']
100
+ else
101
+ [404, {}, 'not found']
102
+ end
103
+ end.start
104
+
105
+ driver = create_driver(<<~CONFIG)
106
+ endpoint #{server.url}
107
+ authority_host #{server.url}
108
+ dcr_immutable_id dcr-000a00a000a00000a000000aa000a0aa
109
+ stream_name Custom-MyTable
110
+ tenant_id tenant
111
+ client_id client
112
+ client_secret secret
113
+ gzip true
114
+ <buffer>
115
+ @type memory
116
+ </buffer>
117
+ CONFIG
118
+
119
+ driver.instance.start
120
+ driver.instance.write(FakeChunk.new([
121
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 0, 0)), { 'message' => 'hello' }]
122
+ ]))
123
+
124
+ ingestion_request = server.requests.last
125
+ json = Zlib::GzipReader.new(StringIO.new(ingestion_request.body)).read
126
+ assert_equal 'gzip', ingestion_request.headers['content-encoding']
127
+ assert_match(/"message":"hello"/, json)
128
+ ensure
129
+ driver&.instance&.shutdown
130
+ driver&.instance&.close
131
+ server&.stop
132
+ end
133
+
134
+ test 'keeps 429 retryable' do
135
+ server = FakeAzureServer.new do |request|
136
+ case request.path
137
+ when %r{/tenant/oauth2/v2.0/token}
138
+ [200, { 'Content-Type' => 'application/json' }, { access_token: 'token-1', expires_in: '3600' }.to_json]
139
+ else
140
+ [429, { 'Retry-After' => '5' }, '{"error":"throttled"}']
141
+ end
142
+ end.start
143
+
144
+ driver = create_driver(<<~CONFIG)
145
+ endpoint #{server.url}
146
+ authority_host #{server.url}
147
+ dcr_immutable_id dcr-000a00a000a00000a000000aa000a0aa
148
+ stream_name Custom-MyTable
149
+ tenant_id tenant
150
+ client_id client
151
+ client_secret secret
152
+ <buffer>
153
+ @type memory
154
+ </buffer>
155
+ CONFIG
156
+
157
+ driver.instance.start
158
+ error = assert_raise(RuntimeError) do
159
+ driver.instance.write(FakeChunk.new([
160
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 0, 0)), { 'message' => 'hello' }]
161
+ ]))
162
+ end
163
+
164
+ assert_match(/429/, error.message)
165
+ ensure
166
+ driver&.instance&.shutdown
167
+ driver&.instance&.close
168
+ server&.stop
169
+ end
170
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require_relative 'support/helpers'
5
+ require 'fluent/plugin/azure_logs_ingestion/payload_builder'
6
+ require 'zlib'
7
+
8
+ class PayloadBuilderTest < Test::Unit::TestCase
9
+ test 'does not inject TimeGenerated by default' do
10
+ builder = Fluent::Plugin::AzureLogsIngestion::PayloadBuilder.new(gzip: false)
11
+
12
+ result = builder.build(FakeChunk.new([
13
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 0, 0)), { 'message' => 'hello' }]
14
+ ]))
15
+
16
+ body = result.io.read
17
+ assert_match(/"message":"hello"/, body)
18
+ assert_not_match(/"TimeGenerated":/, body)
19
+ ensure
20
+ result&.close!
21
+ end
22
+
23
+ test 'builds payload from event time' do
24
+ builder = Fluent::Plugin::AzureLogsIngestion::PayloadBuilder.new(gzip: false)
25
+
26
+ result = builder.build(FakeChunk.new([
27
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 0, 0)), { 'message' => 'hello' }]
28
+ ]))
29
+
30
+ body = result.io.read
31
+ assert_match(/"message":"hello"/, body)
32
+ assert_equal 1, result.record_count
33
+ assert_equal result.raw_size, result.content_length
34
+ ensure
35
+ result&.close!
36
+ end
37
+
38
+ test 'accepts wide time span' do
39
+ builder = Fluent::Plugin::AzureLogsIngestion::PayloadBuilder.new(gzip: false)
40
+
41
+ result = builder.build(FakeChunk.new([
42
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 0, 0)), {}],
43
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 31, 0)), {}]
44
+ ]))
45
+
46
+ assert_equal 2, result.record_count
47
+ result&.close!
48
+ end
49
+
50
+ test 'builds gzip payload when enabled' do
51
+ builder = Fluent::Plugin::AzureLogsIngestion::PayloadBuilder.new(gzip: true)
52
+
53
+ result = builder.build(FakeChunk.new([
54
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 0, 0)), { 'message' => 'hello' }]
55
+ ]))
56
+
57
+ json = Zlib::GzipReader.new(result.io).read
58
+ assert_equal 'gzip', result.content_encoding
59
+ assert_match(/"message":"hello"/, json)
60
+ assert_not_nil result.gzip_size
61
+ ensure
62
+ result&.io&.rewind
63
+ result&.close!
64
+ end
65
+
66
+ test 'rejects oversized payload' do
67
+ builder = Fluent::Plugin::AzureLogsIngestion::PayloadBuilder.new(gzip: false)
68
+
69
+ error = assert_raise(Fluent::UnrecoverableError) do
70
+ builder.build(FakeChunk.new([
71
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 0, 0)), { 'message' => 'a' * 1_100_000 }]
72
+ ]))
73
+ end
74
+
75
+ assert_match(/payload size/, error.message)
76
+ end
77
+
78
+ test 'accepts non-parseable event time when payload is otherwise valid' do
79
+ builder = Fluent::Plugin::AzureLogsIngestion::PayloadBuilder.new(gzip: false)
80
+
81
+ result = builder.build(FakeChunk.new([
82
+ ['not-a-time', { 'message' => 'hello' }]
83
+ ]))
84
+
85
+ assert_match(/"message":"hello"/, result.io.read)
86
+ ensure
87
+ result&.close!
88
+ end
89
+
90
+ test 'accepts payload records with existing TimeGenerated' do
91
+ builder = Fluent::Plugin::AzureLogsIngestion::PayloadBuilder.new(gzip: false)
92
+
93
+ result = builder.build(FakeChunk.new([
94
+ ['not-a-time', { 'message' => 'hello', 'TimeGenerated' => '2026-01-01T00:00:00Z' }]
95
+ ]))
96
+
97
+ body = result.io.read
98
+ assert_match(/"TimeGenerated":"2026-01-01T00:00:00Z"/, body)
99
+ ensure
100
+ result&.close!
101
+ end
102
+
103
+ test 'rejects oversized gzip payload' do
104
+ builder = Fluent::Plugin::AzureLogsIngestion::PayloadBuilder.new(gzip: true)
105
+
106
+ error = assert_raise(Fluent::UnrecoverableError) do
107
+ builder.build(FakeChunk.new([
108
+ [Fluent::EventTime.from_time(Time.utc(2026, 1, 1, 0, 0, 0)), { 'message' => 'a' * 1_100_000 }]
109
+ ]))
110
+ end
111
+
112
+ assert_match(/payload size|gzip payload size/, error.message)
113
+ end
114
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-azure-logs-ingestion
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - fukasawah
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fluentd
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.16'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '1.16'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rake
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '13.0'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '13.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: test-unit
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '3.6'
53
+ type: :development
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '3.6'
60
+ description: Buffered Fluentd output plugin for Azure Monitor Logs Ingestion API.
61
+ email:
62
+ - ''
63
+ executables: []
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - Gemfile
68
+ - LICENSE
69
+ - README.md
70
+ - README_ja.md
71
+ - Rakefile
72
+ - lib/fluent/plugin/azure_logs_ingestion/auth.rb
73
+ - lib/fluent/plugin/azure_logs_ingestion/client.rb
74
+ - lib/fluent/plugin/azure_logs_ingestion/payload_builder.rb
75
+ - lib/fluent/plugin/azure_logs_ingestion/version.rb
76
+ - lib/fluent/plugin/out_azure_logs_ingestion.rb
77
+ - test/helper.rb
78
+ - test/support/fake_azure_server.rb
79
+ - test/support/helpers.rb
80
+ - test/test_auth_msi.rb
81
+ - test/test_auth_service_principal.rb
82
+ - test/test_out_azure_logs_ingestion.rb
83
+ - test/test_out_azure_logs_ingestion_write.rb
84
+ - test/test_payload_builder.rb
85
+ homepage: https://github.com/fukasawah/fluent-plugin-azure-logs-ingestion
86
+ licenses:
87
+ - Apache-2.0
88
+ metadata:
89
+ rubygems_mfa_required: 'true'
90
+ source_code_uri: https://github.com/fukasawah/fluent-plugin-azure-logs-ingestion
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '3.1'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 3.6.9
106
+ specification_version: 4
107
+ summary: Fluentd output plugin for Azure Monitor Logs Ingestion API
108
+ test_files: []