zuora_observability 0.1.0.pre.b → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3c6f15e5345dd32d8ada993579cf90efa2a2382573fe86f608f598be3f69f9e
4
- data.tar.gz: 38035565943c737e2f5ac59032780b6ce0c9e2eed0d039870d9b31b4780f7629
3
+ metadata.gz: 52240e5716643d75479140930fd29ed88b154af9c01511fcceea92338b399ae2
4
+ data.tar.gz: 20fa0460d7e665ee83b6265f69d928ed74b90fef9231e7211dac287d0e0475f0
5
5
  SHA512:
6
- metadata.gz: c51373a4c25bebde5158c57e035a4363895f67c6f991fc7dbcd9a0741496e65ae07d7f6f65da86f8de14c4ef86ea509f0ba110d9fd7e3d29dcaa83e19117bbd6
7
- data.tar.gz: ca248b0270c959804ded1396591e6bacde2abaa2f478ea3b12c2a8265716562ddf0f8f87ddbd90f1ba228657568c337258936faaccebb9540dcbdff59e2903f9
6
+ metadata.gz: 0246f0a9836504bbd5fb8ed0e0ec909664a6848cca592a4756c47dff3153eeb763e3f7e179fe031fe65161b9bf01ffbbc396f995738501f625dcbdf9b824436e
7
+ data.tar.gz: b7da319f502643521f8b229e2f33b889d9fbb6823b38b948b51d38f073f348d59affc8f84fca734635294627225766ab1ea7f2d49ac91b7b71a71c2b9e396518
data/README.md CHANGED
@@ -2,25 +2,10 @@
2
2
 
3
3
  A ruby gem to enable observability into rails applications
4
4
 
5
- ## Usage
6
-
7
- ### Configuration
8
-
9
- Observability can be configured through an initializer as shown below.
5
+ ## Prerequisites
10
6
 
11
- ```ruby
12
- # config/initializers/zuora_observability.rb
13
- ZuoraObservability.configure do |config|
14
- config.enable_metrics = true
15
- end
16
- ```
17
-
18
- | Configuration Field | Default Value | Description |
19
- | ------------------- | ---------------------------------------------------------------- | ------------------------------------ |
20
- | `enable_metrics` | `false` | Enables writing metrics to telegraf |
21
- | `telegraf_endpoint` | `'udp://telegraf-app-metrics.monitoring.svc.cluster.local:8094'` | The URL to send telegraf metrics to |
22
- | `telegraf_debug` | `false` | Log info about data sent to telegraf |
23
- | `json_logging` | `false` in development/test, `true` otherwise | Use structured JSON logging for ZuoraObservability::Logger. **This configuration option cannot be changed in an initializer, but can be set to `true` with the presence of `ZUORA_JSON_LOGGING` environment variable** |
7
+ - Ruby 2.5+
8
+ - Rails 5.0+
24
9
 
25
10
  ## Installation
26
11
 
@@ -42,6 +27,10 @@ Or install it yourself as:
42
27
  $ gem install zuora_observability
