zuora_observability 0.1.0.pre.a → 0.3.5

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: c7e3a6c154d518f30f5a95e1be272b96b1853b46df0300d4fa45ca8e17f04451
4
- data.tar.gz: 0c510e0da92d8b7139586e60bd048bc91f18cc61a9cfe8165f4f44676c223180
3
+ metadata.gz: 2f179eca39cb93afe9e161797593b9b36e0b99cc4b2f002c98e549a0eac24b17
4
+ data.tar.gz: 041fdacdc8805507528d04bbc0e73c3de85f16d60e3d92b15c0a9715650c1015
5
5
  SHA512:
6
- metadata.gz: 3948b6dacece27853e9df426c81d6f475c1b8b5417131c4318eeac8b88c8ffa4c4f625036f95cc5b73ed78ed53352f182c2f7efb16800e313b797a7416372b3b
7
- data.tar.gz: f67b67d6bbbf2ff33c30433f32f321bf28109540bbf54250764509332de9caba0dbcf94917439882ea7a0f8d6e1bc437eae221134ceb4adfac00933dddadd1f6
6
+ metadata.gz: f0ef15e3d143be63cadc70a1249b18e4d10be281d1a7fe30523beae340d85e8dc2bc141f30a73e3dda0162e4bc3171848f87d11509ab26bfa9c6bae3d9737f0a
7
+ data.tar.gz: 35ae00955747c5da4e62e58f9634157618d07d5b8749230b86158f0ae9691b0e6306e49f96b34a9702e4f2c0d87ebb36d37e97b705127fb9a6e429f1d1a7d646
data/README.md CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  A ruby gem to enable observability into rails applications
4
4
 
5
- ## Usage
6
- How to use my plugin.
5
+ ## Prerequisites
6
+
7
+ - Ruby 2.5+
8
+ - Rails 5.0+
7
9
 
8
10
  ## Installation
11
+
9
12
  Add this line to your application's Gemfile:
10
13
 
11
14
  ```ruby
@@ -13,14 +16,21 @@ gem 'zuora_observability'
13
16
  ```
14
17
 
15
18
  And then execute:
19
+
16
20
  ```bash
17
21
  $ bundle
18
22
  ```
19
23
 
20
24
  Or install it yourself as:
25
+
21
26
  ```bash
22
27
  $ gem install zuora_observability
23
28
  ```
24
29
 
30
+ ## Usage
31
+
32
+ - [Logging](doc/logging.md)
33
+
25
34
  ## License
