zuora_observability 0.1.0.pre.a

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c7e3a6c154d518f30f5a95e1be272b96b1853b46df0300d4fa45ca8e17f04451
4
+ data.tar.gz: 0c510e0da92d8b7139586e60bd048bc91f18cc61a9cfe8165f4f44676c223180
5
+ SHA512:
6
+ metadata.gz: 3948b6dacece27853e9df426c81d6f475c1b8b5417131c4318eeac8b88c8ffa4c4f625036f95cc5b73ed78ed53352f182c2f7efb16800e313b797a7416372b3b
7
+ data.tar.gz: f67b67d6bbbf2ff33c30433f32f321bf28109540bbf54250764509332de9caba0dbcf94917439882ea7a0f8d6e1bc437eae221134ceb4adfac00933dddadd1f6
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Hartley McGuire
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,26 @@
1
+ # Zuora Observability
2
+
3
+ A ruby gem to enable observability into rails applications
4
+
5
+ ## Usage
6
+ How to use my plugin.
7
+
8
+ ## Installation
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'zuora_observability'
13
+ ```
14
+
15
+ And then execute:
16
+ ```bash
17
+ $ bundle
18
+ ```
19
+
20
+ Or install it yourself as:
21
+ ```bash
22
+ $ gem install zuora_observability
23
+ ```
24
+
25
+ ## License
26
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'ZuoraObservability'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
20
+ load 'rails/tasks/engine.rake'
21
+
22
+ load 'rails/tasks/statistics.rake'
23
+
24
+ require 'bundler/gem_tasks'
25
+
26
+ require 'rake/testtask'
27
+
28
+ Rake::TestTask.new(:test) do |t|
29
+ t.libs << 'test'
30
+ t.pattern = 'test/**/*_test.rb'
31
+ t.verbose = false
32
+ end
33
+
34
+ task default: :test
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/zuora_observability .css
@@ -0,0 +1,15 @@
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
+ */
@@ -0,0 +1,5 @@
1
+ module ZuoraObservability
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require vs. require_dependency for namespaced controllers
4
+ # https://github.com/rails/rails/commit/29d17d3ab65633695babc9123463c78248e41a67
5
+ require_dependency 'zuora_observability/application_controller'
6
+
7
+ module ZuoraObservability
8
+ class MetricsController < ApplicationController
9
+ # GET /connect/internal/data
10
+ def metrics
11
+ if params[:type] == 'stats'
12
+ render json: Metrics.resque, status: :ok
13
+ else
14
+ render json: Metrics.versions, status: :ok
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ module ZuoraObservability
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ZuoraObservability
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module ZuoraObservability
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module ZuoraObservability
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Zuora observability</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "zuora_observability/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ ZuoraObservability::Engine.routes.draw do
4
+ get 'metrics', to: 'metrics#metrics'
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # desc "Explaining what the task does"
4
+ # task :zuora_observability do
5
+ # # Task goes here
6
+ # end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zuora_observability/configuration'
4
+ require 'zuora_observability/engine'
5
+ require 'zuora_observability/env'
6
+ require 'zuora_observability/metrics'
7
+ require 'zuora_observability/logger'
8
+
9
+ require 'zuora_observability/logging/formatter'
10
+ require 'zuora_observability/metrics/telegraf'
11
+ require 'zuora_observability/metrics/point_value'
12
+
13
+ # Provides Rails application with tools for observabilty
14
+ module ZuoraObservability
15
+ class << self
16
+ attr_writer :configuration
17
+
18
+ def configuration
19
+ @configuration ||= Configuration.new
20
+ end
21
+
22
+ def configure
23
+ yield(configuration)
24
+ end
25
+
26
+ def reset
27
+ @configuration = Configuration.new
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZuoraObservability
4
+ # Global configuration that can be set in a Rails initializer
5
+ class Configuration
6
+ attr_accessor :enable_metrics, :telegraf_endpoint, :telegraf_debug,
7
+ :json_logging
8
+
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
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZuoraObservability
4
+ # The ZuoraObservability Engine is mounted to hook into Rails
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace ZuoraObservability
7
+
8
+ config.generators do |g|
9
+ g.test_framework :rspec
10
+ end
11
+
12
+ initializer(:rails_stdout_logging, before: :initialize_logger) do
13
+ require 'lograge'
14
+
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
20
+
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?
38
+
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
63
+
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
69
+ end
70
+ return items
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZuoraObservability
4
+ # Methods to get information about the application environment
5
+ class Env
6
+ class << self
7
+ 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
10
+ end
11
+
12
+ def pod_name
13
+ ENV['HOSTNAME'].presence || Socket.gethostname
14
+ end
15
+
16
+ def full_process_name(process_name: nil, function: nil)
17
+ keys = [pod_name, process_name.presence || process_type, Process.pid, function]
18
+ keys.compact.join('][').prepend('[').concat(']')
19
+ end
20
+
21
+ # Returns the process type if any
22
+ def process_type(default: 'Unknown')
23
+ p_type = default
24
+ if ENV['HOSTNAME'] && ENV['DEIS_APP']
25
+ temp = ENV['HOSTNAME'].split(ENV['DEIS_APP'])[1]
26
+ temp = temp.split(/(-[0-9a-zA-Z]{5})$/)[0] # remove the 5 char hash
27
+ p_type = temp[1, temp.rindex("-")-1]
28
+ end
29
+ return p_type
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mono_logger'
4
+
5
+ module ZuoraObservability
6
+ # 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] + ' - '
12
+ 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
44
+ else
45
+ require 'mono_logger'
46
+ logger = MonoLogger.new(STDOUT)
47
+ logger.level = level
48
+ logger.formatter = proc do |serverity, datetime, progname, msg|
49
+ begin
50
+ msg = JSON.parse(msg)
51
+ rescue JSON::ParserError => ex
52
+ end
53
+ if ZuoraObservability.configuration.json_logging
54
+ require 'json'
55
+ store = {
56
+ name: name,
57
+ level: serverity,
58
+ timestamp: datetime.strftime('%FT%T.%6NZ'),
59
+ pid: Process.pid,
60
+ message: name == "ActionMailer" ? msg.strip : msg
61
+ }
62
+ JSON.dump(store) + "\n"
63
+ else
64
+ format('%s %s: %s', serverity.ljust(6), datetime, msg) + "\n"
65
+ end
66
+ end
67
+ end
68
+ return logger
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ougai/formatters/base'
4
+ require 'ougai/formatters/for_json'
5
+
6
+ module ZuoraObservability
7
+ 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
26
+
27
+ 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))
39
+ end
40
+
41
+ def convert_time(data)
42
+ # data[:timestamp] = format_datetime(data[:time])
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZuoraObservability
4
+ # Methods to gather and format metrics
5
+ module Metrics
6
+ @@telegraf_host = nil
7
+
8
+ 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
+ def resque
19
+ Resque.redis.ping
20
+
21
+ resque = Resque.info
22
+
23
+ {
24
+ app_name: ZuoraObservability::Env.app_name, url: 'dummy', Resque: {
25
+ Jobs_Finished: resque[:processed], Jobs_Failed: resque[:failed],
26
+ Jobs_Pending: resque[:pending], Workers_Active: resque[:working],
27
+ Workers_Total: resque[:workers]
28
+ }
29
+ }
30
+ end
31
+
32
+ def versions
33
+ {
34
+ app_name: ZuoraObservability::Env.app_name,
35
+ url: 'dummy',
36
+ Version_Gem: ZuoraConnect::VERSION,
37
+ Version_Zuora: ZuoraAPI::VERSION,
38
+ Version_Ruby: RUBY_VERSION,
39
+ Version_Rails: Rails.version,
40
+ hold: 1
41
+ }
42
+ end
43
+
44
+ def unicorn_listener
45
+ stats_hash = {}
46
+ stats_hash["total_active"] = 0
47
+ stats_hash["total_queued"] = 0
48
+
49
+ begin
50
+ tmp = Unicorn.listener_names
51
+ unix = tmp.grep(%r{\A/})
52
+ tcp = tmp.grep(/\A.+:\d+\z/)
53
+ tcp = nil if tcp.empty?
54
+ unix = nil if unix.empty?
55
+
56
+
57
+ Raindrops::Linux.tcp_listener_stats(tcp).each do |addr,stats|
58
+ stats_hash["active_#{addr}"] = stats.active
59
+ stats_hash["queued_#{addr}"] = stats.queued
60
+ stats_hash["total_active"] = stats.active + stats_hash["total_active"]
61
+ stats_hash["total_queued"] = stats.queued + stats_hash["total_queued"]
62
+ end if tcp
63
+
64
+ Raindrops::Linux.unix_listener_stats(unix).each do |addr,stats|
65
+ stats_hash["active_#{addr}"] = stats.active
66
+ stats_hash["queued_#{addr}"] = stats.queued
67
+ stats_hash["total_active"] = stats.active + stats_hash["total_active"]
68
+ stats_hash["total_queued"] = stats.queued + stats_hash["total_queued"]
69
+ end if unix
70
+ rescue IOError => ex
71
+ rescue => ex
72
+ Rails.logger.error(ex)
73
+ puts ex
74
+ end
75
+ return stats_hash
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,84 @@
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
@@ -0,0 +1,98 @@
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZuoraObservability
4
+ VERSION = '0.1.0-a'
5
+ end
metadata ADDED
@@ -0,0 +1,291 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zuora_observability
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.a
5
+ platform: ruby
6
+ authors:
7
+ - Hartley McGuire
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-11-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: lograge
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ougai
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ougai-formatters-customizable
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 1.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ 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
+ - !ruby/object:Gem::Dependency
70
+ name: rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '5'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: brakeman
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: resque
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec_junit_formatter
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec-rails
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.2'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.2'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop-rails
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rubocop-rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 2.0.0.pre
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 2.0.0.pre
181
+ - !ruby/object:Gem::Dependency
182
+ name: simplecov
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: simplecov-cobertura
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: sqlite3
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: unicorn
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ description: Description of ZuoraObservability.
238
+ email:
239
+ - hmcguire@zuora.com
240
+ executables: []
241
+ extensions: []
242
+ extra_rdoc_files: []
243
+ files:
244
+ - MIT-LICENSE
245
+ - README.md
246
+ - Rakefile
247
+ - app/assets/config/zuora_observability_manifest.js
248
+ - app/assets/stylesheets/zuora_observability/application.css
249
+ - app/controllers/zuora_observability/application_controller.rb
250
+ - app/controllers/zuora_observability/metrics_controller.rb
251
+ - 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
+ - app/views/layouts/zuora_observability/application.html.erb
256
+ - config/routes.rb
257
+ - lib/tasks/zuora_observability_tasks.rake
258
+ - lib/zuora_observability.rb
259
+ - lib/zuora_observability/configuration.rb
260
+ - lib/zuora_observability/engine.rb
261
+ - lib/zuora_observability/env.rb
262
+ - lib/zuora_observability/logger.rb
263
+ - lib/zuora_observability/logging/formatter.rb
264
+ - lib/zuora_observability/metrics.rb
265
+ - lib/zuora_observability/metrics/point_value.rb
266
+ - lib/zuora_observability/metrics/telegraf.rb
267
+ - lib/zuora_observability/version.rb
268
+ homepage:
269
+ licenses:
270
+ - MIT
271
+ metadata: {}
272
+ post_install_message:
273
+ rdoc_options: []
274
+ require_paths:
275
+ - lib
276
+ required_ruby_version: !ruby/object:Gem::Requirement
277
+ requirements:
278
+ - - ">="
279
+ - !ruby/object:Gem::Version
280
+ version: '2.4'
281
+ required_rubygems_version: !ruby/object:Gem::Requirement
282
+ requirements:
283
+ - - ">"
284
+ - !ruby/object:Gem::Version
285
+ version: 1.3.1
286
+ requirements: []
287
+ rubygems_version: 3.1.4
288
+ signing_key:
289
+ specification_version: 4
290
+ summary: Summary of ZuoraObservability.
291
+ test_files: []