43
28
  ```
44
29
 
30
+ ## Usage
31
+
32
+ - [Logging](doc/logging.md)
33
+
45
34
  ## License
46
35
 
47
36
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  if defined?(Resque.logger)
4
- Resque.logger = ZuoraObservability::Logger.custom_logger(name: 'Resque', type: 'Monologger', level: MonoLogger::INFO)
4
+ Resque.logger = ZuoraObservability::Logger.custom_logger(name: 'Resque')
5
5
  if defined?(Resque::Scheduler)
6
6
  Resque::Scheduler.logger = ZuoraObservability::Logger.custom_logger(name: 'ResqueScheduler')
7
7
  end
8
8
  end
9
9
 
10
10
  if defined?(Delayed::Worker.logger)
11
- Delayed::Worker.logger = ZuoraObservability::Logger.custom_logger(
12
- name: 'DelayedJob', type: 'Monologger', level: MonoLogger::INFO
13
- )
11
+ Delayed::Worker.logger = ZuoraObservability::Logger.custom_logger(name: 'DelayedJob', level: Logger::INFO)
14
12
  end
15
13
 
16
14
  if defined?(Makara)
@@ -18,9 +16,9 @@ if defined?(Makara)
18
16
  end
19
17
 
20
18
  if defined?(ElasticAPM) && ElasticAPM.running?
21
- ElasticAPM.agent.config.logger = ZuoraObservability::Logger.custom_logger(name: 'ElasticAPM', level: MonoLogger::WARN)
19
+ ElasticAPM.agent.config.logger = ZuoraObservability::Logger.custom_logger(name: 'ElasticAPM', level: Logger::WARN)
22
20
  end
23
21
 
24
22
  if defined?(ActionMailer)
25
- ActionMailer::Base.logger = ZuoraObservability::Logger.custom_logger(name: 'ActionMailer', type: 'Monologger')
23
+ ActionMailer::Base.logger = ZuoraObservability::Logger.custom_logger(name: 'ActionMailer')
26
24
  end
@@ -7,8 +7,6 @@ require 'zuora_observability/metrics'
7
7
  require 'zuora_observability/logger'
8
8
 
9
9
  require 'zuora_observability/logging/formatter'
10
- require 'zuora_observability/metrics/telegraf'
11
- require 'zuora_observability/metrics/point_value'
12
10
 
13
11
  # Provides Rails application with tools for observabilty
14
12
  module ZuoraObservability
@@ -3,16 +3,19 @@
3
3
  module ZuoraObservability
4
4
  # Global configuration that can be set in a Rails initializer
5
5
  class Configuration
6
- attr_accessor :enable_metrics, :telegraf_endpoint, :telegraf_debug,
7
- :json_logging
6
+ attr_reader :custom_payload_hooks
7
+ attr_accessor :zecs_service_hook, :json_logging
8
8
 
9
9
  def initialize
10
- @enable_metrics = false
11
- @telegraf_endpoint = 'udp://telegraf-app-metrics.monitoring.svc.cluster.local:8094'
12
- @telegraf_debug = false
10
+ @custom_payload_hooks = []
11
+ @zecs_service_hook = nil
13
12
  # NOTE(hartley): this checks the var for presence rather than value to
14
13
  # align with how Rails does RAILS_LOG_TO_STDOUT
15
14
  @json_logging = ENV['ZUORA_JSON_LOGGING'].present? ? true : !(Rails.env.development? || Rails.env.test?)
16
15
  end
16
+
17
+ def add_custom_payload_hook(&block)
18
+ custom_payload_hooks << block
19
+ end
17
20
  end
18
21
  end
@@ -1,93 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'zuora_observability/metrics/page_request'
4
-
5
3
  module ZuoraObservability
6
4
  # The ZuoraObservability Engine is mounted to hook into Rails
7
5
  class Engine < ::Rails::Engine
8
6
  isolate_namespace ZuoraObservability
9
7
 
10
- REQUEST_HEADERS_TO_IGNORE = %w[
11
- RAW_POST_DATA
12
- REQUEST_METHOD
13
- REQUEST_URI
14
- REQUEST_PATH
15
- PATH_INFO
16
- CONTENT_TYPE
17
- ORIGINAL_FULLPATH
18
- QUERY_STRING
19
- ].freeze
20
-
21
8
  config.generators do |g|
22
9
  g.test_framework :rspec
23
10
  end
24
11
 
25
- # hook to process_action
26
- ActiveSupport::Notifications.subscribe(
27
- 'process_action.action_controller',
28
- ZuoraObservability::Metrics::PageRequest.new
29
- )
30
-
31
12
  initializer(:rails_stdout_logging, before: :initialize_logger) do
32
- require 'lograge'
13
+ Rails.application.configure do
14
+ config.logger = ZuoraObservability::Logger.custom_logger(name: 'Rails')
33
15
 
34
- Rails.configuration.logger = ZuoraObservability::Logger.custom_logger(name: "Rails")
16
+ next unless ZuoraObservability.configuration.json_logging
35
17
 
36
- if ZuoraObservability.configuration.json_logging
37
- Rails.configuration.lograge.enabled = true
38
- Rails.configuration.colorize_logging = false
39
- end
18
+ require 'lograge'
19
+ require 'zuora_observability/logging/custom_options'
40
20
 
41
- if Rails.configuration.lograge.enabled
42
- if Rails.configuration.logger.class.to_s == 'Ougai::Logger'
43
- Rails.configuration.lograge.formatter = Class.new do |fmt|
44
- def fmt.call(data)
45
- { msg: 'Rails Request', request: data }
46
- end
47
- end
48
- end
49
- #Rails.configuration.lograge.formatter = Lograge::Formatters::Json.new
50
- Rails.configuration.lograge.custom_options = lambda do |event|
51
- exceptions = %w(controller action format)
52
- items = {
53
- #time: event.time.strftime('%FT%T.%6N'),
54
- params: event.payload[:params].as_json(except: exceptions).to_json.to_s
55
- }
56
- items.merge!({exception_object: event.payload[:exception_object]}) if event.payload[:exception_object].present?
57
- items.merge!({exception: event.payload[:exception]}) if event.payload[:exception].present?
21
+ config.lograge.enabled = true
22
+ config.colorize_logging = false
58
23
 
59
- if event.payload[:headers].present?
60
- # By convertion, headers usually do not have dots. Nginx even rejects headers with dots
61
- # All Rails headers are namespaced, like 'rack.input'.
62
- # Thus, we can obtain the client headers by rejecting dots
63
- request_headers =
64
- event.payload[:headers].env.
65
- reject { |key| key.to_s.include?('.') || REQUEST_HEADERS_TO_IGNORE.include?(key.to_s) }
66
- begin
67
- if request_headers["HTTP_AUTHORIZATION"].present?
68
- if request_headers["HTTP_AUTHORIZATION"].include?("Basic")
69
- user_password = request_headers["HTTP_AUTHORIZATION"].split("Basic").last.strip
70
- user, password = Base64.decode64(user_password).split(":")
71
- request_headers["HTTP_AUTHORIZATION"] = "Basic #{user}:ValueFiltered"
72
- elsif
73
- request_headers["HTTP_AUTHORIZATION"] = "ValueFiltered"
74
- end
75
- end
76
- request_headers["HTTP_API_TOKEN"] = "ValueFiltered" if request_headers["HTTP_API_TOKEN"].present?
77
- rescue
78
- request_headers.delete("HTTP_API_TOKEN")
79
- request_headers.delete("HTTP_AUTHORIZATION")
80
- end
81
- items.merge!({ headers: request_headers.to_s })
82
- end
24
+ config.lograge.formatter = Lograge::Formatters::Raw.new
25
+
26
+ config.lograge.custom_options = Logging::CustomOptions
27
+ config.lograge.custom_payload do |controller|
28
+ payload = {}
83
29
 
84
- if Thread.current[:appinstance].present?
85
- items.merge!({connect_user: Thread.current[:appinstance].connect_user, new_session: Thread.current[:appinstance].new_session_message})
86
- if Thread.current[:appinstance].logitems.present? && Thread.current[:appinstance].logitems.class == Hash
87
- items.merge!(Thread.current[:appinstance].logitems)
88
- end
30
+ ZuoraObservability.configuration.custom_payload_hooks.each do |hook|
31
+ payload.merge!(hook.call(controller))
89
32
  end
90
- return items
33
+
34
+ next payload unless ZuoraObservability.configuration.zecs_service_hook
35
+
36
+ payload[:zecs_service] =
37
+ ZuoraObservability.configuration.zecs_service_hook.call(controller)
38
+
39
+ payload
91
40
  end
92
41
  end
93
42
  end
@@ -4,9 +4,20 @@ module ZuoraObservability
4
4
  # Methods to get information about the application environment
5
5
  class Env
6
6
  class << self
7
+ def name
8
+ ENV['Z_APPLICATION_NAME']
9
+ end
10
+
11
+ def version
12
+ ENV['Z_APPLICATION_VERSION']
13
+ end
14
+
15
+ def environment
16
+ ENV['Z_APPLICATION_ENVIRONMENT']
17
+ end
18
+
7
19
  def app_name
8
- # parent_name is deprecated in Rails 6.0, removed in 6.1
9
- ENV['DEIS_APP'].presence || Rails.application.class.parent_name
20
+ ENV['DEIS_APP'].presence || app_parent_name
10
21
  end
11
22
 
12
23
  def pod_name
@@ -28,6 +39,16 @@ module ZuoraObservability
28
39
  end
29
40
  return p_type
30
41
  end
42
+
43
+ private
44
+
45
+ def app_parent_name
46
+ if Rails::VERSION::MAJOR >= 6 && Rails::VERSION::MINOR >= 1
47
+ Rails.application.class.module_parent_name
48
+ else
49
+ Rails.application.class.parent_name
50
+ end
51
+ end
31
52
  end
32
53
  end
33
54
  end
@@ -1,49 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ougai'
3
4
  require 'mono_logger'
5
+ require 'zuora_observability/logging/formatter'
6
+ require 'ougai/formatters/customizable'
4
7
 
5
8
  module ZuoraObservability
6
9
  # A configurable logger that can be used for Rails and additional libraries
7
- module Logger
8
- # NOTE(hartley): replace this with a subclass of Ougai Logger
9
- # - enable silence method for Rails 5.2 asset requests
10
+ class Logger < Ougai::Logger
11
+ # TODO(hartley): enable silence method for Rails 5.2 asset requests
10
12
  # https://github.com/tilfin/ougai/wiki/Use-as-Rails-logger
11
- # - potentially integrate with MonoLogger so no conditional is needed
12
- # https://github.com/tilfin/ougai/issues/74
13
- def self.custom_logger(name: "", level: Rails.logger.present? ? Rails.logger.level : MonoLogger::INFO, type: :ougai)
14
- #puts name + ' - ' + {Logger::WARN => 'Logger::WARN', Logger::ERROR => 'Logger::ERROR', Logger::DEBUG => 'Logger::DEBUG', Logger::INFO => 'Logger::INFO' }[level] + ' - '
13
+ def initialize(logdev, **)
14
+ super
15
+
16
+ # NOTE(hartley): the purpose for the original split between Ougai and
17
+ # MonoLogger was that MonoLogger enables logging in a trap context
18
+ # https://github.com/tilfin/ougai/issues/74
19
+ # By using our own Logger class, we can override the LogDevice created
20
+ # by ruby with MonoLogger's, enabling logging in a trap context
21
+ @logdev = MonoLogger::LocklessLogDevice.new(logdev)
22
+ end
23
+
24
+ def create_formatter
25
+ return ZuoraObservability::Logging::Formatter.new if ZuoraObservability.configuration.json_logging
26
+
27
+ formatter = Ougai::Formatters::Customizable.new(
28
+ format_err: method(:custom_error_formatter),
29
+ format_data: method(:custom_data_formatter),
30
+ format_msg: method(:custom_message_formatter)
31
+ )
32
+ formatter.datetime_format = '%FT%T.%3NZ'
33
+ formatter
34
+ end
35
+
36
+ def custom_message_formatter(severity, datetime, _progname, data)
37
+ msg = data.delete(:msg)
38
+ "#{severity.ljust(6)} #{datetime}: #{msg}"
39
+ end
40
+
41
+ def custom_data_formatter(data)
42
+ data.delete_if do |k|
43
+ %i[app_instance_id tenant_ids organization environment].include? k
44
+ end
45
+
46
+ return nil if data.blank?
47
+
48
+ "#{'DATA'.ljust(6)} #{Time.current.strftime('%FT%T.%3NZ')}: #{data.to_json}"
49
+ end
50
+
51
+ def custom_error_formatter(data)
52
+ return nil unless data.key?(:err)
53
+
54
+ err = data.delete(:err)
55
+ " #{err[:name]} (#{err[:message]})\n #{err[:stack]}"
56
+ end
57
+
58
+ def self.custom_logger(name: '', level: Rails.logger.present? ? Rails.logger.level : MonoLogger::INFO, type: :ougai)
15
59
  if type == :ougai
16
- require 'ougai'
17
- require "ougai/formatters/customizable"
18
- #logger = Ougai::Logger.new(MonoLogger.new(STDOUT))
19
- logger = Ougai::Logger.new(STDOUT)
20
- logger.level = level
21
- if ZuoraObservability.configuration.json_logging
22
- require 'zuora_observability/logging/formatter'
23
- logger.formatter = ZuoraObservability::Logging::Formatter.new(name)
24
- logger.before_log = lambda do |data|
25
- data[:trace_id] = ZuoraConnect::RequestIdMiddleware.request_id if ZuoraConnect::RequestIdMiddleware.request_id.present?
26
- data[:zuora_trace_id] = ZuoraConnect::RequestIdMiddleware.zuora_request_id if ZuoraConnect::RequestIdMiddleware.zuora_request_id.present?
27
- #data[:traces] = {amazon_id: data[:trace_id], zuora_id: data[:zuora_trace_id]}
28
- end
29
- else
30
- logger.formatter = Ougai::Formatters::Customizable.new(
31
- format_err: proc do |data|
32
- next nil unless data.key?(:err)
33
- err = data.delete(:err)
34
- " #{err[:name]} (#{err[:message]})\n #{err[:stack]}"
35
- end,
36
- format_data: proc do |data|
37
- data.delete(:app_instance_id); data.delete(:tenant_ids); data.delete(:organization); data.delete(:environment)
38
- format('%s %s: %s', 'DATA'.ljust(6), Time.now.strftime('%FT%T.%6NZ'), "#{data.to_json}") if data.present?
39
- end,
40
- format_msg: proc do |severity, datetime, _progname, data|
41
- msg = data.delete(:msg)
42
- format('%s %s: %s', severity.ljust(6), datetime, msg)
43
- end
44
- )
45
- logger.formatter.datetime_format = '%FT%T.%6NZ'
46
- end
60
+ logger = new($stdout, level: level, progname: name)
47
61
  else
48
62
  require 'mono_logger'
49
63
  logger = MonoLogger.new(STDOUT)
@@ -68,7 +82,8 @@ module ZuoraObservability
68
82
  end
69
83
  end
70
84
  end
71
- return logger
85
+
86
+ logger
72
87
  end
73
88
  end
74
89
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZuoraObservability
4
+ module Logging
5
+ # Custom Options for lograge to add additional fields to logged data
6
+ module CustomOptions
7
+ IGNORE_HEADERS = %w[
8
+ RAW_POST_DATA
9
+ REQUEST_METHOD
10
+ REQUEST_URI
11
+ REQUEST_PATH
12
+ PATH_INFO
13
+ CONTENT_TYPE
14
+ ORIGINAL_FULLPATH
15
+ QUERY_STRING
16
+ ].freeze
17
+
18
+ IGNORE_PARAMS = %w[controller action format].freeze
19
+
20
+ FILTERED = '[FILTERED]'
21
+
22
+ class << self
23
+ def call(event)
24
+ items = {
25
+ msg: 'Rails Request',
26
+ params: event.payload[:params].as_json(except: IGNORE_PARAMS).to_s,
27
+ trace_id: event.payload[:headers]['action_dispatch.request_id'],
28
+ zuora_trace_id: event.payload[:headers]['HTTP_ZUORA_REQUEST_ID'],
29
+ error: event.payload[:exception_object]
30
+ }
31
+
32
+ if event.payload[:headers].present?
33
+ request_headers = filter_request_headers(event.payload[:headers])
34
+
35
+ filtered_headers = filter_sensitive_headers(request_headers)
36
+
37
+ items.merge!({ headers: filtered_headers })
38
+ end
39
+
40
+ items
41
+ end
42
+
43
+ private
44
+
45
+ def filter_request_headers(action_dispatch_headers)
46
+ action_dispatch_headers.env.select do |header|
47
+ next false if IGNORE_HEADERS.include? header
48
+
49
+ header =~ /^HTTP_/ || ActionDispatch::Http::Headers::CGI_VARIABLES.include?(header)
50
+ end
51
+ end
52
+
53
+ def filter_sensitive_headers(headers)
54
+ filtered = {}
55
+
56
+ auth = headers['HTTP_AUTHORIZATION']
57
+ filtered['HTTP_AUTHORIZATION'] = filter_auth_header(auth) if auth
58
+
59
+ filtered['HTTP_API_TOKEN'] = FILTERED if headers['HTTP_API_TOKEN']
60
+
61
+ headers.merge(filtered)
62
+ end
63
+
64
+ def filter_auth_header(authorization)
65
+ return FILTERED unless authorization.include? 'Basic'
66
+
67
+ encoded = authorization.split(' ', 2).second
68
+ user, _password = Base64.decode64(encoded).split(':')
69
+
70
+ "Basic #{user}:#{FILTERED}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,45 +1,170 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ougai/formatters/base'
4
- require 'ougai/formatters/for_json'
5
-
6
3
  module ZuoraObservability
7
4
  module Logging
8
- # A JSON formatter compatible with node-bunyan
9
- class Formatter < Ougai::Formatters::Base
10
- include Ougai::Formatters::ForJson
11
-
12
- # Intialize a formatter
13
- # @param [String] app_name application name (execution program name if nil)
14
- # @param [String] hostname hostname (hostname if nil)
15
- # @param [Hash] opts the initial values of attributes
16
- # @option opts [String] :trace_indent (2) the value of trace_indent attribute
17
- # @option opts [String] :trace_max_lines (100) the value of trace_max_lines attribute
18
- # @option opts [String] :serialize_backtrace (true) the value of serialize_backtrace attribute
19
- # @option opts [String] :jsonize (true) the value of jsonize attribute
20
- # @option opts [String] :with_newline (true) the value of with_newline attribute
21
- def initialize(app_name = nil, hostname = nil, opts = {})
22
- aname, hname, opts = Ougai::Formatters::Base.parse_new_params([app_name, hostname, opts])
23
- super(aname, hname, opts)
24
- init_opts_for_json(opts)
25
- end
5
+ # Formats data into ZECS Logging Standard and dumps as JSON
6
+ # https://tag.pages.gitlab.zeta.tools/standards/logging
7
+ class Formatter < Ougai::Formatters::Bunyan
8
+ ECS_VERSION = '1.5.0'
9
+
10
+ ZECS_TOP_LEVEL_FIELDS = %i[
11
+ error event http organization process url user
12
+ ].freeze
13
+ ECS_HTTP_FIELDS = %i[request response].freeze
26
14
 
27
15
  def _call(severity, time, progname, data)
28
- data.merge!({ message: data.delete(:msg) })
29
- if data[:timestamp].present?
30
- time = data[:timestamp]
31
- data.delete(:timestamp)
32
- end
33
- dump({
34
- name: progname || @app_name,
35
- pid: $$,
36
- level: severity,
37
- timestamp: time.utc.strftime('%FT%T.%6NZ'),
38
- }.merge(data))
16
+ output = base_fields(severity, time, progname, data)
17
+
18
+ other_fields = get_fields_for(ZECS_TOP_LEVEL_FIELDS, data)
19
+ output.merge!(other_fields)
20
+
21
+ zuora_fields = ZuoraFields.call(data)
22
+ output[:zuora] = zuora_fields unless zuora_fields.empty?
23
+
24
+ dump(output)
39
25
  end
40
26
 
27
+ # Note(hartley): Ougai::Formatters::ForJson requires this be present
41
28
  def convert_time(data)
42
- # data[:timestamp] = format_datetime(data[:time])
29
+ data[:@timestamp] = data[:@timestamp].utc.iso8601(3)
30
+ end
31
+
32
+ private
33
+
34
+ def get_fields_for(list, data)
35
+ final_hash = {}
36
+
37
+ list.each do |field|
38
+ field_output = send("#{field}_fields", data)
39
+ final_hash[field] = field_output unless field_output.empty?
40
+ end
41
+
42
+ final_hash
43
+ end
44
+
45
+ # Fields required by ECS
46
+ def base_fields(severity, time, progname, data)
47
+ {
48
+ :@timestamp => time,
49
+ message: data[:msg],
50
+ ecs: { version: ECS_VERSION },
51
+ log: { level: severity, logger: progname || @app_name },
52
+ service: { name: Env.name, version: Env.version },
53
+ trace: { id: data[:trace_id] }
54
+ }
55
+ end
56
+
57
+ # error
58
+ def error_fields(data)
59
+ return {} unless data[:error]
60
+
61
+ {
62
+ message: data[:error].message,
63
+ stack_trace: data[:error].backtrace.join("\n"),
64
+ type: data[:error].class
65
+ }.compact
66
+ end
67
+
68
+ # http
69
+ def http_fields(data)
70
+ get_fields_for(ECS_HTTP_FIELDS, data)
71
+ end
72
+
73
+ # http.request
74
+ def request_fields(data)
75
+ {
76
+ method: data[:method],
77
+ body: ({ content: data[:params] } if data.key? :params)
78
+ }.compact
79
+ end
80
+
81
+ # http.response
82
+ def response_fields(data)
83
+ {
84
+ status_code: data[:status]
85
+ }.compact
86
+ end
87
+
88
+ # organization
89
+ def organization_fields(data)
90
+ {
91
+ name: data[:organization]
92
+ }.compact
93
+ end
94
+
95
+ # process
96
+ def process_fields(_data)
97
+ { id: Process.pid }
98
+ end
99
+
100
+ # url
101
+ def url_fields(data)
102
+ {
103
+ path: data[:path]
104
+ }.compact
105
+ end
106
+
107
+ # user
108
+ def user_fields(data)
109
+ {
110
+ email: data[:email]
111
+ }.compact
112
+ end
113
+
114
+ # event
115
+ def event_fields(data)
116
+ full_action = "#{data[:controller]}##{data[:action]}"
117
+
118
+ {
119
+ action: (full_action unless full_action == '#'),
120
+ duration: data[:duration]
121
+ }.compact
122
+ end
123
+ end
124
+
125
+ # The Zuora Extension to ECS defined by ZECS
126
+ module ZuoraFields
127
+ ZECS_VERSION = '1.1'
128
+
129
+ class << self
130
+ # zuora top level field
131
+ def call(data)
132
+ hash = base_fields(data)
133
+
134
+ z_http = http_fields(data)
135
+ hash[:http] = z_http unless z_http.empty?
136
+
137
+ z_service = service_fields(data)
138
+ hash[Env.name.to_sym] = z_service unless z_service.empty?
139
+
140
+ return {} if hash.empty?
141
+
142
+ hash.merge({ ecs: { version: ZECS_VERSION } })
143
+ end
144
+
145
+ def base_fields(data)
146
+ {
147
+ apartment_id: data[:app_instance_id],
148
+ cp_id: data[:zuora_track_id],
149
+ environment: data[:environment],
150
+ tenant_id: data[:tenant_ids],
151
+ trace_id: data[:zuora_trace_id]
152
+ }.compact
153
+ end
154
+
155
+ # zuora.http
156
+ def http_fields(data)
157
+ z_http_request = { headers: data[:headers] }.compact
158
+
159
+ {
160
+ request: (z_http_request unless z_http_request.empty?)
161
+ }.compact
162
+ end
163
+
164
+ # Service's Custom Fields
165
+ def service_fields(data)
166
+ data[:zecs_service].presence || {}
167
+ end
43
168
  end
44
169
  end
45
170
  end
@@ -3,18 +3,7 @@
3
3
  module ZuoraObservability
4
4
  # Methods to gather and format metrics
5
5
  module Metrics
6
- @@telegraf_host = nil
7
-
8
6
  class << self
9
- def write_to_telegraf(*args)
10
- if ZuoraObservability.configuration.enable_metrics && !defined?(Prometheus)
11
- @@telegraf_host = Metrics::Telegraf.new() if @@telegraf_host == nil
12
- unicorn_stats = Metrics.unicorn_listener if defined?(Unicorn) && Unicorn.respond_to?(:listener_names)
13
- @@telegraf_host.write(direction: 'Raindrops', tags: {}, values: unicorn_stats) unless unicorn_stats.blank?
14
- return @@telegraf_host.write(*args)
15
- end
16
- end
17
-
18
7
  def resque
19
8
  Resque.redis.ping
20
9
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ZuoraObservability
4
- VERSION = '0.1.0-b'
4
+ VERSION = '0.1.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zuora_observability
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.b
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hartley McGuire
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-03 00:00:00.000000000 Z
11
+ date: 2021-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lograge
@@ -140,16 +140,16 @@ dependencies:
140
140
  name: rubocop
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - "~>"
143
+ - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: '1.2'
145
+ version: '0'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - "~>"
150
+ - - ">="
151
151
  - !ruby/object:Gem::Version
152
- version: '1.2'
152
+ version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: rubocop-rails
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -168,16 +168,16 @@ dependencies:
168
168
  name: rubocop-rspec
169
169
  requirement: !ruby/object:Gem::Requirement
170
170
  requirements:
171
- - - "~>"
171
+ - - ">="
172
172
  - !ruby/object:Gem::Version
173
- version: 2.0.0.pre
173
+ version: '0'
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
- - - "~>"
178
+ - - ">="
179
179
  - !ruby/object:Gem::Version
180
- version: 2.0.0.pre
180
+ version: '0'
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: simplecov
183
183
  requirement: !ruby/object:Gem::Requirement
@@ -261,11 +261,9 @@ files:
261
261
  - lib/zuora_observability/engine.rb
262
262
  - lib/zuora_observability/env.rb
263
263
  - lib/zuora_observability/logger.rb
264
+ - lib/zuora_observability/logging/custom_options.rb
264
265
  - lib/zuora_observability/logging/formatter.rb
265
266
  - lib/zuora_observability/metrics.rb
266
- - lib/zuora_observability/metrics/page_request.rb
267
- - lib/zuora_observability/metrics/point_value.rb
268
- - lib/zuora_observability/metrics/telegraf.rb
269
267
  - lib/zuora_observability/version.rb
270
268
  homepage:
271
269
  licenses:
@@ -279,12 +277,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
279
277
  requirements:
280
278
  - - ">="
281
279
  - !ruby/object:Gem::Version
282
- version: '2.4'
280
+ version: '2.5'
283
281
  required_rubygems_version: !ruby/object:Gem::Requirement
284
282
  requirements:
285
- - - ">"
283
+ - - ">="
286
284
  - !ruby/object:Gem::Version
287
- version: 1.3.1
285
+ version: '0'
288
286
  requirements: []
289
287
  rubygems_version: 3.1.4
290
288
  signing_key:
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ZuoraObservability
4
- module Metrics
5
- # Object of this class is passed to the ActiveSupport::Notification hook
6
- class PageRequest
7
- # This method is triggered when a non error page is loaded (not 404)
8
- def call(_name, started, finished, unique_id, payload)
9
- # If the url contains any css or JavaScript files then do not collect metrics for them
10
- return nil if %w[css assets jpg png jpeg ico].any? { |word| payload[:path].include?(word) }
11
-
12
- # Getting the endpoint and the content_type
13
- content_hash = { :html => 'text/html', :js => 'application/javascript', :json => 'application/json', :csv => 'text/csv' }
14
- content_type = content_hash.key?(payload[:format]) ? content_hash[payload[:format]] : payload[:format]
15
- content_type = content_type.to_s.gsub('text/javascript', 'application/javascript')
16
-
17
- # payloads with 500 requests do not have status as it is not set by the controller
18
- # https://github.com/rails/rails/issues/33335
19
- # status_code = payload[:status] ? payload[:status] : payload[:exception_object].present? ? 500 : ""
20
- if payload[:exception].present?
21
- status_code, exception = [500, payload[:exception].first]
22
- else
23
- status_code, exception = [payload[:status], nil]
24
- end
25
-
26
- tags = { method: payload[:method], status: status_code, error_type: exception, content_type: content_type, controller: payload[:controller], action: payload[:action] }.compact
27
-
28
- values = { view_time: payload[:view_runtime], db_time: payload[:db_runtime], response_time: ((finished - started) * 1000) }.compact
29
- values = values.map { |k, v| [k, v.round(2)] }.to_h
30
-
31
- ZuoraObservability::Metrics.write_to_telegraf(direction: :inbound, tags: tags, values: values)
32
- end
33
- end
34
- end
35
- end
@@ -1,84 +0,0 @@
1
- # this looks copied from https://github.com/influxdata/influxdb-ruby, it may be
2
- # worth just using the gem instead of vendoring the file
3
- # module InfluxDB
4
- module ZuoraObservability
5
- module Metrics
6
- # Convert data point to string using Line protocol
7
- class PointValue
8
- attr_reader :series, :values, :tags, :timestamp
9
-
10
- def initialize(data)
11
- @series = escape data[:series], :measurement
12
- @values = escape_values data[:values]
13
- @tags = escape_tags data[:tags]
14
- @timestamp = data[:timestamp]
15
- end
16
-
17
- def dump
18
- dump = @series.dup
19
- dump << ",#{@tags}" if @tags
20
- dump << " #{@values}"
21
- dump << " #{@timestamp}" if @timestamp
22
- dump
23
- end
24
-
25
- private
26
-
27
- ESCAPES = {
28
- measurement: [' '.freeze, ','.freeze],
29
- tag_key: ['='.freeze, ' '.freeze, ','.freeze],
30
- tag_value: ['='.freeze, ' '.freeze, ','.freeze],
31
- field_key: ['='.freeze, ' '.freeze, ','.freeze, '"'.freeze],
32
- field_value: ["\\".freeze, '"'.freeze],
33
- }.freeze
34
-
35
- private_constant :ESCAPES
36
-
37
- def escape(str, type)
38
- # rubocop:disable Layout/AlignParameters
39
- str = str.encode "UTF-8".freeze, "UTF-8".freeze,
40
- invalid: :replace,
41
- undef: :replace,
42
- replace: "".freeze
43
- # rubocop:enable Layout/AlignParameters
44
-
45
- ESCAPES[type].each do |ch|
46
- str = str.gsub(ch) { "\\#{ch}" }
47
- end
48
- str
49
- end
50
-
51
- def escape_values(values)
52
- return if values.nil?
53
- values.map do |k, v|
54
- key = escape(k.to_s, :field_key)
55
- val = escape_value(v)
56
- "#{key}=#{val}"
57
- end.join(",".freeze)
58
- end
59
-
60
- def escape_value(value)
61
- if value.is_a?(String)
62
- '"'.freeze + escape(value, :field_value) + '"'.freeze
63
- elsif value.is_a?(Integer)
64
- "#{value}i"
65
- else
66
- value.to_s
67
- end
68
- end
69
-
70
- def escape_tags(tags)
71
- return if tags.nil?
72
-
73
- tags = tags.map do |k, v|
74
- key = escape(k.to_s, :tag_key)
75
- val = escape(v.to_s, :tag_value)
76
-
77
- "#{key}=#{val}" unless key == "".freeze || val == "".freeze
78
- end.compact
79
-
80
- tags.join(",") unless tags.empty?
81
- end
82
- end
83
- end
84
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ZuoraObservability
4
- module Metrics
5
- # Functionality for sending metrics to a Telegraf endpoint
6
- #
7
- # it looks like https://github.com/influxdata/influxdb-ruby may provide some
8
- # more high level abstractions instead of using a UDPSocket directly
9
- class Telegraf
10
- attr_accessor :host
11
-
12
- OUTBOUND_METRICS = true
13
- OUTBOUND_METRICS_NAME = 'request-outbound'
14
- INBOUND_METRICS = true
15
- INBOUND_METRICS_NAME = 'request-inbound'
16
-
17
- def initialize
18
- connect
19
- end
20
-
21
- def connect
22
- # TODO(hartley): this Rails logger was originally ZuoraConnect.logger
23
- Rails.logger.debug(format_metric_log('Telegraf', 'Need new connection')) if ZuoraObservability.configuration.telegraf_debug
24
- uri = URI.parse(ZuoraObservability.configuration.telegraf_endpoint)
25
- self.host = UDPSocket.new.tap do |socket|
26
- socket.connect uri.host, uri.port
27
- end
28
- rescue => ex
29
- self.host = nil
30
- # TODO(hartley): this Rails logger was originally ZuoraConnect.logger
31
- Rails.logger.warn(self.format_metric_log('Telegraf', "Failed to connect: #{ex.class}")) if Rails.env.to_s != 'production'
32
- end
33
-
34
- def write(direction: 'Unknown', tags: {}, values: {})
35
- time = Benchmark.measure do
36
- # To avoid writing metrics from rspec tests
37
- if Rails.env.to_sym != :test
38
- app_instance = Thread.current[:appinstance].present? ? Thread.current[:appinstance].id : 0
39
- tags = {
40
- app_name: Env.app_name, process_type: Env.process_type,
41
- app_instance: app_instance, pod_name: Env.pod_name
42
- }.merge(tags)
43
-
44
- if direction == :inbound
45
- # This condition relies on a monkey patch in the connect gem that
46
- # adds a to_bool method for Nil, True, and False that are not
47
- # present by default
48
- if INBOUND_METRICS && !Thread.current[:inbound_metric].to_bool
49
- self.write_udp(series: INBOUND_METRICS_NAME, tags: tags, values: values)
50
- Thread.current[:inbound_metric] = true
51
- else
52
- return
53
- end
54
- elsif direction == :outbound
55
- write_udp(series: OUTBOUND_METRICS_NAME, tags: tags, values: values) if OUTBOUND_METRICS
56
- else
57
- write_udp(series: direction, tags: tags, values: values)
58
- end
59
- end
60
- end
61
-
62
- return unless ZuoraObservability.configuration.telegraf_debug
63
-
64
- # TODO(hartley): these Rails loggers were originally ZuoraConnect.logger
65
- Rails.logger.debug(format_metric_log('Telegraf', tags.to_s))
66
- Rails.logger.debug(format_metric_log('Telegraf', values.to_s))
67
- Rails.logger.debug(
68
- format_metric_log(
69
- 'Telegraf',
70
- "Writing '#{direction.capitalize}': #{time.real.round(5)} ms"
71
- )
72
- )
73
- end
74
-
75
- def write_udp(series: '', tags: {}, values: {})
76
- return if values.blank?
77
-
78
- host.write PointValue.new({ series: series, tags: tags, values: values }).dump
79
- rescue => ex
80
- self.connect
81
- ZuoraConnect.logger.warn(self.format_metric_log('Telegraf', "Failed to write udp: #{ex.class}")) if Rails.env.to_s != 'production'
82
- end
83
-
84
- def format_metric_log(message, dump = nil)
85
- message_color = '1;91'
86
- dump_color = '0;1'
87
- log_entry = " \e[#{message_color}m#{message}\e[0m #{
88
- "\e[#{dump_color}m#{dump}\e[0m" if dump
89
- }"
90
- if Rails.env.development?
91
- log_entry
92
- else
93
- [message, dump].compact.join(' - ')
94
- end
95
- end
96
- end
97
- end
98
- end