35
+
26
36
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Resque.logger)
4
+ Resque.logger = ZuoraObservability::Logger.custom_logger(name: 'Resque')
5
+ if defined?(Resque::Scheduler)
6
+ Resque::Scheduler.logger = ZuoraObservability::Logger.custom_logger(name: 'ResqueScheduler')
7
+ end
8
+ end
9
+
10
+ if defined?(Delayed::Worker.logger)
11
+ Delayed::Worker.logger = ZuoraObservability::Logger.custom_logger(name: 'DelayedJob', level: Logger::INFO)
12
+ end
13
+
14
+ if defined?(Makara)
15
+ Makara::Logging::Logger.logger = ZuoraObservability::Logger.custom_logger(name: 'Makara')
16
+ end
17
+
18
+ if defined?(ElasticAPM) && ElasticAPM.running?
19
+ ElasticAPM.agent.config.logger = ZuoraObservability::Logger.custom_logger(name: 'ElasticAPM', level: Logger::WARN)
20
+ end
21
+
22
+ if defined?(ActionMailer)
23
+ ActionMailer::Base.logger = ZuoraObservability::Logger.custom_logger(name: 'ActionMailer')
24
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module TaggedLogging
5
+ # Patch to apply structured tags to Tagged Logs
6
+ module Formatter
7
+ def call(severity, timestamp, progname, data)
8
+ super(severity, timestamp, progname, merged_tags.merge!(data))
9
+ end
10
+
11
+ private
12
+
13
+ def merged_tags
14
+ tags = {}
15
+
16
+ current_tags.each do |tag|
17
+ tags.merge!(tag) if tag.is_a?(Hash)
18
+ end
19
+
20
+ tags
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ module Rails
27
+ module Rack
28
+ # Patch to compute structured tags for Tagged Logging
29
+ class Logger
30
+ private
31
+
32
+ def compute_tags(request)
33
+ {
34
+ trace_id: request.uuid
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -3,14 +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
13
- @json_logging = false
10
+ @custom_payload_hooks = []
11
+ @zecs_service_hook = nil
12
+ # NOTE(hartley): this checks the var for presence rather than value to
13
+ # align with how Rails does RAILS_LOG_TO_STDOUT
14
+ @json_logging = ENV['ZUORA_JSON_LOGGING'].present? ? true : !(Rails.env.development? || Rails.env.test?)
15
+ end
16
+
17
+ def add_custom_payload_hook(&block)
18
+ custom_payload_hooks << block
14
19
  end
15
20
  end
16
21
  end
@@ -10,64 +10,35 @@ module ZuoraObservability
10
10
  end
11
11
 
12
12
  initializer(:rails_stdout_logging, before: :initialize_logger) do
13
- require 'lograge'
13
+ Rails.application.configure do
14
+ config.logger = ZuoraObservability::Logger.custom_logger(name: 'Rails')
14
15
 
15
- Rails.configuration.logger = ZuoraObservability::Logger.custom_logger(name: "Rails")
16
- if !Rails.env.test? && !Rails.env.development?
17
- Rails.configuration.lograge.enabled = true
18
- Rails.configuration.colorize_logging = false
19
- end
16
+ next unless ZuoraObservability.configuration.json_logging
20
17
 
21
- if Rails.configuration.lograge.enabled
22
- if Rails.configuration.logger.class.to_s == 'Ougai::Logger'
23
- Rails.configuration.lograge.formatter = Class.new do |fmt|
24
- def fmt.call(data)
25
- { msg: 'Rails Request', request: data }
26
- end
27
- end
28
- end
29
- #Rails.configuration.lograge.formatter = Lograge::Formatters::Json.new
30
- Rails.configuration.lograge.custom_options = lambda do |event|
31
- exceptions = %w(controller action format)
32
- items = {
33
- #time: event.time.strftime('%FT%T.%6N'),
34
- params: event.payload[:params].as_json(except: exceptions).to_json.to_s
35
- }
36
- items.merge!({exception_object: event.payload[:exception_object]}) if event.payload[:exception_object].present?
37
- items.merge!({exception: event.payload[:exception]}) if event.payload[:exception].present?
18
+ config.logger = ActiveSupport::TaggedLogging.new(config.logger)
38
19
 
39
- if event.payload[:headers].present?
40
- # By convertion, headers usually do not have dots. Nginx even rejects headers with dots
41
- # All Rails headers are namespaced, like 'rack.input'.
42
- # Thus, we can obtain the client headers by rejecting dots
43
- request_headers =
44
- event.payload[:headers].env.
45
- reject { |key| key.to_s.include?('.') || REQUEST_HEADERS_TO_IGNORE.include?(key.to_s) }
46
- begin
47
- if request_headers["HTTP_AUTHORIZATION"].present?
48
- if request_headers["HTTP_AUTHORIZATION"].include?("Basic")
49
- user_password = request_headers["HTTP_AUTHORIZATION"].split("Basic").last.strip
50
- user, password = Base64.decode64(user_password).split(":")
51
- request_headers["HTTP_AUTHORIZATION"] = "Basic #{user}:ValueFiltered"
52
- elsif
53
- request_headers["HTTP_AUTHORIZATION"] = "ValueFiltered"
54
- end
55
- end
56
- request_headers["HTTP_API_TOKEN"] = "ValueFiltered" if request_headers["HTTP_API_TOKEN"].present?
57
- rescue
58
- request_headers.delete("HTTP_API_TOKEN")
59
- request_headers.delete("HTTP_AUTHORIZATION")
60
- end
61
- items.merge!({ headers: request_headers.to_s })
62
- end
20
+ require 'lograge'
21
+ require 'zuora_observability/logging/custom_options'
22
+
23
+ config.lograge.enabled = true
24
+ config.colorize_logging = false
63
25
 
64
- if Thread.current[:appinstance].present?
65
- items.merge!({connect_user: Thread.current[:appinstance].connect_user, new_session: Thread.current[:appinstance].new_session_message})
66
- if Thread.current[:appinstance].logitems.present? && Thread.current[:appinstance].logitems.class == Hash
67
- items.merge!(Thread.current[:appinstance].logitems)
68
- end
26
+ config.lograge.formatter = Lograge::Formatters::Raw.new
27
+
28
+ config.lograge.custom_options = Logging::CustomOptions
29
+ config.lograge.custom_payload do |controller|
30
+ payload = {}
31
+
32
+ ZuoraObservability.configuration.custom_payload_hooks.each do |hook|
33
+ payload.merge!(hook.call(controller))
69
34
  end
70
- return items
35
+
36
+ next payload unless ZuoraObservability.configuration.zecs_service_hook
37
+
38
+ payload[:zecs_service] =
39
+ ZuoraObservability.configuration.zecs_service_hook.call(controller)
40
+
41
+ payload
71
42
  end
72
43
  end
73
44
  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
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,47 +1,69 @@
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): potentially change Logger module to delegate methods to
9
- # value returned from custom_logger (so it ends up being a pure wrapper)
10
- def self.custom_logger(name: "", level: Rails.logger.present? ? Rails.logger.level : MonoLogger::INFO, type: :ougai)
11
- #puts name + ' - ' + {Logger::WARN => 'Logger::WARN', Logger::ERROR => 'Logger::ERROR', Logger::DEBUG => 'Logger::DEBUG', Logger::INFO => 'Logger::INFO' }[level] + ' - '
10
+ class Logger < Ougai::Logger
11
+ # TODO(hartley): enable silence method for Rails 5.2 asset requests
12
+ # https://github.com/tilfin/ougai/wiki/Use-as-Rails-logger
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)
12
59
  if type == :ougai
