zuora_observability 0.1.0.pre.a

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []