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.
- checksums.yaml +7 -0
- data/Gemfile +8 -0
- data/LICENSE +13 -0
- data/README.md +209 -0
- data/README_ja.md +209 -0
- data/Rakefile +10 -0
- data/lib/fluent/plugin/azure_logs_ingestion/auth.rb +184 -0
- data/lib/fluent/plugin/azure_logs_ingestion/client.rb +92 -0
- data/lib/fluent/plugin/azure_logs_ingestion/payload_builder.rb +112 -0
- data/lib/fluent/plugin/azure_logs_ingestion/version.rb +9 -0
- data/lib/fluent/plugin/out_azure_logs_ingestion.rb +99 -0
- data/test/helper.rb +6 -0
- data/test/support/fake_azure_server.rb +100 -0
- data/test/support/helpers.rb +41 -0
- data/test/test_auth_msi.rb +76 -0
- data/test/test_auth_service_principal.rb +57 -0
- data/test/test_out_azure_logs_ingestion.rb +60 -0
- data/test/test_out_azure_logs_ingestion_write.rb +170 -0
- data/test/test_payload_builder.rb +114 -0
- metadata +108 -0
|
@@ -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: []
|