13
- require 'ougai'
14
- require "ougai/formatters/customizable"
15
- #logger = Ougai::Logger.new(MonoLogger.new(STDOUT))
16
- logger = Ougai::Logger.new(STDOUT)
17
- logger.level = level
18
- if ZuoraObservability.configuration.json_logging
19
- require 'zuora_observability/logging/formatter'
20
- logger.formatter = ZuoraObservability::Logging::Formatter.new(name)
21
- logger.before_log = lambda do |data|
22
- data[:trace_id] = ZuoraConnect::RequestIdMiddleware.request_id if ZuoraConnect::RequestIdMiddleware.request_id.present?
23
- data[:zuora_trace_id] = ZuoraConnect::RequestIdMiddleware.zuora_request_id if ZuoraConnect::RequestIdMiddleware.zuora_request_id.present?
24
- #data[:traces] = {amazon_id: data[:trace_id], zuora_id: data[:zuora_trace_id]}
25
- end
26
- else
27
- logger.formatter = Ougai::Formatters::Customizable.new(
28
- format_err: proc do |data|
29
- next nil unless data.key?(:err)
30
- err = data.delete(:err)
31
- " #{err[:name]} (#{err[:message]})\n #{err[:stack]}"
32
- end,
33
- format_data: proc do |data|
34
- data.delete(:app_instance_id); data.delete(:tenant_ids); data.delete(:organization); data.delete(:environment)
35
- format('%s %s: %s', 'DATA'.ljust(6), Time.now.strftime('%FT%T.%6NZ'), "#{data.to_json}") if data.present?
36
- end,
37
- format_msg: proc do |severity, datetime, _progname, data|
38
- msg = data.delete(:msg)
39
- format('%s %s: %s', severity.ljust(6), datetime, msg)
40
- end
41
- )
42
- logger.formatter.datetime_format = '%FT%T.%6NZ'
43
- end
60
+ logger = new($stdout, level: level, progname: name)
44
61
  else
