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 +4 -4
- data/README.md +30 -2
- data/fluent-plugin-lm-logs.gemspec +1 -1
- data/lib/fluent/plugin/environment_detector.rb +201 -0
- data/lib/fluent/plugin/out_lm.rb +39 -2
- data/lib/fluent/plugin/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8458c35bd163ce3c82f8c7e71e9d83450e1063bee51f6f497f3a6714c7269fc1
|
4
|
+
data.tar.gz: 9ebae5b64a71e61b335f957ec61e3ceb27d766e5e4af1a634247845f082d5503
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
data/lib/fluent/plugin/out_lm.rb
CHANGED
@@ -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
|
-
|
178
|
-
|
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
|
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.
|
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
|