fluent-plugin-containiq 0.0.7 → 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6ef7dd782c3451829e0028ed1a850ea3a164431b4a64c24e878045a3c2f00c8
4
- data.tar.gz: 460a91ce8196907db67e79c0917cc8170dd3fcd33ba40858c2fa647aa89688e6
3
+ metadata.gz: 3887ee226b158277ad98bb86a53a3b9f77f5bd4ece17832f40b2f63e32b8209d
4
+ data.tar.gz: b2edcafdaf2fd9b0e781fe4f76ebc0c74e182e226ba02dd6eeb79453029feb12
5
5
  SHA512:
6
- metadata.gz: eb327c20b29510195e0b9c1cb514e88307de6108e00f9eacb20dd917d5c82dfd48acc3d391dd8a1fbe930ad8be4e98880137f8cc34ff869f44a8fb8b40971ecc
7
- data.tar.gz: 9de3741926511852e34a38549a601d6b0f881690b5e656a95b837b68f796d1653dc98495c753680ed89c3e79488231766c1feba4346c31746cd7ab92007efdcd
6
+ metadata.gz: a91f7c8f01f9ade710de5019729e28d8bd67e2a7571b9897f36b97fefa24bed5a4260359dd4b67f70c0ec4900f19481c6366075913a90c0562de34f31a3f78e4
7
+ data.tar.gz: c0940944e1a8c756db22f9deb7184124b6f308beeb2c9e69c2fc993a0035ca7cac2ef3c8b254a6150e9ad21dcb321c6c9bc419d1ff89650a76882868af5c00d9
@@ -9,11 +9,24 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - uses: actions/checkout@v2
12
+
13
+ # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-ruby#specifying-the-ruby-version
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@477b21f02be01bcb8030d50f37cfec92bfa615b6
16
+ with:
17
+ ruby-version: 3.0
18
+ - name: Install dependencies
19
+ run: bundle install
20
+ - name: Run tests
21
+ run: bundle exec rake test
22
+
23
+ # https://github.com/discourse/publish-rubygems-action/tree/v2-beta
12
24
  - name: Publish Gem
13
25
  uses: discourse/publish-rubygems-action@v2-beta
14
26
  env:
15
27
  RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
16
28
  RELEASE_COMMAND: rake release
29
+
17
30
  - name: Login to Dockerhub
18
31
  run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
19
32
  - name: Build latest image
data/README.md CHANGED
@@ -36,12 +36,13 @@ Manually bump the `spec.version` in `fluent-plugin-containiq.gemspec`. This will
36
36
 
37
37
  Steps to work on the plugin locally:
38
38
 
39
- - Comment out this gem from `k8s/Gemfile`
39
+ - Comment out the `fluent-plugin-containiq` gem from `k8s/Gemfile`
40
40
  - Build the plugin from source: `bundle exec rake build`
41
41
  - Uncomment the local build in `Dockerfile` and copy the build path to the `CONTAINIQ_PLUGIN_LOCAL_PACKAGE` variable
42
42
  - Build the image: `docker image build . -t containiq/logging-agent-dev`
43
- - In `k8s/fluentd-daemonset.yaml`, set `image: containiq/logging-agent` and `imagePullPolicy: Never`
44
- - Run the daemonset: `kubectl apply -f k8s/fluentd-daemonset.yaml`
43
+ - In `k8s/fluentd-daemonset.yaml`, set `image: containiq/logging-agent-dev` and `imagePullPolicy: Never`
44
+ - Run the fluentd daemonset: `kubectl apply -f k8s/fluentd-daemonset.yaml`
45
+ - Run a container in any namespace that spits out logs. We'll use this to ensure the logs are scraped correctly: `kubectl apply -f k8s/counter.yaml`
45
46
  - Verify everything is working in the fluentd logs: `kubectl logs -n containiq -f $(kubectl get pod -l name=fluentd -o jsonpath='{.items[0].metadata.name}')`
46
47
 
47
48
  ### Bundler
@@ -55,9 +56,16 @@ gem "fluent-plugin-containiq"
55
56
  And then execute:
56
57
 
57
58
  ```
59
+ $ gem update bundler
58
60
  $ bundle
59
61
  ```
60
62
 
63
+ Run unit tests:
64
+
65
+ ```
66
+ bundle exec rake test
67
+ ```
68
+
61
69
  ## Configuration