62
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
63
+ Creating a non-ougai custom_logger is deprecated and will be removed
64
+ in a future version of zuora_observability.
65
+ MSG
66
+
45
67
  require 'mono_logger'
46
68
  logger = MonoLogger.new(STDOUT)
47
69
  logger.level = level
@@ -65,7 +87,8 @@ module ZuoraObservability
65
87
  end
66
88
  end
67
89
  end
68
- return logger
90
+
91
+ logger
69
92
  end
70
93
  end
71
94
  end
@@ -0,0 +1,79 @@
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
+ HTTP_COOKIE
17
+ HTTP_X_ENVOY_PEER_METADATA
18
+ HTTP_X_ENVOY_DECORATOR_OPERATION
19
+ HTTP_ZSESSION
20
+ HTTP_SEC_CH_UA_MOBILE
21
+ ].freeze
22
+
23
+ IGNORE_PARAMS = %w[controller action format].freeze
24
+
25
+ FILTERED = '[FILTERED]'
26
+
27
+ class << self
28
+ def call(event)
29
+ items = {
30
+ msg: 'Rails Request',
31
+ params: event.payload[:params].as_json(except: IGNORE_PARAMS).to_s,
32
+ zuora_trace_id: event.payload[:headers]['HTTP_ZUORA_REQUEST_ID'],
33
+ error: event.payload[:exception_object]
34
+ }
35
+
36
+ if event.payload[:headers].present?
37
+ request_headers = filter_request_headers(event.payload[:headers])
38
+
39
+ filtered_headers = filter_sensitive_headers(request_headers)
40
+
41
+ items.merge!({ headers: filtered_headers })
42
+ end
43
+
44
+ items
45
+ end
46
+
47
+ private
48
+
49
+ def filter_request_headers(action_dispatch_headers)
50
+ action_dispatch_headers.env.select do |header|
51
+ next false if IGNORE_HEADERS.include? header
52
+
53
+ header =~ /^HTTP_/ || ActionDispatch::Http::Headers::CGI_VARIABLES.include?(header)
54
+ end
55
+ end
56
+
57
+ def filter_sensitive_headers(headers)
58
+ filtered = {}
59
+
60
+ auth = headers['HTTP_AUTHORIZATION']
61
+ filtered['HTTP_AUTHORIZATION'] = filter_auth_header(auth) if auth
62
+
63
+ filtered['HTTP_API_TOKEN'] = FILTERED if headers['HTTP_API_TOKEN']
64
+
65
+ headers.merge(filtered)
66
+ end
67
+
68
+ def filter_auth_header(authorization)
69
+ return FILTERED unless authorization.include? 'Basic'
70
+
71
+ encoded = authorization.split(' ', 2).second
72
+ user, _password = Base64.decode64(encoded).split(':')
73
+
74
+ "Basic #{user}:#{FILTERED}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,45 +1,191 @@
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
+ tags: ['zecs']
55
+ }
56
+ end
57
+
58
+ # errors
59
+ def error_fields(data)
60
+ return {} unless data[:error] || data[:err]
61
+
62
+ error = data[:err] || serialize_exc(data[:error])
63
+
64
+ {
65
+ message: error[:message],
66
+ stack_trace: error[:stack],
67
+ type: error[:name]
68
+ }.compact
69
+ end
70
+
71
+ # http
72
+ def http_fields(data)
73
+ get_fields_for(ECS_HTTP_FIELDS, data)
74
+ end
75
+
76
+ # http.request
77
+ def request_fields(data)
78
+ request = data.slice(:method, :params).merge(data.fetch(:request, {}))
79
+
80
+ {
81
+ method: request[:method],
82
+ body: ({ content: request[:params] } if request.key? :params)
83
+ }.compact
84
+ end
85
+
86
+ # http.response
87
+ def response_fields(data)
88
+ response = data.slice(:status).merge(data.fetch(:response, {}))
89
+
90
+ {
91
+ status_code: response[:status],
92
+ body: ({ content: response[:params] } if response.key? :params),
93
+ }.compact
94
+ end
95
+
96
+ # organization
97
+ def organization_fields(data)
98
+ {
99
+ name: data[:organization]
100
+ }.compact
101
+ end
102
+
103
+ # process
104
+ def process_fields(_data)
105
+ { id: Process.pid }
106
+ end
107
+
108
+ # url
109
+ def url_fields(data)
110
+ {
111
+ full: data.dig(:url, :full),
112
+ path: data[:path]
113
+ }.compact
114
+ end
115
+
116
+ # user
117
+ def user_fields(data)
118
+ {
119
+ id: data[:user_id],
120
+ email: data[:email]
121
+ }.compact
122
+ end
123
+
124
+ # event
125
+ def event_fields(data)
126
+ full_action = "#{data[:controller]}##{data[:action]}"
127
+
128
+ {
129
+ action: (full_action unless full_action == '#'),
130
+ duration: data[:duration]
131
+ }.compact
132
+ end
133
+ end
134
+
135
+ # The Zuora Extension to ECS defined by ZECS
136
+ module ZuoraFields
137
+ ZECS_VERSION = '1.1'
138
+
139
+ class << self
140
+ # zuora top level field
141
+ def call(data)
142
+ hash = base_fields(data)
143
+
144
+ z_http = http_fields(data)
145
+ hash[:http] = z_http unless z_http.empty?
146
+
147
+ z_service = service_fields(data)
148
+ hash[Env.name.to_sym] = z_service unless z_service.empty?
149
+
150
+ return {} if hash.empty?
151
+
152
+ hash.merge({ ecs: { version: ZECS_VERSION } })
153
+ end
154
+
155
+ def base_fields(data)
156
+ {
157
+ cp_id: data[:zuora_track_id],
158
+ entity_ids: data[:entity_ids],
159
+ environment: data[:environment],
160
+ tenant_id: data[:tenant_ids],
161
+ trace_id: data[:zuora_trace_id]
162
+ }.compact
163
+ end
164
+
165
+ # zuora.http
166
+ def http_fields(data)
167
+ z_http_request = {
168
+ headers: data[:headers] || data.dig(:request, :headers),
169
+ headers_blob: data.dig(:request, :headers_blob)
170
+ }.compact
171
+
172
+ z_http_response = {
173
+ headers: data.dig(:response, :headers),
174
+ headers_blob: data.dig(:response, :headers_blob)
175
+ }.compact
176
+
177
+ {
178
+ request: (z_http_request unless z_http_request.empty?),
179
+ response: (z_http_response unless z_http_response.empty?)
180
+ }.compact
181
+ end
182
+
183
+ # Service's Custom Fields
184
+ def service_fields(data)
185
+ {
186
+ app_instance_id: data[:app_instance_id]
187
+ }.merge(data.fetch(:zecs_service, {})).compact
188
+ end
43
189
  end
