zuora_observability 0.1.0.pre.c → 0.1.0.pre.d

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: be19d78e7a52c884e0bd5cc705a3725ea9616cd96491116d6ac047abbe8018b3
4
- data.tar.gz: 446dc9c840d1d00cf66c0316d2fca0b38718d2b4ac32bcda718069515283c3f9
3
+ metadata.gz: f4a7c9aeb569ef2885c3f8b298771323d5a2619340b47e390d790966794b67df
4
+ data.tar.gz: 417163de43f269d1c5f82e2bf1c09c5066a5f752bc8e4d77d6bf27435de1d137
5
5
  SHA512:
6
- metadata.gz: a70635409b8672344d7efd3c4025507ca0f450fc64aa78d5d81de9558926d1e5a59716832e7408a83abf24ed6492e4470a80fbb8bf1f84ad6391d1e188eaf94f
7
- data.tar.gz: 7718bfc8650dc24092149163861a6f55ae7164cecda3f91534ee98c92645462195b9be625d0f212df974d2b87d4cc767ef9721d6f45581d47dfa0c878a53fccb
6
+ metadata.gz: e8435b4a4aa06bebf6d0924c8f39d71da9b96debf12582e0b92d147389e8072c8bcc40c13e2978a81cc7c8b0da9efcb540e3fa864a91e90f0b001b76d0c45667
7
+ data.tar.gz: a2adea8aef5fc82317077e4f68eb7061c66ca53991a545642deac1f5367cf35dfbae329fe83ee6feb3aa727e958cc0c6d7cae27c9062852affe29f8048ca6227
data/README.md CHANGED
@@ -4,6 +4,8 @@ A ruby gem to enable observability into rails applications
4
4
 
5
5
  ## Usage
6
6
 
7
+ - [Logging](doc/logging.md)
8
+
7
9
  ### Configuration
8
10
 
9
11
  Observability can be configured through an initializer as shown below.
@@ -3,12 +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 :json_logging
6
+ attr_reader :custom_payload_hooks
7
+ attr_accessor :zecs_service_hook, :json_logging
7
8
 
8
9
  def initialize
10
+ @custom_payload_hooks = []
11
+ @zecs_service_hook = nil
9
12
  # NOTE(hartley): this checks the var for presence rather than value to
10
13
  # align with how Rails does RAILS_LOG_TO_STDOUT
11
14
  @json_logging = ENV['ZUORA_JSON_LOGGING'].present? ? true : !(Rails.env.development? || Rails.env.test?)
12
15
  end
16
+
17
+ def add_custom_payload_hook(&block)
18
+ custom_payload_hooks << block
19
+ end
13
20
  end
14
21
  end
@@ -5,88 +5,38 @@ module ZuoraObservability
5
5
  class Engine < ::Rails::Engine
6
6
  isolate_namespace ZuoraObservability
7
7
 
8
- REQUEST_HEADERS_TO_IGNORE = %w[
9
- RAW_POST_DATA
10
- REQUEST_METHOD
11
- REQUEST_URI
12
- REQUEST_PATH
13
- PATH_INFO
14
- CONTENT_TYPE
15
- ORIGINAL_FULLPATH
16
- QUERY_STRING
17
- ].freeze
18
-
19
8
  config.generators do |g|
20
9
  g.test_framework :rspec
21
10
  end
22
11
 
23
12
  initializer(:rails_stdout_logging, before: :initialize_logger) do
24
- require 'lograge'
13
+ Rails.application.configure do
14
+ config.logger = ZuoraObservability::Logger.custom_logger(name: 'Rails')
25
15
 
26
- Rails.configuration.logger = ZuoraObservability::Logger.custom_logger(name: "Rails")
16
+ next unless ZuoraObservability.configuration.json_logging
27
17
 
28
- if ZuoraObservability.configuration.json_logging
29
- Rails.configuration.lograge.enabled = true
30
- Rails.configuration.colorize_logging = false
31
- end
18
+ require 'lograge'
19
+ require 'zuora_observability/logging/custom_options'
32
20
 