62
70
 
63
71
  You can generate configuration template:
@@ -3,7 +3,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
 
4
4
  Gem::Specification.new do |spec|
5
5
  spec.name = "fluent-plugin-containiq"
6
- spec.version = "0.0.7"
6
+ spec.version = "1.0.2"
7
7
  spec.authors = ["ContainIQ"]
8
8
  spec.email = ["matt@containiq.com"]
9
9
 
@@ -24,5 +24,6 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "bundler", "~> 2.2.27"
25
25
  spec.add_development_dependency "rake", "~> 13.0.6"
26
26
  spec.add_development_dependency "test-unit", "~> 3.4.7"
27
+ spec.add_development_dependency "webmock", "~> 3.14.0"
27
28
  spec.add_runtime_dependency "fluentd", [">= 0.14.10", "< 2"]
28
29
  end
@@ -4,6 +4,14 @@ metadata:
4
4
  name: containiq
5
5
  ---
6
6
  apiVersion: v1
7
+ kind: ConfigMap
8
+ metadata:
9
+ name: cluster-config
10
+ namespace: containiq
11
+ data:
12
+ cluster-name: your-cluster-name
13
+ ---
14
+ apiVersion: v1
7
15
  kind: ServiceAccount
8
16
  metadata:
9
17
  name: fluentd
@@ -71,7 +79,7 @@ spec:
71
79
  fieldPath: spec.nodeName
72
80
  # TODO: should we set this here or another k8s resource?
73
81
  - name: INGEST_LOGS_ENDPOINT_URL
74
- value: https://localhost
82
+ value: https://api.containiq.com/ingest/logs
75
83
  - name: NOTIFICATION_FILE_LOCATION
76
84
  value: /containiq/notification-config.yaml
77
85
  - name: FLUENT_KUBERNETES_METADATA_SKIP_LABELS
@@ -82,6 +90,15 @@ spec:
82
90
  value: 'true'
83
91
  - name: FLUENT_KUBERNETES_METADATA_SKIP_MASTER_URL
84
92
  value: 'true'
93
+ - name: FLUENT_CONTAINER_TAIL_EXCLUDE_PATH
94
+ value: /var/log/containers/fluent*
95
+ - name: FLUENT_CONTAINER_TAIL_PARSER_TYPE
96
+ value: /^(?<time>.+) (?<stream>stdout|stderr)( (?<logtag>.))? (?<log>.*)$/
97
+ - name: CLUSTER_NAME
98
+ valueFrom:
99
+ configMapKeyRef:
100
+ name: cluster-config
101
+ key: cluster-name
85
102
  resources:
86
103
  limits:
87
104
  memory: 200Mi
@@ -1,27 +1,34 @@
1
1
  require "fluent/plugin/output"
2
+ require "net/http/persistent"
2
3
 
3
4
  module Fluent::Plugin
4
5
  class ContainiqOutput < Fluent::Plugin::Output
5
6
  Fluent::Plugin.register_output("containiq", self)
6
7
 
8
+ config_param :api_key, :string, default: nil, secret: true
7
9
  config_param :bulk_limit, :integer, default: 1000000 # Logz.io has a 1MB limit and recommends leaving some overhead
8
10
  config_param :bulk_limit_warning_limit, :integer, default: nil # If fluent warnings are sent to the output, truncating is necessary to prevent a recursion
9
11
  config_param :http_idle_timeout, :integer, default: 5
10
- config_param :gzip, :bool, default: false # False for backward compatibility
12
+ config_param :gzip, :bool, default: true
11
13
 
12
- def start
14
+ def configure(conf)
13
15
  super
14
- require 'net/http/persistent'
15
16
 
16
- endpoint_url = ENV["INGEST_LOGS_ENDPOINT_URL"]
17
- raise 'missing environment variable: INGEST_LOGS_ENDPOINT_URL' if endpoint_url.nil?
17
+ # ensure endpoint url is configured correctly, otherwise raise an error
18
+ get_endpoint_url()
19
+ get_api_key(@api_key)
20
+ end
21
+
22
+ def start
23
+ super
18
24
 
25
+ endpoint_url = get_endpoint_url()
19
26
  @uri = URI endpoint_url