44
190
  end
45
191
  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
- # frozen_string_literal: true
1
+ # frozen_string_literal: truemf
2
2
 
3
3
  module ZuoraObservability
4
- VERSION = '0.1.0-a'
4
+ VERSION = '0.3.5'
5
5
  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
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.a
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hartley McGuire
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-25 00:00:00.000000000 Z
11
+ date: 2024-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lograge
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mono_logger
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: ougai
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +66,6 @@ dependencies:
52
66
  - - '='
53
67
  - !ruby/object:Gem::Version
54
68
  version: 1.0.0
55
- - !ruby/object:Gem::Dependency
56
- name: mono_logger
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '1.0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '1.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rails
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -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
@@ -244,15 +244,12 @@ files:
244
244
  - MIT-LICENSE
245
245
  - README.md
246
246
  - Rakefile
247
- - app/assets/config/zuora_observability_manifest.js
248
- - app/assets/stylesheets/zuora_observability/application.css
249
247
  - app/controllers/zuora_observability/application_controller.rb
250
248
  - app/controllers/zuora_observability/metrics_controller.rb
251
249
  - app/helpers/zuora_observability/application_helper.rb
252
- - app/jobs/zuora_observability/application_job.rb
253
- - app/mailers/zuora_observability/application_mailer.rb
254
- - app/models/zuora_observability/application_record.rb
255
250
  - app/views/layouts/zuora_observability/application.html.erb
