fluent-plugin-lm-logs 1.2.6 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 377dc56ec3f62bb91303431bbba701b179e5c5540feb4af72cfd91c27ac7b930
4
- data.tar.gz: 80317f28d2cbe4a3315d715a408212c9d047874416e6e9b0fad84719fe6e4c76
3
+ metadata.gz: 8458c35bd163ce3c82f8c7e71e9d83450e1063bee51f6f497f3a6714c7269fc1
4
+ data.tar.gz: 9ebae5b64a71e61b335f957ec61e3ceb27d766e5e4af1a634247845f082d5503
5
5
  SHA512:
6
- metadata.gz: 5fd55fb16669b87f4118e3a3617042974829ebb765cf6396909c18573840661415740cb448621e0ed1c36896882a3dc6b43a9ae93b163fdce849cd39fd47b181
7
- data.tar.gz: 83fdf0fe55a63007ba22f7bd9c1d447dc984cd2f741454c9746edef91a902a3020a68c04eb5b05df3ab9c2920079b27434d73e9c8b61a5ef195757c6866f1525
6
+ metadata.gz: e85a136c8d024082707e7daf0109de9f026ec2916a370c12e8a3b58b0fe7725e09601550fa2bd2d6a366c5e4fa63843589010b87f9642164962e26e24d174c65
7
+ data.tar.gz: 4b2e8b6b36c4baa54443068c9af875db9d93e57231bebcb48b0e419bd9fb68704c7da0dd9cd54901916cbf33f820f27f08e8ba82193056335e70495445539c01
data/README.md CHANGED
@@ -34,6 +34,34 @@ Create a custom `fluent.conf` or edit the existing one to specify which logs sho
34
34
  debug false
35
35
  </match>
36
36
  ```
37
+ ### Dynamic resource type
38
+
39
+ If you want to use a dynamic resource type, you can leave the `resource_type` field empty. The plugin will then automatically assign the resource type based on either of below:
40
+ * If user assigns source-specific tags as below:
41
+ ```
42
+ Tag windows.server1.logs
43
+ Tag linux.vm02.logs
44
+ ```
45
+ * In the fluentd conf file:
46
+ ```
47
+ <filter **>
48
+ @type record_transformer
49
+ enable_ruby true
50
+ <record>
51
+ resource_type ${record["resource_type"] || "Unknown"}
52
+ </record>
53
+ </filter>
54
+ ```
55
+ * If the remote agent includes a host field (many do), we can use heuristics:
56
+ ```
57
+
58
+ host = record['host'] || record['hostname'] || ''
59
+ return 'AWS/VirtualMachine' if host.start_with?('ip-')
60
+ return 'GCP/VirtualMachine' if host.include?('.c.') || host.include?('gcp')
61
+ return 'WindowsServer' if host.include?('win')
62
+ return 'LinuxServer' if host.include?('linux')
63
+ 'Unknown'
64
+ ```
37
65
 
38
66
  ### Request example
39
67
 
@@ -69,8 +97,8 @@ See the [LogicMonitor Helm repository](https://github.com/logicmonitor/k8s-helm-
69
97
  | `resource_mapping` | The mapping that defines the source of the log event to the LM resource. In this case, the `<event_key>` in the incoming event is mapped to the value of `<lm_property>`.|
70
98
  | `access_id` | LM API Token access ID. |
71
99
  | `access_key` | LM API Token access key. |
72
- | `resource_type` | If a Resource Type is explicitly specified, that value will be statically applied to all ingested logs. If set to `##predef.externalResourceType##`, the Resource Type will be assigned dynamically based on the log context or configuration. If left blank, the Resource Type field will remain unset in the ingested logs. |
73
- | `bearer_token` | LM API Bearer Token. Either specify `access_id` and `access_key` both or `bearer_token`. If all specified, LMv1 token(`access_id` and `access_key`) will be used for authentication with Logicmonitor. |
100
+ | `resource_type` | If a Resource Type is specified, it will be statically applied to all ingested logs. If left blank, a dynamic Resource Type will be assigned. |
101
+ | `bearer_token` | LM API Bearer Token. Either specify `access_id` and `access_key` both or `bearer_token`. If all specified, LMv1 token(`access_id` and `access_key`) will be used for authentication with Logicmonitor. |
74
102
  | `flush_interval` | Defines the time in seconds to wait before sending batches of logs to LogicMonitor. Default is `60s`. |