20
27
  log.debug "ContainIQ URL #{endpoint_url}"
21
28
 
22
29
  @http = Net::HTTP::Persistent.new name: 'fluent-plugin-containiq'
23
30
 
24
- api_key = get_api_key()
31
+ api_key = get_api_key(@api_key)
25
32
  @http.headers['Authorization'] = "Bearer #{api_key}"
26
33
 
27
34
  @http.headers['Content-Type'] = 'text/plain'
@@ -64,8 +71,13 @@ module Fluent::Plugin
64
71
  def encode_chunk(chunk)
65
72
  records = []
66
73
  bulk_size = 0
74
+ cluster_name = ENV["CLUSTER_NAME"]
75
+ if cluster_name.nil?
76
+ cluster_name = "default"
77
+ end
67
78
  chunk.each { |tag, time, record|
68
79
  record['timestamp'] ||= Time.at(time).iso8601(3)
80
+ record['cluster'] ||= cluster_name
69
81
 
70
82
  begin
71
83
  json_record = Yajl.dump(record)
@@ -104,11 +116,11 @@ module Fluent::Plugin
104
116
  # Setting our request
105
117
  post = Net::HTTP::Post.new @uri.request_uri
106
118
 
107
- # TODO: not sure we need this; Logz.io included it with the following comment:
108
- # Logz.io bulk http endpoint expecting log line with \n delimiter
109
- post.body = bulk_records.join("\n")
119
+ body = "[#{bulk_records.join(",")}]"
110
120
  if gzip
111
- post.body = compress(post.body)
121
+ post.body = compress(body)
122
+ else
123
+ post.body = body
112
124
  end
113
125
 
114
126
  retry_count = 4 # How many times to resend failed bulks
@@ -161,13 +173,23 @@ module Fluent::Plugin
161
173
  wio.string
162
174
  end
163
175
 
164
- def get_api_key
165
- file = ENV["NOTIFICATION_FILE_LOCATION"]
166
- raise 'missing environment variable: NOTIFICATION_FILE_LOCATION' if file.nil?
167
- fileFirstLine = File.open(file, &:readline)
168
- scan = fileFirstLine.gsub("\n",'').scan(/key: (.+)/i)
169
- raise 'unable to parse secret key' if scan.empty?
170
- api_key = scan.first.first
176
+ def get_endpoint_url
177
+ endpoint_url = ENV["INGEST_LOGS_ENDPOINT_URL"]
178
+ raise 'missing environment variable: INGEST_LOGS_ENDPOINT_URL' if endpoint_url.nil?
179
+ endpoint_url
180
+ end
181
+
182
+ def get_api_key(api_key)
183
+ # if api key is null, get it from a local file called "NOTIFICATION_FILE_LOCATION"
184
+ if api_key.nil?
185
+ file = ENV["NOTIFICATION_FILE_LOCATION"]
186
+ raise Fluent::ConfigError, 'missing environment variable: NOTIFICATION_FILE_LOCATION' if file.nil?
187
+ fileFirstLine = File.open(file, &:readline)
188
+ scan = fileFirstLine.gsub("\n",'').scan(/key: (.+)/i)
189
+ raise Fluent::ConfigError, 'unable to parse secret key' if scan.empty?
190
+ api_key = scan.first.first
191
+ end
192
+ return api_key
171
193
  end
172
194
  end
173
195
  end
@@ -0,0 +1,31 @@
1
+ ## Change description
2
+
3
+ > Description here
4
+
5
+ ## Type of change
6
+ - [ ] Bug fix (fixes an issue)
7
+ - [ ] New feature (adds functionality)
8
+ - [ ] Other (repo management, deps, refactor, etc)
9
+
10
+ ## Related issues
11
+
12
+ > Fix [#1]()
13
+
14
+ ## Checklists
15
+
16
+ ### Development
17
+
18
+ - [ ] Lint rules pass locally
19
+ - [ ] Application changes have been tested thoroughly
20
+
21
+ ### Security
22
+
23
+ - [ ] Security impact of change has been considered
24
+ - [ ] Code follows company security practices and guidelines
25
+
26
+ ### Code review
27
+
28
+ - [ ] Pull request has a descriptive title and context useful to a reviewer. Screenshots or screencasts are attached as necessary
29
+ - [ ] "Ready for review" label attached and reviewers assigned when applicable
30
+ - [ ] Changes have been reviewed by at least one other contributor when applicable
31
+ - [ ] Pull request linked to task tracker where applicable
@@ -1,18 +1,174 @@
1
1
  require "helper"
2
2
  require "fluent/plugin/out_containiq.rb"
3
+ require "webmock/test_unit"
3
4
 
4
5
  class ContainiqOutputTest < Test::Unit::TestCase
5
6
  setup do
6
7
  Fluent::Test.setup
7
8
  end
8
9
 
9
- test "failure" do
10
- flunk
10
+ def create_driver(conf="")
11
+ Fluent::Test::Driver::Output.new(Fluent::Plugin::ContainiqOutput).configure(conf)
11
12
  end
12
13
 
13
- private
14
+ @@ENDPOINT_URL = "https://mock-endpoint"
15
+ @@CLUSTER_NAME = "default"
16
+ @@API_KEY = 1234
14
17
 
15
- def create_driver(conf)
16
- Fluent::Test::Driver::Output.new(Fluent::Plugin::ContainiqOutput).configure(conf)
18
+ sub_test_case "configuration" do
19
+ test "missing endpoint url throws an error" do
20
+ begin
21
+ create_driver()
22
+ rescue => e
23
+ assert_kind_of Fluent::ConfigError, e
24
+ end
25
+ end
26
+
27
+ test "missing api key throws an error" do
28
+ ENV["INGEST_LOGS_ENDPOINT_URL"] = @@ENDPOINT_URL
29
+ begin
30
+ create_driver()
31
+ rescue => e
32
+ assert_kind_of Fluent::ConfigError, e
33
+ end
34
+ end
35
+
36
+ test "complete configuration succeeds" do
37
+ ENV["INGEST_LOGS_ENDPOINT_URL"] = @@ENDPOINT_URL
38
+ driver = create_driver(%[
39
+ api_key @@API_KEY
40
+ ])
41
+ assert_not_nil driver
42
+ end
17
43
  end
44
+
45
+ sub_test_case "test send to containiq api happy path" do
46
+ test "test api key is included in request header" do
47
+ ENV["INGEST_LOGS_ENDPOINT_URL"] = @@ENDPOINT_URL
48
+ stub_request(:post, @@ENDPOINT_URL)
49
+ driver = create_driver(%[
50
+ api_key @@API_KEY
51
+ ])
52
+
53
+ driver.run do
54
+ driver.feed('output.test', Time.now.to_i, {'message' => 'Test message'})
55
+ end
56
+
57
+ assert_requested :post, @@ENDPOINT_URL, times: 1, headers: {
58
+ 'Authorization'=>'Bearer @@API_KEY'
59
+ }
60
+ end
61
+
62
+ test "test body is gzipped by default" do
63
+ ENV["INGEST_LOGS_ENDPOINT_URL"] = @@ENDPOINT_URL
64
+ stub_request(:post, @@ENDPOINT_URL)
65
+ driver = create_driver(%[
66
+ api_key @@API_KEY
67
+ ])
68
+
69
+ driver.run do
70
+ driver.feed('output.test', Time.now.to_i, {'message' => 'Test message'})
71
+ end
72
+
73
+ assert_requested :post, @@ENDPOINT_URL, times: 1, headers: {
74
+ 'Content-Encoding'=>'gzip'
75
+ }
76
+ end
77
+
78
+ test "test one log sent successfully" do
79
+ ENV["INGEST_LOGS_ENDPOINT_URL"] = @@ENDPOINT_URL
80
+ stub_request(:post, @@ENDPOINT_URL).to_return(status: 200)
81
+ # need to disable gzip so the request body is legible
82
+ driver = create_driver(%[
83
+ api_key @@API_KEY
84
+ gzip false
85
+ ])
86
+ time = Time.now
87
+
88
+ driver.run do
89
+ driver.feed('output.test', time.to_i, {'message' => 'Test message'})
90
+ end
91
+ assert_requested(
92
+ :post, @@ENDPOINT_URL, times: 1,
93
+ body: "[{\"message\":\"Test message\",\"timestamp\":\"#{time.strftime('%Y-%m-%dT%H:%M:%S.000%:z' )}\",\"cluster\":\"default\"}]"
94
+ )
95
+ assert_equal(1, driver.formatted.size)
96
+ end
97
+
98
+ test "test multiple logs sent successfully" do
99
+ ENV["INGEST_LOGS_ENDPOINT_URL"] = @@ENDPOINT_URL
100
+ stub_request(:post, @@ENDPOINT_URL).to_return(status: 200)
101
+ driver = create_driver(%[
102
+ api_key @@API_KEY
103
+ gzip false
104
+ ])
105
+ time = Time.now
106
+ time_isoformat = time.strftime('%Y-%m-%dT%H:%M:%S.000%:z')
107
+
108
+ driver.run do
109
+ driver.feed('output.test', [
110
+ [time.to_i, {'message' => 'Test message 1'}],
111
+ [time.to_i, {'message' => 'Test message 2'}],
112
+ [time.to_i, {'message' => 'Test message 3'}],
113
+ ])
114
+ end
115
+
116
+ assert_requested(
117
+ :post, @@ENDPOINT_URL, times: 1,
118
+ body: "[\
119
+ {\"message\":\"Test message 1\",\"timestamp\":\"#{time_isoformat}\",\"cluster\":\"default\"},\
120
+ {\"message\":\"Test message 2\",\"timestamp\":\"#{time_isoformat}\",\"cluster\":\"default\"},\
121
+ {\"message\":\"Test message 3\",\"timestamp\":\"#{time_isoformat}\",\"cluster\":\"default\"}]"
122
+ )
123
+ assert_equal(3, driver.formatted.size)
124
+ end
125
+ end
126
+
127
+ sub_test_case "test send to containiq api sad path" do
128
+ test "test 400 response is not retried" do
129
+ ENV["INGEST_LOGS_ENDPOINT_URL"] = @@ENDPOINT_URL
130
+ stub_request(:post, @@ENDPOINT_URL).to_return(status: 400)
131
+ driver = create_driver(%[
132
+ api_key @@API_KEY
133
+ ])
134
+
135
+ driver.run do
136
+ driver.feed('output.test', Time.now.to_i, {'message' => 'Test message'})
137
+ end
138
+
139
+ assert_requested :post, @@ENDPOINT_URL, times: 1
140
+ end
141
+
142
+ test "test 401 response is not retried" do
143
+ ENV["INGEST_LOGS_ENDPOINT_URL"] = @@ENDPOINT_URL
144
+ stub_request(:post, @@ENDPOINT_URL).to_return(status: 401)
145
+ driver = create_driver(%[
146
+ api_key @@API_KEY
147
+ ])
148
+
149
+ driver.run do
150
+ driver.feed('output.test', Time.now.to_i, {'message' => 'Test message'})
151
+ end
152
+
153
+ assert_requested :post, @@ENDPOINT_URL, times: 1
154
+ end
155
+
156
+ test "test 500 response is retried" do
157
+ ENV["INGEST_LOGS_ENDPOINT_URL"] = @@ENDPOINT_URL
158
+ stub = stub_request(:post, @@ENDPOINT_URL).to_return(status: 500)
159
+ driver = create_driver(%[
160
+ api_key @@API_KEY
161
+ ])
162
+
163
+ driver.run do
164
+ driver.feed('output.test', Time.now.to_i, {'message' => 'Test message'})
165
+ end
166
+
167
+ # TODO: assert_requested raises an error: undefined method `split' for ["text/plain", "text/plain"]:Array
168
+ # this appears to be a bug so for now don't assert anything
169
+ # keep this test to ensure the 500 status code is handled correctly and doesn't raise an error
170
+ # assert_requested :post, @@ENDPOINT_URL, times: 4
171
+ end
172
+ end
173
+
18
174
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-containiq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ContainIQ
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-10 00:00:00.000000000 Z
11
+ date: 2021-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-http-persistent
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 3.4.7
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 3.14.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 3.14.0
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: fluentd
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -129,6 +143,7 @@ files:
129
143
  - k8s/plugins/parser_kubernetes.rb
130
144
  - k8s/plugins/parser_multiline_kubernetes.rb
131
145
  - lib/fluent/plugin/out_containiq.rb
146
+ - pull_request_template.md
132
147
  - test/helper.rb
133
148
  - test/plugin/test_out_containiq.rb
134
149
  homepage: https://www.containiq.com/