fluent-plugin-zebrium_output 1.57.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,1131 @@
1
+ require 'fluent/plugin/output'
2
+ require 'net/https'
3
+ require 'yajl'
4
+ require 'httpclient'
5
+ require 'uri'
6
+ require 'json'
7
+ require 'docker'
8
+ require 'yaml'
9
+ require 'time'
10
+
11
+ $ZLOG_COLLECTOR_VERSION = '1.57.0'
12
+
13
+ class PathMappings
14
+ def initialize
15
+ @active = false
16
+ @patterns = Array.new
17
+ @ids = Hash.new
18
+ @cfgs = Hash.new
19
+ @tags = Hash.new
20
+ end
21
+
22
+ attr_accessor :active
23
+ attr_accessor :patterns
24
+ attr_accessor :ids
25
+ attr_accessor :cfgs
26
+ attr_accessor :tags
27
+ end
28
+
29
+ class PodConfig
30
+ def initialize
31
+ @cfgs = Hash.new
32
+ @atime = Time.now()
33
+ end
34
+
35
+ attr_accessor :cfgs
36
+ attr_accessor :atime
37
+ end
38
+
39
+ class PodConfigs
40
+ def initialize
41
+ @cfgs = Hash.new
42
+ end
43
+ attr_accessor :cfgs
44
+ end
45
+
46
+ class NamespaceToServiceGroup
47
+ def initialize
48
+ @active = false
49
+ @svcgrps = Hash.new
50
+ end
51
+ attr_accessor :active
52
+ attr_accessor :svcgrps
53
+ end
54
+
55
+ class Fluent::Plugin::Zebrium < Fluent::Plugin::Output
56
+ Fluent::Plugin.register_output('zebrium', self)
57
+
58
+ helpers :inject, :formatter, :compat_parameters
59
+
60
+ DEFAULT_LINE_FORMAT_TYPE = 'stdout'
61
+ DEFAULT_FORMAT_TYPE = 'json'
62
+ DEFAULT_BUFFER_TYPE = "memory"
63
+ DEFAULT_DEPLOYMENT_NAME = "default"
64
+
65
+ config_param :ze_log_collector_url, :string, :default => ""
66
+ config_param :ze_log_collector_token, :string, :default => ""
67
+ config_param :ze_log_collector_type, :string, :default => "kubernetes"
68
+ config_param :ze_host, :string, :default => ""
69
+ config_param :ze_timezone, :string, :default => ""
70
+ config_param :ze_deployment_name, :string, :default => ""
71
+ config_param :ze_host_tags, :string, :default => ""
72
+ config_param :use_buffer, :bool, :default => true
73
+ config_param :verify_ssl, :bool, :default => false
74
+ config_param :ze_send_json, :bool, :default => false
75
+ config_param :ze_support_data_send_intvl, :integer, :default => 600
76
+ config_param :log_forwarder_mode, :bool, :default => false
77
+ config_param :ec2_api_client_timeout_secs, :integer, :default => 1
78
+ config_param :disable_ec2_meta_data, :bool, :default => true
79
+ config_param :ze_host_in_logpath, :integer, :default => 0
80
+ config_param :ze_forward_tag, :string, :default => "ze_forwarded_logs"
81
+ config_param :ze_path_map_file, :string, :default => ""
82
+ config_param :ze_ns_svcgrp_map_file, :string, :default => ""
83
+ config_param :ze_handle_host_as_config, :bool, :default => false
84
+
85
+ config_section :format do
86
+ config_set_default :@type, DEFAULT_LINE_FORMAT_TYPE
87
+ config_set_default :output_type, DEFAULT_FORMAT_TYPE
88
+ end
89
+
90
+ config_section :buffer do
91
+ config_set_default :@type, DEFAULT_BUFFER_TYPE
92
+ config_set_default :chunk_keys, ['time']
93
+ end
94
+
95
+ def initialize
96
+ super
97
+ @etc_hostname = ""
98
+ @k8s_hostname = ""
99
+ if File.exist?("/mnt/etc/hostname")
100
+ # Inside fluentd container
101
+ # In that case that host /etc/hostname is a directory, we will
102
+ # get empty string (for example, on GKE hosts). We will
103
+ # try to get hostname from log record from kubernetes.
104
+ if File.file?("/mnt/etc/hostname")
105
+ File.open("/mnt/etc/hostname", "r").each do |line|
106
+ @etc_hostname = line.strip().chomp
107
+ end
108
+ end
109
+ else
110
+ if File.exist?("/etc/hostname")
111
+ # Run directly on host
112
+ File.open("/etc/hostname", "r").each do |line|
113
+ @etc_hostname = line.strip().chomp
114
+ end
115
+ end
116
+ if @etc_hostname.empty?
117
+ @etc_hostname = `hostname`.strip().chomp
118
+ end
119
+ end
120
+ # Pod names can have two formats:
121
+ # 1. <deployment_name>-84ff57c87c-pc6xm
122
+ # 2. <deployment_name>-pc6xm
123
+ # We use the following two regext to find deployment name. Ideally we want kubernetes filter
124
+ # to pass us deployment name, but currently it doesn't.
125
+ @pod_name_to_deployment_name_regexp_long_compiled = Regexp.compile('(?<deployment_name>[a-z0-9]([-a-z0-9]*))-[a-f0-9]{9,10}-[a-z0-9]{5}')
126
+ @pod_name_to_deployment_name_regexp_short_compiled = Regexp.compile('(?<deployment_name>[a-z0-9]([-a-z0-9]*))-[a-z0-9]{5}')
127
+ @stream_tokens = {}
128
+ @stream_token_req_sent = 0
129
+ @stream_token_req_success = 0
130
+ @data_post_sent = 0
131
+ @data_post_success = 0
132
+ @support_post_sent = 0
133
+ @support_post_success = 0
134
+ @last_support_data_sent = 0
135
+ end
136
+
137
+ def multi_workers_ready?
138
+ false
139
+ end
140
+
141
+ def prefer_buffered_processing
142
+ @use_buffer
143
+ end
144
+
145
+ attr_accessor :formatter
146
+
147
+ # This method is called before starting.
148
+ def configure(conf)
149
+ log.info("out_zebrium::configure() called")
150
+ compat_parameters_convert(conf, :inject, :formatter)
151
+ super
152
+ @formatter = formatter_create
153
+ @ze_tags = {}
154
+ kvs = conf.key?('ze_host_tags') ? conf['ze_host_tags'].split(','): []
155
+ for kv in kvs do
156
+ ary = kv.split('=')
157
+ if ary.length != 2 or ary[0].empty? or ary[1].empty?
158
+ log.error("Invalid tag in ze_host_tags: #{kv}")
159
+ continue
160
+ end
161
+ log.info("add ze_tag[" + ary[0] + "]=" + ary[1])
162
+ if ary[0] == "ze_deployment_name" and @ze_deployment_name.empty?
163
+ log.info("Use ze_deployment_name from ze_tags")
164
+ @ze_deployment_name = ary[1]
165
+ else
166
+ @ze_tags[ary[0]] = ary[1]
167
+ end
168
+ end
169
+ if @ze_deployment_name.empty?
170
+ log.info("Set deployment name to default value " + DEFAULT_DEPLOYMENT_NAME)
171
+ @ze_deployment_name = DEFAULT_DEPLOYMENT_NAME
172
+ end
173
+
174
+ @path_mappings = PathMappings.new
175
+ @pod_configs = PodConfigs.new
176
+ @ns_to_svcgrp_mappings = NamespaceToServiceGroup.new
177
+ read_path_mappings()
178
+ read_ns_to_svcgrp_mappings()
179
+ @file_mappings = {}
180
+ if @log_forwarder_mode
181
+ log.info("out_zebrium running in log forwarder mode")
182
+ else
183
+ read_file_mappings()
184
+ if @disable_ec2_meta_data == false
185
+ ec2_host_meta = get_ec2_host_meta_data()
186
+ for k in ec2_host_meta.keys do
187
+ log.info("add ec2 meta data " + k + "=" + ec2_host_meta[k])
188
+ @ze_tags[k] = ec2_host_meta[k]
189
+ end
190
+ else
191
+ log.info("EC2 meta data collection is disabled")
192
+ end
193
+ end
194
+ @http = HTTPClient.new()
195
+ if @verify_ssl
196
+ @http.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_PEER
197
+ @http.ssl_config.add_trust_ca "/usr/lib/ssl/certs"
198
+ else
199
+ @http.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
200
+ end
201
+ @http.connect_timeout = 60
202
+ @zapi_uri = URI(conf["ze_log_collector_url"])
203
+ @zapi_token_uri = @zapi_uri.clone
204
+ @zapi_token_uri.path = @zapi_token_uri.path + "/log/api/v2/token"
205
+ @zapi_post_uri = @zapi_uri.clone
206
+ @zapi_post_uri.path = @zapi_post_uri.path + "/log/api/v2/tmpost"
207
+ @zapi_ingest_uri = @zapi_uri.clone
208
+ @zapi_ingest_uri.path = @zapi_ingest_uri.path + "/log/api/v2/ingest"
209
+ @zapi_support_uri = @zapi_uri.clone
210
+ @zapi_support_uri.path = "/api/v2/support"
211
+ @auth_token = conf["ze_log_collector_token"]
212
+ log.info("ze_log_collector_vers=" + $ZLOG_COLLECTOR_VERSION)
213
+ log.info("ze_log_collector_type=" + @ze_log_collector_type)
214
+ log.info("ze_deployment_name=" + (conf["ze_deployment_name"].nil? ? "<not set>": conf["ze_deployment_name"]))
215
+ log.info("log_collector_url=" + conf["ze_log_collector_url"])
216
+ log.info("etc_hostname=" + @etc_hostname)
217
+ log.info("ze_forward_tag=" + @ze_forward_tag)
218
+ log.info("ze_path_map_file=" + @ze_path_map_file)
219
+ log.info("ze_host_in_logpath=#{@ze_host_in_logpath}")
220
+ log.info("ze_ns_svcgrp_map_file=" + @ze_ns_svcgrp_map_file)
221
+ data = {}
222
+ data['msg'] = "log collector starting"
223
+ send_support_data(data)
224
+ end
225
+
226
+ # def format(tag, time, record)
227
+ # record = inject_values_to_record(tag, time, record)
228
+ # @formatter.format(tag, time, record).chomp + "\n"
229
+ # end
230
+
231
+ def read_ns_to_svcgrp_mappings()
232
+ if ze_ns_svcgrp_map_file.length() == 0
233
+ @ns_to_svcgrp_mappings.active = false
234
+ return
235
+ end
236
+ ns_svcgrp_map_file = @ze_ns_svcgrp_map_file
237
+ if not File.exist?(ns_svcgrp_map_file)
238
+ log.info(ns_svcgrp_map_file + " ns_svcgrp_map_file does not exist.")
239
+ @ns_to_svcgrp_mappings.active = false
240
+ return
241
+ end
242
+ @ns_to_svcgrp_mappings.active = true
243
+ nsj = ""
244
+ log.info(ns_svcgrp_map_file + " exists, loading namespace to svcgrp maps")
245
+ file = File.read(ns_svcgrp_map_file)
246
+ begin
247
+ nsj = JSON.parse(file)
248
+ rescue Exception => e
249
+ log.error(ns_svcgrp_map_file + " does not appear to contain valid JSON: " + e.message)
250
+ @ns_to_svcgrp_mappings.active = false
251
+ return
252
+ end
253
+ log.info(nsj)
254
+ nsj.each { |key, value|
255
+ if( value != "" )
256
+ @ns_to_svcgrp_mappings.svcgrps.store(key, value)
257
+ end
258
+ }
259
+ if @ns_to_svcgrp_mappings.svcgrps.length() == 0
260
+ log.error("No ns/svcgrp mappings are defined in "+ns_svcgrp_map_file)
261
+ @ns_to_svcgrp_mappings.active = false
262
+ end
263
+ end
264
+
265
+ def read_path_mappings()
266
+ if ze_path_map_file.length() == 0
267
+ return
268
+ end
269
+ path_map_cfg_file = @ze_path_map_file
270
+ if not File.exist?(path_map_cfg_file)
271
+ log.info(path_map_cfg_file + " does not exist.")
272
+ @path_mappings.active = false
273
+ return
274
+ end
275
+ @path_mappings.active = true
276
+ pmj = ""
277
+ log.info(path_map_cfg_file + " exists, loading path maps")
278
+ file = File.read(path_map_cfg_file)
279
+ begin
280
+ pmj = JSON.parse(file)
281
+ rescue
282
+ log.error(path_map_cfg_file+" does not appear to contain valid JSON")
283
+ @path_mappings.active = false
284
+ return
285
+ end
286
+ log.info(pmj)
287
+ pmj['mappings'].each { |key, value|
288
+ if key == 'patterns'
289
+ # patterns
290
+ value.each { |pattern|
291
+ begin
292
+ re = Regexp.compile(pattern, Regexp::EXTENDED)
293
+ @path_mappings.patterns.append(re)
294
+ rescue
295
+ log.error("Invalid path pattern '" + pattern + "' detected")
296
+ end
297
+ }
298
+ elsif key == 'ids'
299
+ # ids
300
+ value.each { |id|
301
+ @path_mappings.ids.store(id, id)
302
+ }
303
+ elsif key == 'configs'
304
+ # configs
305
+ value.each { |config|
306
+ @path_mappings.cfgs.store(config, config)
307
+ }
308
+ elsif key == 'tags'
309
+ # tags
310
+ value.each { |tag|
311
+ log.info(@path_mappings.tags)
312
+ @path_mappings.tags.store(tag, tag)
313
+ }
314
+ else
315
+ log.error("Invalid JSON key '"+key+"' detected")
316
+ end
317
+ }
318
+ if @path_mappings.patterns.length() == 0
319
+ log.info("No patterns are defined in "+path_map_cfg_file)
320
+ @path_mappings.active = false
321
+ elsif @path_mappings.ids.length() == 0 and
322
+ @path_mappings.cfgs.length() == 0 and
323
+ @path_mappings.tags.length() == 0
324
+ log.error("No ids/configs/tag mappings are defined in "+path_map_cfg_file)
325
+ @path_mappings.active = false
326
+ end
327
+
328
+ end
329
+
330
+ def read_file_mappings()
331
+ file_map_cfg_file = "/etc/td-agent/log-file-map.conf"
332
+ if not File.exist?(file_map_cfg_file)
333
+ log.info(file_map_cfg_file + " does not exist")
334
+ old_file_map_cfg_file = "/etc/zebrium/log-file-map.cfg"
335
+ if not File.exist?(old_file_map_cfg_file)
336
+ log.info(old_file_map_cfg_file + " does not exist")
337
+ return
338
+ end
339
+ log.warn(old_file_map_cfg_file + " is obsolete, please move it to " + file_map_cfg_file)
340
+ file_map_cfg_file = old_file_map_cfg_file
341
+ end
342
+ log.info(file_map_cfg_file + " exists")
343
+ file = File.read(file_map_cfg_file)
344
+ file_mappings = JSON.parse(file)
345
+
346
+ file_mappings['mappings'].each { |item|
347
+ if item.key?('file') and item['file'].length > 0 and item.key?('alias') and item['alias'].length > 0
348
+ if item['file'].index(',')
349
+ log.warn(item['file'] + " in " + file_map_cfg_file + " has comma, alias mapping must be one-to-one mapping ")
350
+ next
351
+ end
352
+ if item['file'].index('*')
353
+ log.warn(item['file'] + " in " + file_map_cfg_file + " has *, alias mapping must be one-to-one mapping ")
354
+ next
355
+ end
356
+ log.info("Adding mapping " + item['file'] + " => " + item['alias'])
357
+ @file_mappings[item['file']] = item['alias']
358
+ end
359
+ }
360
+ end
361
+
362
+ def map_path_ids(tailed_path, ids, cfgs, tags)
363
+ if not @path_mappings.active
364
+ return
365
+ end
366
+ @path_mappings.patterns.each { |re|
367
+ res = re.match(tailed_path)
368
+ if res
369
+ captures = res.named_captures
370
+ captures.each { |key, value|
371
+ if @path_mappings.ids[key] != nil
372
+ ids[key] = value
373
+ end
374
+ if @path_mappings.cfgs[key] != nil
375
+ cfgs[key] = value
376
+ end
377
+ if @path_mappings.tags[key] != nil
378
+ tags[key] = value
379
+ end
380
+ }
381
+ end
382
+ }
383
+ end
384
+
385
+ def get_ec2_host_meta_data()
386
+ host_meta = {}
387
+ token = ""
388
+ client = HTTPClient.new()
389
+ client.connect_timeout = @ec2_api_client_timeout_secs
390
+ begin
391
+ log.info("Getting ec2 api token")
392
+ resp = client.put('http://169.254.169.254/latest/api/token', :header => {'X-aws-ec2-metadata-token-ttl-seconds' => '21600'})
393
+ if resp.ok?
394
+ token = resp.body
395
+ log.info("Got ec2 host meta token=")
396
+ else
397
+ log.info("Failed to get AWS EC2 host meta data API token")
398
+ end
399
+ rescue
400
+ log.info("Exception: failed to get AWS EC2 host meta data API token")
401
+ return host_meta
402
+ end
403
+
404
+ begin
405
+ log.info("Calling ec2 instance meta data API")
406
+ meta_resp = client.get('http://169.254.169.254/latest/meta-data/', :header => {'X-aws-ec2-metadata-token' => token})
407
+ log.info("Returned from c2 instance meta call")
408
+ if meta_resp.ok?
409
+ meta_data_arr = meta_resp.body.split()
410
+ for k in ['ami-id', 'instance-id', 'instance-type', 'hostname', 'local-hostname', 'local-ipv4', 'mac', 'placement', 'public-hostname', 'public-ipv4'] do
411
+ if meta_data_arr.include?(k)
412
+ data_resp = client.get("http://169.254.169.254/latest/meta-data/" + k, :header => {'X-aws-ec2-metadata-token' => token})
413
+ if data_resp.ok?
414
+ log.info("#{k}=#{data_resp.body}")
415
+ host_meta['ec2-' + k] = data_resp.body
416
+ else
417
+ log.error("Failed to get meta data with key #{k}")
418
+ end
419
+ end
420
+ end
421
+ else
422
+ log.error("host meta data request failed: #{meta_resp}")
423
+ end
424
+ rescue
425
+ log.error("host meta data post request exception")
426
+ end
427
+ return host_meta
428
+ end
429
+
430
+ def get_host()
431
+ host = @k8s_hostname.empty? ? @etc_hostname : @k8s_hostname
432
+ unless @ze_tags["ze_tag_node"].nil? or @ze_tags["ze_tag_node"].empty?
433
+ host = @ze_tags["ze_tag_node"]
434
+ end
435
+ return host
436
+ end
437
+
438
+ def get_container_meta_data(container_id)
439
+ meta_data = {}
440
+ begin
441
+ container = Docker::Container.get(container_id)
442
+ json = container.json()
443
+ meta_data['name'] = json['Name'].sub(/^\//, '')
444
+ meta_data['image'] = json['Config']['Image']
445
+ meta_data['labels'] = json['Config']['Labels']
446
+ return meta_data
447
+ rescue
448
+ log.info("Exception: failed to get container (#{container_id} meta data")
449
+ return nil
450
+ end
451
+ end
452
+
453
+ # save kubernetes configues, related to a specifc pod_id for
454
+ # potential use later for container file-based logs
455
+ def save_kubernetes_cfgs(cfgs)
456
+ if (not cfgs.key?("pod_id")) or cfgs.fetch("pod_id").nil?
457
+ return
458
+ end
459
+ pod_id = cfgs["pod_id"]
460
+ if @pod_configs.cfgs.key?(pod_id)
461
+ pod_cfg = @pod_configs.cfgs[pod_id]
462
+ else
463
+ pod_cfg = PodConfig.new()
464
+ end
465
+ pod_cfg.atime = Time.now()
466
+ # Select which config keys to save.
467
+ keys = [ "cmdb_name", "namespace_name", "namespace_id", "container_name", "pod_name" ]
468
+ for k in keys do
469
+ if cfgs.key?(k) and not cfgs.fetch(k).nil?
470
+ pod_cfg.cfgs[k] = cfgs[k]
471
+ end
472
+ end
473
+ @pod_configs.cfgs[pod_id]=pod_cfg
474
+ end
475
+
476
+ # If the current configuration has a pod_id matching one of the
477
+ # previously stored ones any associated k8s config info will be
478
+ # added.
479
+ def add_kubernetes_cfgs_for_pod_id(in_cfgs)
480
+ if (not in_cfgs.key?("pod_id")) or in_cfgs.fetch("pod_id").nil?
481
+ return in_cfgs
482
+ end
483
+ pod_id = in_cfgs["pod_id"]
484
+
485
+ if not @pod_configs.cfgs.key?(pod_id)
486
+ return in_cfgs
487
+ end
488
+ pod_cfgs = @pod_configs.cfgs(pod_id)
489
+
490
+ # Ruby times are UNIX time in seconds. Toss this if unused for
491
+ # 10 minutes as it may be outdated
492
+ if Time.now() - pod_cfgs.atime > 60*10
493
+ @pod_configs.cfgs.delete(pod_id)
494
+ # while paying the cost, do a quick check for old entries
495
+ @pod_configs.cfgs.each do |pod_id, cfg|
496
+ if Time.now() - cfg.atime > 60*10
497
+ @pod_configs.cfgs.delete(pod_id)
498
+ break
499
+ end
500
+ end
501
+ return in_cfgs
502
+ end
503
+
504
+ pod_cfgs.atime = Time.now()
505
+ pod_cfgs.cfgs.each do |key, value|
506
+ in_cfgs[key] = value
507
+ end
508
+ return in_cfgs
509
+ end
510
+
511
+ def get_request_headers(chunk_tag, record)
512
+ headers = {}
513
+ ids = {}
514
+ cfgs = {}
515
+ tags = {}
516
+
517
+ # Sometimes 'record' appears to be a simple number, which causes an exception when
518
+ # used as a hash. Until the underlying issue is addressed detect this and log.
519
+ if record.class.name != "Hash" or not record.respond_to?(:key?)
520
+ log.error("Record is not a hash, unable to process (class: ${record.class.name}).")
521
+ return false, nil, nil
522
+ end
523
+
524
+ if record.key?("docker") and not record.fetch("docker").nil?
525
+ container_id = record["docker"]["container_id"]
526
+ if record.key?("kubernetes") and not record.fetch("kubernetes").nil?
527
+ cfgs["container_id"] = container_id
528
+ else
529
+ ids["container_id"] = container_id
530
+ end
531
+ end
532
+
533
+ is_container_log = true
534
+ log_type = ""
535
+ forwarded_log = false
536
+ user_mapping = false
537
+ fpath = ""
538
+ override_deployment = ""
539
+ override_deployment_from_ns_svcgrp_map = ""
540
+
541
+ record_host = ""
542
+ if record.key?("host") and not record["host"].empty?
543
+ record_host = record["host"]
544
+ end
545
+ has_container_keys = false
546
+ if record.key?("container_id") and record.key?("container_name")
547
+ has_container_keys = true
548
+ end
549
+ if chunk_tag =~ /^sysloghost\./ or chunk_tag =~ /^#{ze_forward_tag}\./
550
+ if record_host.empty? and ze_host_in_logpath > 0 and record.key?("tailed_path")
551
+ tailed_path = record["tailed_path"]
552
+ path_components = tailed_path.split("/")
553
+ if path_components.length() < ze_host_in_logpath
554
+ log.info("Cannot find host at index #{ze_host_in_logpath} in '#{tailed_path}'")
555
+ else
556
+ # note .split has empty first element from initial '/'
557
+ record_host = path_components[ze_host_in_logpath]
558
+ end
559
+ end
560
+ log_type = "syslog"
561
+ forwarded_log = true
562
+ logbasename = "syslog"
563
+ ids["app"] = logbasename
564
+ ids["host"] = record_host
565
+ is_container_log = false
566
+ elsif record.key?("kubernetes") and not record.fetch("kubernetes").nil?
567
+ kubernetes = record["kubernetes"]
568
+ if kubernetes.key?("namespace_name") and not kubernetes.fetch("namespace_name").nil?
569
+ namespace = kubernetes.fetch("namespace_name")
570
+ if namespace.casecmp?("orphaned") or namespace.casecmp?(".orphaned")
571
+ return false, nil, nil
572
+ end
573
+ end
574
+ fpath = kubernetes["container_name"]
575
+ keys = [ "namespace_name", "host", "container_name" ]
576
+ for k in keys do
577
+ if kubernetes.key?(k) and not kubernetes.fetch(k).nil?
578
+ ids[k] = kubernetes[k]
579
+ if k == "host" and @k8s_hostname.empty?
580
+ @k8s_hostname = kubernetes[k]
581
+ end
582
+ # Requirement for ZS-2185 add cmdb_role, based on namespace_name
583
+ if k == "namespace_name"
584
+ cfgs["cmdb_role"] = kubernetes[k].gsub("-","_")
585
+ if @ns_to_svcgrp_mappings.active
586
+ if @ns_to_svcgrp_mappings.svcgrps.key?(kubernetes[k]) and not @ns_to_svcgrp_mappings.svcgrps.fetch(kubernetes[k]).nil?
587
+ override_deployment_from_ns_svcgrp_map = @ns_to_svcgrp_mappings.svcgrps[kubernetes[k]]
588
+ end
589
+ end
590
+ end
591
+ end
592
+ end
593
+
594
+ for pattern in [ @pod_name_to_deployment_name_regexp_long_compiled, @pod_name_to_deployment_name_regexp_short_compiled ] do
595
+ match_data = kubernetes["pod_name"].match(pattern)
596
+ if match_data
597
+ ids["deployment_name"] = match_data["deployment_name"]
598
+ break
599
+ end
600
+ end
601
+ keys = [ "namespace_id", "container_name", "pod_name", "pod_id", "container_image", "container_image_id" ]
602
+ for k in keys do
603
+ if kubernetes.key?(k) and not kubernetes.fetch(k).nil?
604
+ cfgs[k] = kubernetes[k]
605
+ end
606
+ end
607
+ unless kubernetes["labels"].nil?
608
+ cfgs.merge!(kubernetes["labels"])
609
+ end
610
+ # At this point k8s config should be set. Save these so a subsequent file-log
611
+ # record for the same pod_id can use them.
612
+ save_kubernetes_cfgs(cfgs)
613
+ unless kubernetes["namespace_annotations"].nil?
614
+ tags = kubernetes["namespace_annotations"]
615
+ for t in tags.keys
616
+ if t == "zebrium.com/ze_service_group" and not tags[t].empty?
617
+ override_deployment = tags[t]
618
+ end
619
+ end
620
+ end
621
+
622
+ unless kubernetes["annotations"].nil?
623
+ tags = kubernetes["annotations"]
624
+ for t in tags.keys
625
+ if t == "zebrium.com/ze_logtype" and not tags[t].empty?
626
+ user_mapping = true
627
+ logbasename = tags[t]
628
+ end
629
+ if t == "zebrium.com/ze_service_group" and not tags[t].empty?
630
+ override_deployment = tags[t]
631
+ end
632
+ end
633
+ end
634
+
635
+ unless kubernetes["labels"].nil?
636
+ for k in kubernetes["labels"].keys
637
+ if k == "zebrium.com/ze_logtype" and not kubernetes["labels"][k].empty?
638
+ user_mapping = true
639
+ logbasename = kubernetes["labels"][k]
640
+ end
641
+ if k == "zebrium.com/ze_service_group" and not kubernetes["labels"][k].empty?
642
+ override_deployment = kubernetes["labels"][k]
643
+ end
644
+ end
645
+ end
646
+ if not user_mapping
647
+ logbasename = kubernetes["container_name"]
648
+ end
649
+ elsif chunk_tag =~ /^containers\./
650
+ if record.key?("tailed_path")
651
+ fpath = record["tailed_path"]
652
+ fname = File.basename(fpath)
653
+ ary = fname.split('-')
654
+ container_id = ""
655
+ if ary.length == 2
656
+ container_id = ary[0]
657
+ cm = get_container_meta_data(container_id)
658
+ if cm.nil?
659
+ return false, headers, nil
660
+ end
661
+ cfgs["container_id"] = container_id
662
+ cfgs["container_name"] = cm['name']
663
+ labels = cm['labels']
664
+ for k in labels.keys do
665
+ cfgs[k] = labels[k]
666
+ if k == "zebrium.com/ze_logtype" and not labels[k].empty?
667
+ user_mapping = true
668
+ logbasename = labels[k]
669
+ end
670
+ if k == "zebrium.com/ze_service_group" and not labels[k].empty?
671
+ override_deployment = labels[k]
672
+ end
673
+ end
674
+ if not user_mapping
675
+ logbasename = cm['name']
676
+ end
677
+ ids["app"] = logbasename
678
+ cfgs["image"] = cm['image']
679
+ else
680
+ log.error("Wrong container log file: ", fpath)
681
+ end
682
+ else
683
+ log.error("Missing tailed_path on logs with containers.* tag")
684
+ end
685
+ elsif has_container_keys
686
+ logbasename = record['container_name'].sub(/^\//, '')
687
+ ids["app"] = logbasename
688
+ cfgs["container_id"] = record['container_id']
689
+ cfgs["container_name"] = logbasename
690
+ else
691
+ is_container_log = false
692
+ if record.key?("tailed_path")
693
+ fpath = record["tailed_path"]
694
+ fbname = File.basename(fpath, ".*")
695
+ if @file_mappings.key?(fpath)
696
+ logbasename = @file_mappings[fpath]
697
+ user_mapping = true
698
+ ids["ze_logname"] = fbname
699
+ else
700
+ logbasename = fbname.split('.')[0]
701
+ if logbasename != fbname
702
+ ids["ze_logname"] = fbname
703
+ end
704
+ end
705
+ elsif record.key?("_SYSTEMD_UNIT")
706
+ logbasename = record["_SYSTEMD_UNIT"].gsub(/\.service$/, '')
707
+ elsif chunk_tag =~ /^k8s\.events/
708
+ logbasename = "zk8s-events"
709
+ elsif chunk_tag =~ /^ztcp\.events\./
710
+ ids["host"] = record_host.empty? ? "ztcp_host": record["host"]
711
+ logbasename = record["logbasename"] ? record["logbasename"] : "ztcp_stream"
712
+ forwarded_log = true
713
+ log_type = "tcp_forward"
714
+ elsif chunk_tag =~ /^zhttp\.events\./
715
+ ids["host"] = record_host.empty? ? "ztttp_host" : record["host"]
716
+ logbasename = record["logbasename"] ? record["logbasename"] : "zhttp_stream"
717
+ forwarded_log = true
718
+ log_type = "http_forward"
719
+ else
720
+ # Default goes to zlog-collector. Usually there are fluentd generated message
721
+ # and our own log messages
722
+ # for these generic messages, we will send as json messages
723
+ return true, {}, nil
724
+ end
725
+ ids["app"] = logbasename
726
+ end
727
+ cfgs["ze_file_path"] = fpath
728
+ if not ids.key?("host") or ids.fetch("host").nil?
729
+ if record_host.empty?
730
+ ids["host"] = get_host()
731
+ else
732
+ ids["host"] = record_host
733
+ end
734
+ end
735
+ unless @ze_deployment_name.empty?
736
+ ids["ze_deployment_name"] = @ze_deployment_name
737
+ end
738
+ unless override_deployment_from_ns_svcgrp_map.empty?
739
+ log.debug("Updating ze_deployment_name ns_svcgrp_map '#{override_deployment_from_ns_svcgrp_map}'")
740
+ ids["ze_deployment_name"] = override_deployment_from_ns_svcgrp_map
741
+ end
742
+ unless override_deployment.empty?
743
+ log.debug("Updating ze_deployment_name to '#{override_deployment}'")
744
+ ids["ze_deployment_name"] = override_deployment
745
+ end
746
+ for k in @ze_tags.keys do
747
+ tags[k] = @ze_tags[k]
748
+ end
749
+ tags["fluentd_tag"] = chunk_tag
750
+
751
+ id_key = ""
752
+ keys = ids.keys.sort
753
+ keys.each do |k|
754
+ if ids.key?(k)
755
+ if id_key.empty?
756
+ id_key = k + "=" + ids[k]
757
+ else
758
+ id_key = id_key + "," + k + "=" + ids[k]
759
+ end
760
+ end
761
+ end
762
+
763
+ if record.key?("tailed_path")
764
+ map_path_ids(record["tailed_path"], ids, cfgs, tags)
765
+ add_kubernetes_cfgs_for_pod_id(cfgs)
766
+ end
767
+
768
+ # host should be handled as a config element instead of an id.
769
+ # This is used when host changes frequently, causing issues with
770
+ # detection. The actual host is stored in the cfgs metadata, and
771
+ # a constant is stored in the ids metadata.
772
+ # Note that a host entry must be present in ids for correct backend
773
+ # processing, it is simply a constant at this point.
774
+ if ze_handle_host_as_config && ids.key?("host")
775
+ cfgs["host"] = ids["host"]
776
+ ids["host"] = "host_in_config"
777
+ end
778
+
779
+ has_stream_token = false
780
+ if @stream_tokens.key?(id_key)
781
+ # Make sure there is no meta data change. If there is change, new stream token
782
+ # must be requested.
783
+ cfgs_tags_match = true
784
+ if (cfgs.length == @stream_tokens[id_key]['cfgs'].length &&
785
+ tags.length == @stream_tokens[id_key]['tags'].length)
786
+ @stream_tokens[id_key]['cfgs'].keys.each do |k|
787
+ old_cfg = @stream_tokens[id_key]['cfgs'][k]
788
+ if old_cfg != cfgs[k]
789
+ log.info("Stream " + id_key + " config has changed: old " + old_cfg + ", new " + cfgs[k])
790
+ cfgs_tags_match = false
791
+ break
792
+ end
793
+ end
794
+ @stream_tokens[id_key]['tags'].keys.each do |k|
795
+ old_tag = @stream_tokens[id_key]['tags'][k]
796
+ if old_tag != tags[k]
797
+ log.info("Stream " + id_key + " config has changed: old " + old_tag + ", new " + tags[k])
798
+ cfgs_tags_match = false
799
+ break
800
+ end
801
+ end
802
+ else
803
+ log.info("Stream " + id_key + " number of config or tag has changed")
804
+ cfgs_tags_match = false
805
+ end
806
+ if cfgs_tags_match
807
+ has_stream_token = true
808
+ end
809
+ end
810
+
811
+ if has_stream_token
812
+ stream_token = @stream_tokens[id_key]["token"]
813
+ else
814
+ log.info("Request new stream token with key " + id_key)
815
+ stream_token = get_stream_token(ids, cfgs, tags, logbasename, is_container_log, user_mapping,
816
+ log_type, forwarded_log)
817
+ @stream_tokens[id_key] = {
818
+ "token" => stream_token,
819
+ "cfgs" => cfgs,
820
+ "tags" => tags
821
+ }
822
+ end
823
+
824
+ # User can use node label on pod to override "host" meta data from kubernetes
825
+ headers["authtoken"] = stream_token
826
+ headers["Content-Type"] = "application/json"
827
+ headers["Transfer-Encoding"] = "chunked"
828
+ return true, headers, stream_token
829
+ end
830
+
831
+ def get_stream_token(ids, cfgs, tags, logbasename, is_container_log, user_mapping,
832
+ log_type, forwarded_log)
833
+ meta_data = {}
834
+ meta_data['stream'] = "native"
835
+ meta_data['logbasename'] = logbasename
836
+ meta_data['user_logbasename'] = user_mapping
837
+ meta_data['container_log'] = is_container_log
838
+ meta_data['log_type'] = log_type
839
+ meta_data['forwarded_log'] = forwarded_log
840
+ meta_data['ids'] = ids
841
+ meta_data['cfgs'] = cfgs
842
+ meta_data['tags'] = tags
843
+ meta_data['tz'] = @ze_timezone.empty? ? Time.now.zone : @ze_timezone
844
+ meta_data['ze_log_collector_vers'] = $ZLOG_COLLECTOR_VERSION + "-" + @ze_log_collector_type
845
+
846
+ headers = {}
847
+ headers["authtoken"] = @auth_token.to_s
848
+ headers["Content-Type"] = "application/json"
849
+ headers["Transfer-Encoding"] = "chunked"
850
+ @stream_token_req_sent = @stream_token_req_sent + 1
851
+ resp = post_data(@zapi_token_uri, meta_data.to_json, headers)
852
+ if resp.ok? == false
853
+ if resp.code == 401
854
+ raise RuntimeError, "Invalid auth token: #{resp.code} - #{resp.body}"
855
+ else
856
+ raise RuntimeError, "Failed to send data to HTTP Source. #{resp.code} - #{resp.body}"
857
+ end
858
+ else
859
+ @stream_token_req_success = @stream_token_req_success + 1
860
+ end
861
+ parse_resp = JSON.parse(resp.body)
862
+ if parse_resp.key?("token")
863
+ return parse_resp["token"]
864
+ else
865
+ raise RuntimeError, "Failed to get stream token from zapi. #{resp.code} - #{resp.body}"
866
+ end
867
+ end
868
+
869
+ def post_data(uri, data, headers)
870
+ log.trace("post_data to " + uri.to_s + ": headers: " + headers.to_s)
871
+ myio = StringIO.new(data)
872
+ class <<myio
873
+ undef :size
874
+ end
875
+ resp = @http.post(uri, myio, headers)
876
+ resp
877
+ end
878
+
879
+ def get_k8s_event_str(record)
880
+ evt_obj = record['object']
881
+ severity = evt_obj['type']
882
+ if severity == "Warning"
883
+ severity = "WARN"
884
+ end
885
+ if severity == "Normal"
886
+ severity = "INFO"
887
+ end
888
+ evt_str = "count=" + evt_obj['count'].to_s
889
+ if record.key?('type')
890
+ evt_str = evt_str + " type=" + record['type']
891
+ end
892
+ if evt_obj.key?('source') and evt_obj['source'].key('host')
893
+ evt_str = evt_str + " host=" + evt_obj['source']['host']
894
+ end
895
+ if evt_obj.key?('metadata')
896
+ if evt_obj['metadata'].key?('name')
897
+ evt_str = evt_str + " name=" + evt_obj['metadata']['name']
898
+ end
899
+ if evt_obj['metadata'].key('namespace')
900
+ evt_str = evt_str + " namespace=" + evt_obj['metadata']['namespace']
901
+ end
902
+ end
903
+ if evt_obj.key?('involvedObject')
904
+ in_obj = evt_obj['involvedObject']
905
+ for k in ["kind", "namespace", "name", "uid" ] do
906
+ if in_obj.key?(k)
907
+ evt_str = evt_str + " " + k + "=" + in_obj[k]
908
+ end
909
+ end
910
+ end
911
+ if evt_obj.key?('reason')
912
+ evt_str = evt_str + " reason=" + evt_obj['reason']
913
+ end
914
+ # log.info("Event obj:" + evt_obj.to_s)
915
+
916
+ if evt_obj.key?('lastTimestamp') and not evt_obj.fetch('lastTimestamp').nil?
917
+ timeStamp = evt_obj["lastTimestamp"]
918
+ elsif evt_obj.key('eventTime') and not evt_obj.fetch('eventTime').nil?
919
+ timeStamp = evt_obj["eventTime"]
920
+ else
921
+ timeStamp = ''
922
+ end
923
+ msg = timeStamp + " " + severity + " " + evt_str + " msg=" + evt_obj['message'].chomp
924
+ return msg
925
+ end
926
+
927
+ def process(tag, es)
928
+ es = inject_values_to_event_stream(tag, es)
929
+ es.each {|time,record|
930
+ if record.key?("kubernetes") and not record.fetch("kubernetes").nil?
931
+ str = ""
932
+ kubernetes = record["kubernetes"].clone
933
+ container_name = kubernetes["container_name"]
934
+ str = str + "container_name=" + container_name + ","
935
+ host = kubernetes["host"]
936
+ str = str + "host=" + host + ","
937
+ kubernetes["labels"].each do |k, v|
938
+ str = str + "label:" + k + "=" + v + ","
939
+ end
940
+ str = str + "\n"
941
+ end
942
+ }
943
+ end
944
+
945
+ def prepare_support_data()
946
+ data = {}
947
+ data['stream_token_req_sent'] = @stream_token_req_sent
948
+ data['stream_token_req_success'] = @stream_token_req_success
949
+ data['data_post_sent'] = @data_post_sent
950
+ data['data_post_success'] = @data_post_success
951
+ data['support_post_sent'] = @support_post_sent
952
+ data['support_post_success'] = @support_post_success
953
+ return data
954
+ end
955
+
956
+ def post_message_data(send_json, headers, messages)
957
+ @data_post_sent = @data_post_sent + 1
958
+ if send_json
959
+ req = {}
960
+ req['log_type'] = 'generic'
961
+ req['messages'] = messages
962
+ headers = {}
963
+ headers["authtoken"] = @auth_token
964
+ headers["Content-Type"] = "application/json"
965
+ resp = post_data(@zapi_ingest_uri, req.to_json, headers)
966
+ if resp.ok? == false
967
+ log.error("Server ingest API return error: code #{resp.code} - #{resp.body}")
968
+ else
969
+ @data_post_success = @data_post_success + 1
970
+ end
971
+ else
972
+ resp = post_data(@zapi_post_uri, messages.join("\n") + "\n", headers)
973
+ if resp.ok? == false
974
+ if resp.code == 401
975
+ # Our stream token becomes invalid for some reason, have to acquire new one.
976
+ # Usually this only happens in testing when server gets recreated.
977
+ # There is no harm to clear all stream tokens.
978
+ log.error("Server says stream token is invalid: #{resp.code} - #{resp.body}")
979
+ log.error("Delete all stream tokens")
980
+ @stream_tokens = {}
981
+ raise RuntimeError, "Delete stream token, and retry"
982
+ else
983
+ raise RuntimeError, "Failed to send data to HTTP Source. #{resp.code} - #{resp.body}"
984
+ end
985
+ else
986
+ @data_post_success = @data_post_success + 1
987
+ end
988
+ end
989
+ end
990
+
991
+ def write(chunk)
992
+ epoch = Time.now.to_i
993
+ if epoch - @last_support_data_sent > @ze_support_data_send_intvl
994
+ data = prepare_support_data()
995
+ send_support_data(data)
996
+ @last_support_data_sent = epoch
997
+ end
998
+ tag = chunk.metadata.tag
999
+ messages_list = {}
1000
+ log.trace("out_zebrium: write() called tag=", tag)
1001
+
1002
+ headers = {}
1003
+ messages = []
1004
+ num_records = 0
1005
+ send_json = false
1006
+ host = ''
1007
+ meta_data = {}
1008
+ last_stoken = {}
1009
+ last_headers = {}
1010
+ chunk.each do |entry|
1011
+ record = entry[1]
1012
+ if @ze_send_json == false
1013
+ if entry[1].nil?
1014
+ log.warn("nil detected, ignoring remainder of chunk")
1015
+ return
1016
+ end
1017
+ should_send, headers, cur_stoken = get_request_headers(tag, record)
1018
+ if should_send == false
1019
+ return
1020
+ end
1021
+ end
1022
+
1023
+ # get_request_headers() returns empty header, it means
1024
+ # we should send json message to server
1025
+ if headers.empty? or @ze_send_json
1026
+ send_json = true
1027
+ if host.empty?
1028
+ if record.key?("host") and not record["host"].empty?
1029
+ host = record["host"]
1030
+ else
1031
+ host = get_host()
1032
+ end
1033
+ meta_data['collector'] = $ZLOG_COLLECTOR_VERSION
1034
+ meta_data['host'] = host
1035
+ meta_data['ze_deployment_name'] = @ze_deployment_name
1036
+ meta_data['tags'] = @ze_tags.dup
1037
+ meta_data['tags']['fluentd_tag'] = tag
1038
+ end
1039
+ end
1040
+
1041
+ if num_records == 0
1042
+ last_stoken = cur_stoken
1043
+ last_headers = headers
1044
+ elsif last_stoken != cur_stoken
1045
+ log.info("Streamtoken changed in chunk, num_records="+num_records.to_s)
1046
+ post_message_data(send_json, last_headers, messages)
1047
+ messages = []
1048
+ last_stoken = cur_stoken
1049
+ last_headers = headers
1050
+ num_records = 0
1051
+ end
1052
+
1053
+ if entry[0].nil?
1054
+ epoch_ms = (Time.now.strftime('%s.%3N').to_f * 1000).to_i
1055
+ else
1056
+ epoch_ms = (entry[0].to_f * 1000).to_i
1057
+ end
1058
+
1059
+ if send_json
1060
+ m = {}
1061
+ m['meta'] = meta_data
1062
+ m['line'] = record
1063
+ m['line']['timestamp'] = epoch_ms
1064
+ begin
1065
+ json_str = m.to_json
1066
+ rescue Encoding::UndefinedConversionError
1067
+ json_str = m.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
1068
+ end
1069
+ messages.push(json_str)
1070
+ else
1071
+ msg_key = nil
1072
+ if not tag =~ /^k8s\.events/
1073
+ # journald use key "MESSAGE" for log message
1074
+ for k in ["log", "message", "LOG", "MESSAGE" ]
1075
+ if record.key?(k) and not record.fetch(k).nil?
1076
+ msg_key = k
1077
+ break
1078
+ end
1079
+ end
1080
+ if msg_key.nil?
1081
+ next
1082
+ end
1083
+ end
1084
+
1085
+ if tag =~ /^k8s\.events/ and record.key?('object') and record['object']['kind'] == "Event"
1086
+ line = "ze_tm=" + epoch_ms.to_s + ",msg=" + get_k8s_event_str(record)
1087
+ else
1088
+ line = "ze_tm=" + epoch_ms.to_s + ",msg=" + record[msg_key].chomp
1089
+ end
1090
+ messages.push(line)
1091
+ end
1092
+ num_records += 1
1093
+ end
1094
+ # Post remaining messages, if any
1095
+ if num_records == 0
1096
+ log.trace("Chunk has no record, no data to post")
1097
+ return
1098
+ end
1099
+ post_message_data(send_json, headers, messages)
1100
+ end
1101
+
1102
+ def send_support_data(data)
1103
+ meta_data = {}
1104
+ meta_data['collector_vers'] = $ZLOG_COLLECTOR_VERSION
1105
+ meta_data['host'] = @etc_hostname
1106
+ meta_data['data'] = data
1107
+
1108
+ headers = {}
1109
+ headers["Authorization"] = "Token " + @auth_token.to_s
1110
+ headers["Content-Type"] = "application/json"
1111
+ headers["Transfer-Encoding"] = "chunked"
1112
+ @support_post_sent = @support_post_sent + 1
1113
+ resp = post_data(@zapi_support_uri, meta_data.to_json, headers)
1114
+ if resp.ok? == false
1115
+ log.error("Failed to send data to HTTP Source. #{resp.code} - #{resp.body}")
1116
+ else
1117
+ @support_post_success = @support_post_success + 1
1118
+ end
1119
+ end
1120
+
1121
+ # This method is called when starting.
1122
+ def start
1123
+ super
1124
+ end
1125
+
1126
+ # This method is called when shutting down.
1127
+ def shutdown
1128
+ super
1129
+ end
1130
+
1131
+ end