fluent-plugin-zebrium_output 1.57.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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