33
- if Rails.configuration.lograge.enabled
34
- if Rails.configuration.logger.class.to_s == 'Ougai::Logger'
35
- Rails.configuration.lograge.formatter = Class.new do |fmt|
36
- def fmt.call(data)
37
- {
38
- msg: 'Rails Request',
39
- trace_id: data.delete(:trace_id),
40
- zuora_trace_id: data.delete(:zuora_trace_id),
41
- request: data
42
- }.compact
43
- end
44
- end
45
- end
46
- #Rails.configuration.lograge.formatter = Lograge::Formatters::Json.new
47
- Rails.configuration.lograge.custom_options = lambda do |event|
48
- exceptions = %w(controller action format)
49
- items = {
50
- #time: event.time.strftime('%FT%T.%6N'),
51
- params: event.payload[:params].as_json(except: exceptions).to_json.to_s,
52
- trace_id: event.payload[:headers]['action_dispatch.request_id'],
53
- zuora_trace_id: event.payload[:headers]['HTTP_ZUORA_REQUEST_ID']
54
- }
55
- items.merge!({exception_object: event.payload[:exception_object]}) if event.payload[:exception_object].present?
56
- items.merge!({exception: event.payload[:exception]}) if event.payload[:exception].present?
21
+ config.lograge.enabled = true
22
+ config.colorize_logging = false
57
23
 
58
- if event.payload[:headers].present?
59
- # By convertion, headers usually do not have dots. Nginx even rejects headers with dots
60
- # All Rails headers are namespaced, like 'rack.input'.
61
- # Thus, we can obtain the client headers by rejecting dots
62
- request_headers =
63
- event.payload[:headers].env.
64
- reject { |key| key.to_s.include?('.') || REQUEST_HEADERS_TO_IGNORE.include?(key.to_s) }
65
- begin
66
- if request_headers["HTTP_AUTHORIZATION"].present?
67
- if request_headers["HTTP_AUTHORIZATION"].include?("Basic")
68
- user_password = request_headers["HTTP_AUTHORIZATION"].split("Basic").last.strip
69
- user, password = Base64.decode64(user_password).split(":")
70
- request_headers["HTTP_AUTHORIZATION"] = "Basic #{user}:ValueFiltered"
71
- elsif
72
- request_headers["HTTP_AUTHORIZATION"] = "ValueFiltered"
73
- end
74
- end
75
- request_headers["HTTP_API_TOKEN"] = "ValueFiltered" if request_headers["HTTP_API_TOKEN"].present?
76
- rescue
77
- request_headers.delete("HTTP_API_TOKEN")
78
- request_headers.delete("HTTP_AUTHORIZATION")
79
- end
80
- items.merge!({ headers: request_headers.to_s })
81
- end
24
+ config.lograge.formatter = Lograge::Formatters::Raw.new
82
25
 
83
- if Thread.current[:appinstance].present?
84
- items.merge!({connect_user: Thread.current[:appinstance].connect_user, new_session: Thread.current[:appinstance].new_session_message})
85
- if Thread.current[:appinstance].logitems.present? && Thread.current[:appinstance].logitems.class == Hash
86
- items.merge!(Thread.current[:appinstance].logitems)
87
- end
26
+ config.lograge.custom_options = Logging::CustomOptions
27
+ config.lograge.custom_payload do |controller|
28
+ payload = {}
29
+
30
+ ZuoraObservability.configuration.custom_payload_hooks.each do |hook|
31
+ payload.merge!(hook.call(controller))
88
32
  end
89
- 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
90
40
  end
91
41
  end
92
42
  end
@@ -4,6 +4,18 @@ 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
20
  # parent_name is deprecated in Rails 6.0, removed in 6.1
9
21
  ENV['DEIS_APP'].presence || Rails.application.class.parent_name
