zuora_observability 0.1.0.pre.b → 0.1.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.
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