75
103
  | `debug` | When `true`, logs more information to the fluentd console. |
76
104
  | `force_encoding` | Specify charset when logs contains invalid utf-8 characters. |
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.metadata["source_code_uri"] = "https://github.com/logicmonitor/lm-logs-fluentd"
22
22
  spec.metadata["documentation_uri"] = "https://www.rubydoc.info/gems/lm-logs-fluentd"
23
23
 
24
- spec.files = [".gitignore", "Gemfile", "LICENSE", "README.md", "Rakefile", "fluent-plugin-lm-logs.gemspec", "lib/fluent/plugin/version.rb", "lib/fluent/plugin/out_lm.rb"]
24
+ spec.files = [".gitignore", "Gemfile", "LICENSE", "README.md", "Rakefile", "fluent-plugin-lm-logs.gemspec", "lib/fluent/plugin/version.rb", "lib/fluent/plugin/out_lm.rb", "lib/fluent/plugin/environment_detector.rb"]
25
25
  spec.require_paths = ["lib"]
26
26
  spec.required_ruby_version = '>= 2.0.0'
27
27
 
@@ -0,0 +1,201 @@
1
+ require 'net/http'
2
+ require 'timeout'
3
+ require 'json'
4
+
5
+ class EnvironmentDetector
6
+ METADATA_TIMEOUT = 1
7
+
8
+ def detect
9
+ if running_in_kubernetes?
10
+ { runtime: 'kubernetes' }.merge(detect_node_info)
11
+ elsif running_in_docker?
12
+ { runtime: 'docker' }.merge(detect_node_info)
13
+ else
14
+ detect_host_environment
15
+ end
16
+ end
17
+
18
+ def format_environment(env_info)
19
+ runtime = env_info[:runtime]
20
+ provider = env_info[:provider] if env_info.key?(:provider)
21
+
22
+ case runtime
23
+ when 'kubernetes'
24
+ 'Kubernetes/Node'
25
+ when 'docker'
26
+ 'Docker/Host'
27
+ when 'vm'
28
+ case provider&.downcase
29
+ when 'azure'
30
+ 'Azure/VirtualMachine'
31
+ when 'aws'
32
+ 'AWS/EC2'
33
+ when 'gcp'
34
+ 'GCP/ComputeEngine'
35
+ else
36
+ 'Unknown/VirtualMachine'
37
+ end
38
+ when 'physical'
39
+ os = env_info[:os] || 'UnknownOS'
40
+ product = env_info[:product] || 'UnknownHardware'
41
+ "#{os} / #{product}"
42
+ else
43
+ 'UnknownEnvironment'
44
+ end
45
+ end
46
+
47
+ def infer_resource_type(record, tag = nil)
48
+ return record['resource_type'] if record['resource_type']
49
+
50
+ host = (record['host'] || record['hostname'] || '').to_s
51
+ msg = (record['message'] || '').to_s
52
+ program = (record['syslog_program'] || '').to_s
53
+ tags = record['tags'] || []
54
+ tag_down = tag&.downcase || ''
55
+
56
+ host_down = host.downcase
57
+ msg_down = msg.downcase
58
+ program_down = program.downcase
59
+ # From tag pattern (case-insensitive)
60
+ return 'WindowsServer' if tag_down.include?('windows')
61
+ return 'LinuxServer' if tag_down.include?('linux')
62
+ return 'Kubernetes/Node' if tag_down.include?('k8s') || tag_down.include?('kubernetes')
63
+ return 'Docker/Host' if tag_down.include?('docker')
64
+
65
+ # Structured metadata
66
+ return 'Kubernetes/Node' if record.key?('kubernetes')
67
+ return 'Docker/Host' if record.key?('container_id') || record.dig('docker', 'container_id')
68
+ return 'AWS/VirtualMachine' if host_down.start_with?('ip-') || msg_down.include?('amazon')
69
+ return 'GCP/VirtualMachine' if host_down.include?('.c.') || host_down.include?('gcp')
70
+ return 'Azure/VirtualMachine' if host_down.include?('cloudapp.net') || msg_down.include?('azure')
71
+ return 'VMware/VirtualMachine' if msg_down.include?('vmware') || host_down.include?('vmware')
72
+
73
+ return 'WindowsServer' if record.key?('EventID') || record.key?('ProviderName') || record.key?('Computer')
74
+ return 'LinuxServer' if record.key?('syslog_facility') || program_down != ''
75
+
76
+ return 'Firewall' if program_down.downcase.include?('firewalld') || msg_down.downcase.include?('iptables') || msg_down.include?('blocked by policy')
77
+ return 'ACMEServer' if host_down.include?('acme') || msg_down.include?('ACME-Request') || tags.include?('acme')
78
+ return 'WebServer' if msg_down.include?('nginx') || msg_down.include?('apache')
79
+ return 'DatabaseServer' if msg_down.include?('mysql') || msg_down.include?('postgres') || msg_down.include?('oracle')
80
+
81
+ 'Unknown'
82
+ end
83
+
84
+
85
+ private
86
+
87
+ def running_in_kubernetes?
88
+ ENV.key?('KUBERNETES_SERVICE_HOST') || ENV.key?('KUBERNETES_PORT')
89
+ end
90
+
91
+ def running_in_docker?
92
+ return true if ENV['container'] == 'docker'
93
+ cgroup = File.read('/proc/1/cgroup') rescue ''
94
+ return true if cgroup.include?('docker') || cgroup.include?('containerd')
95
+ File.exist?('/.dockerenv')
96
+ end
97
+
98
+ def detect_host_environment
99
+ provider_info = detect_cloud_provider
100
+ return { runtime: 'vm', provider: provider_info[:provider], details: provider_info[:details] } if provider_info
101
+
102
+ os = detect_os
103
+ product = detect_product_info
104
+
105
+ if product.downcase.include?('xen hvm domu') && os.downcase.include?('amazon')
106
+ return { runtime: 'vm', provider: 'aws', details: { os: os, product: product } }
107
+ end
108
+
109
+ { runtime: 'physical', os: os, product: product }
110
+ end
111
+
112
+ def detect_node_info
113
+ { node_os: detect_os, node_product: detect_product_info }
114
+ end
115
+
116
+ def detect_cloud_provider
117
+ azure_metadata || aws_metadata || gcp_metadata
118
+ end
119
+
120
+ def azure_metadata
121
+ url = 'http://169.254.169.254/metadata/instance?api-version=2021-02-01'
122
+ headers = { 'Metadata' => 'true' }
123
+ response = fetch_metadata(url, headers)
124
+ return unless response
125
+ json = JSON.parse(response) rescue {}
126
+ {
127
+ provider: 'azure',
128
+ details: {
129
+ vm_id: json.dig('compute', 'vmId'),
130
+ location: json.dig('compute', 'location'),
131
+ name: json.dig('compute', 'name'),
132
+ vm_size: json.dig('compute', 'vmSize')
133
+ }
134
+ }
135
+ end
136
+
137
+ def aws_metadata
138
+ url = 'http://169.254.169.254/latest/meta-data/instance-id'
139
+ response = fetch_metadata(url)
140
+ return unless response
141
+ { provider: 'aws', details: { instance_id: response.strip } }
142
+ end
143
+
144
+ def gcp_metadata
145
+ url = 'http://169.254.169.254/computeMetadata/v1/instance/id'
146
+ headers = { 'Metadata-Flavor' => 'Google' }
147
+ response = fetch_metadata(url, headers)
148
+ return unless response
149
+ { provider: 'gcp', details: { instance_id: response.strip } }
150
+ end
151
+
152
+ def fetch_metadata(url, headers = {}, timeout_sec = METADATA_TIMEOUT)
153
+ uri = URI(url)
154
+ Timeout.timeout(timeout_sec) do
155
+ req = Net::HTTP::Get.new(uri)
156
+ headers.each { |k, v| req[k] = v }
157
+ res = Net::HTTP.start(uri.host, uri.port, open_timeout: timeout_sec, read_timeout: timeout_sec) { |http| http.request(req) }
158
+ return res.body if res.is_a?(Net::HTTPSuccess)
159
+ end
160
+ rescue Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, EOFError
161
+ nil
162
+ end
163
+
164
+ def detect_os
165
+ if File.exist?('/etc/os-release')
166
+ os_info = {}
167
+ File.foreach('/etc/os-release') do |line|
168
+ key, value = line.strip.split('=', 2)
169
+ os_info[key] = value&.gsub('"', '')
170
+ end
171
+ "#{os_info['NAME']} #{os_info['VERSION']}"
172
+ elsif RUBY_PLATFORM.include?('darwin')
173
+ product_name = `sw_vers -productName`.strip
174
+ product_version = `sw_vers -productVersion`.strip
175
+ "#{product_name} #{product_version}"
176
+ else
177
+ `uname -a`.strip
178
+ end
179
+ rescue
180
+ 'unknown'
181
+ end
182
+
183
+ def detect_product_info
184
+ if File.exist?('/sys/class/dmi/id/sys_vendor') && File.exist?('/sys/class/dmi/id/product_name')
185
+ vendor = read_file('/sys/class/dmi/id/sys_vendor')
186
+ product = read_file('/sys/class/dmi/id/product_name')
187
+ "#{vendor} #{product}".strip
188
+ elsif RUBY_PLATFORM.include?('darwin')
189
+ model = `system_profiler SPHardwareDataType | awk '/Model Identifier/ { print $3 }'`.strip
190
+ model.empty? ? 'Mac' : model
191
+ else
192
+ 'unknown'
193
+ end
194
+ rescue
195
+ 'unknown'
196
+ end
197
+
198
+ def read_file(path)
199
+ File.read(path).strip if File.exist?(path)
200
+ end
201
+ end
@@ -10,6 +10,7 @@ require 'net/http'
10
10
  require 'net/http/persistent'