@@ -1,44 +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
- else
25
- logger.formatter = Ougai::Formatters::Customizable.new(
26
- format_err: proc do |data|
27
- next nil unless data.key?(:err)
28
- err = data.delete(:err)
29
- " #{err[:name]} (#{err[:message]})\n #{err[:stack]}"
30
- end,
31
- format_data: proc do |data|
32
- data.delete(:app_instance_id); data.delete(:tenant_ids); data.delete(:organization); data.delete(:environment)
33
- format('%s %s: %s', 'DATA'.ljust(6), Time.now.strftime('%FT%T.%6NZ'), "#{data.to_json}") if data.present?
34
- end,
35
- format_msg: proc do |severity, datetime, _progname, data|
36
- msg = data.delete(:msg)
37
- format('%s %s: %s', severity.ljust(6), datetime, msg)
38
- end
39
- )
40
- logger.formatter.datetime_format = '%FT%T.%6NZ'
41
- end
60
+ logger = new($stdout, level: level, progname: name)
42
61
  else
43
62
  require 'mono_logger'
44
63
  logger = MonoLogger.new(STDOUT)
@@ -63,7 +82,8 @@ module ZuoraObservability
63
82
  end
64
83
  end
65
84
  end
66
- return logger
85
+
86
+ logger
67
87
  end
68
88
  end
69
89
  end
@@ -0,0 +1,82 @@
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
+ if Thread.current[:appinstance].present?
41
+ items.merge!({connect_user: Thread.current[:appinstance].connect_user, new_session: Thread.current[:appinstance].new_session_message})
42
+ if Thread.current[:appinstance].logitems.present? && Thread.current[:appinstance].logitems.class == Hash
43
+ items.merge!(Thread.current[:appinstance].logitems)
44
+ end
45
+ end
46
+
47
+ items
48
+ end
49
+
50
+ private
51
+
52
+ def filter_request_headers(action_dispatch_headers)
53
+ action_dispatch_headers.env.select do |header|
54
+ next false if IGNORE_HEADERS.include? header
55
+
56
+ header =~ /^HTTP_/ || ActionDispatch::Http::Headers::CGI_VARIABLES.include?(header)
57
+ end
58
+ end
59
+
60
+ def filter_sensitive_headers(headers)
61
+ filtered = {}
62
+
63
+ auth = headers['HTTP_AUTHORIZATION']
64
+ filtered['HTTP_AUTHORIZATION'] = filter_auth_header(auth) if auth
65
+
66
+ filtered['HTTP_API_TOKEN'] = FILTERED if headers['HTTP_API_TOKEN']
67
+
68
+ headers.merge(filtered)
69
+ end
70
+
71
+ def filter_auth_header(authorization)
72
+ return FILTERED unless authorization.include? 'Basic'
73
+
74
+ encoded = authorization.split(' ', 2).second
75
+ user, _password = Base64.decode64(encoded).split(':')
76
+
77
+ "Basic #{user}:#{FILTERED}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,45 +1,169 @@
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.0'
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
+ environment: data[:environment],
149
+ tenant_id: data[:tenant_ids],
150
+ trace_id: data[:zuora_trace_id]
151
+ }.compact
152
+ end
153
+
154
+ # zuora.http
155
+ def http_fields(data)
156
+ z_http_request = { headers: data[:headers] }.compact
157
+
158
+ {
159
+ request: (z_http_request unless z_http_request.empty?)
160
+ }.compact
161
+ end
162
+
163
+ # Service's Custom Fields
164
+ def service_fields(data)
165
+ data[:zecs_service].presence || {}
166
+ end
43
167
  end
44
168
  end
45
169
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ZuoraObservability
4
- VERSION = '0.1.0-c'
4
+ VERSION = '0.1.0-d'
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.c
4
+ version: 0.1.0.pre.d
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hartley McGuire
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-05 00:00:00.000000000 Z
11
+ date: 2021-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lograge
@@ -261,6 +261,7 @@ 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
267
  - lib/zuora_observability/version.rb
@@ -276,7 +277,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
276
277
  requirements:
277
278
  - - ">="
278
279
  - !ruby/object:Gem::Version
279
- version: '2.4'
280
+ version: '2.5'
280
281
  required_rubygems_version: !ruby/object:Gem::Requirement
281
282
  requirements:
282
283
  - - ">"