251
+ - config/initializers/loggers.rb
252
+ - config/initializers/tagged_logging.rb
256
253
  - config/routes.rb
257
254
  - lib/tasks/zuora_observability_tasks.rake
258
255
  - lib/zuora_observability.rb
@@ -260,16 +257,15 @@ files:
260
257
  - lib/zuora_observability/engine.rb
261
258
  - lib/zuora_observability/env.rb
262
259
  - lib/zuora_observability/logger.rb
260
+ - lib/zuora_observability/logging/custom_options.rb
263
261
  - lib/zuora_observability/logging/formatter.rb
264
262
  - lib/zuora_observability/metrics.rb
265
- - lib/zuora_observability/metrics/point_value.rb
266
- - lib/zuora_observability/metrics/telegraf.rb
267
263
  - lib/zuora_observability/version.rb
268
- homepage:
264
+ homepage:
269
265
  licenses:
270
266
  - MIT
271
267
  metadata: {}
272
- post_install_message:
268
+ post_install_message:
273
269
  rdoc_options: []
274
270
  require_paths:
275
271
  - lib
@@ -277,15 +273,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
277
273
  requirements:
278
274
  - - ">="
279
275
  - !ruby/object:Gem::Version
280
- version: '2.4'
276
+ version: '2.5'
281
277
  required_rubygems_version: !ruby/object:Gem::Requirement
282
278
  requirements:
283
- - - ">"
279
+ - - ">="
284
280
  - !ruby/object:Gem::Version
285
- version: 1.3.1
281
+ version: '0'
286
282
  requirements: []
287
- rubygems_version: 3.1.4
288
- signing_key:
283
+ rubygems_version: 3.2.22
284
+ signing_key:
289
285
  specification_version: 4
290
286
  summary: Summary of ZuoraObservability.
291
287
  test_files: []
@@ -1 +0,0 @@
1
- //= link_directory ../stylesheets/zuora_observability .css
@@ -1,15 +0,0 @@
1
- /*
2
- * This is a manifest file that'll be compiled into application.css, which will include all the files
3
- * listed below.
4
- *
5
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
- * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
- *
8
- * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
- * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
- * files in this directory. Styles in this file should be added after the last require_* statement.
11
- * It is generally better to create a new file per style scope.
12
- *
13
- *= require_tree .
14
- *= require_self
15
- */
@@ -1,4 +0,0 @@
1
- module ZuoraObservability
2
- class ApplicationJob < ActiveJob::Base
3
- end
4
- end
@@ -1,6 +0,0 @@
1
- module ZuoraObservability
2
- class ApplicationMailer < ActionMailer::Base
3
- default from: 'from@example.com'
4
- layout 'mailer'
5
- end
6
- end
@@ -1,5 +0,0 @@
1
- module ZuoraObservability
2
- class ApplicationRecord < ActiveRecord::Base
3
- self.abstract_class = true
4
- end
5
- 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