11
11
  require 'net/https'
12
12
  require('zlib')
13
+ require_relative 'environment_detector'
13
14
 
14
15
  require_relative "version"
15
16
 
@@ -83,6 +84,11 @@ module Fluent
83
84
  @http_client.override_headers["User-Agent"] = log_source + "/" + LmLogsFluentPlugin::VERSION
84
85
  @url = "https://#{@company_name}.#{@company_domain}/rest/log/ingest"
85
86
  @uri = URI.parse(@url)
87
+ @detector = EnvironmentDetector.new
88
+ @environment_info = @detector.detect
89
+ @local_env_str = format_environment(@environment_info)
90
+
91
+ log.info("Environment detected: #{@environment_info}")
86
92
  end
87
93
 
88
94
  def configure_auth
@@ -174,10 +180,13 @@ module Fluent
174
180
  end
175
181
  lm_event["message"] = encode_if_necessary(record["message"])
176
182
 
177
- if !is_blank(@resource_type)
178
- lm_event['_resource.type'] = resource_type
183
+ resource_type = @resource_type || @detector.infer_resource_type(record, tag)
184
+ if resource_type.nil? || resource_type.strip.empty? || resource_type == 'Unknown'
185
+ resource_type = @local_env_str
179
186
  end
180
187
 
188
+ lm_event['_resource.type'] = resource_type
189
+
181
190
  return lm_event
182
191
  end
183
192
 
@@ -264,6 +273,34 @@ module Fluent
264
273
  end
265
274
  end
266
275
 
276
+ def format_environment(env_info)
277
+ runtime = env_info[:runtime]
278
+ provider = env_info[:provider] if env_info.key?(:provider)
279
+
280
+ case runtime
281
+ when 'kubernetes'
282
+ 'Kubernetes/Node'
283
+ when 'docker'
284
+ 'Docker/Host'
285
+ when 'vm'
286
+ case provider&.downcase
287
+ when 'azure'
288
+ 'Azure/VirtualMachine'
289
+ when 'aws'
290
+ 'AWS/EC2'
291
+ when 'gcp'
292
+ 'GCP/ComputeEngine'
293
+ else
294
+ 'Unknown/VirtualMachine'
295
+ end
296
+ when 'physical'
297
+ os = env_info[:os] || 'UnknownOS'
298
+ product = env_info[:product] || 'UnknownHardware'
299
+ "#{os} / #{product}"
300
+ else
301
+ 'UnknownEnvironment'
302
+ end
303
+ end
267
304
  end
268
305
  end
269
306
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LmLogsFluentPlugin
4
- VERSION = '1.2.6'
4
+ VERSION = '1.2.7'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-lm-logs
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.6
4
+ version: 1.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - LogicMonitor
@@ -56,6 +56,7 @@ files:
56
56
  - README.md
57
57
  - Rakefile
58
58
  - fluent-plugin-lm-logs.gemspec
59
+ - lib/fluent/plugin/environment_detector.rb
59
60
  - lib/fluent/plugin/out_lm.rb
60
61
  - lib/fluent/plugin/version.rb
61
62
  homepage: https://www.logicmonitor.com