kubernetes-deploy 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +8 -0
  3. data/CHANGELOG.md +16 -0
  4. data/README.md +32 -0
  5. data/exe/kubernetes-deploy +2 -15
  6. data/exe/kubernetes-render +32 -0
  7. data/kubernetes-deploy.gemspec +5 -3
  8. data/lib/kubernetes-deploy.rb +5 -3
  9. data/lib/kubernetes-deploy/cluster_resource_discovery.rb +34 -0
  10. data/lib/kubernetes-deploy/container_logs.rb +25 -13
  11. data/lib/kubernetes-deploy/deploy_task.rb +68 -50
  12. data/lib/kubernetes-deploy/errors.rb +1 -0
  13. data/lib/kubernetes-deploy/formatted_logger.rb +16 -2
  14. data/lib/kubernetes-deploy/kubeclient_builder/google_friendly_config.rb +4 -6
  15. data/lib/kubernetes-deploy/kubectl.rb +20 -9
  16. data/lib/kubernetes-deploy/kubernetes_resource.rb +5 -6
  17. data/lib/kubernetes-deploy/kubernetes_resource/cloudsql.rb +3 -4
  18. data/lib/kubernetes-deploy/kubernetes_resource/daemon_set.rb +4 -5
  19. data/lib/kubernetes-deploy/kubernetes_resource/deployment.rb +7 -8
  20. data/lib/kubernetes-deploy/kubernetes_resource/memcached.rb +4 -5
  21. data/lib/kubernetes-deploy/kubernetes_resource/pod.rb +7 -5
  22. data/lib/kubernetes-deploy/kubernetes_resource/pod_set_base.rb +12 -6
  23. data/lib/kubernetes-deploy/kubernetes_resource/redis.rb +5 -6
  24. data/lib/kubernetes-deploy/kubernetes_resource/replica_set.rb +23 -5
  25. data/lib/kubernetes-deploy/kubernetes_resource/role.rb +22 -0
  26. data/lib/kubernetes-deploy/kubernetes_resource/service.rb +8 -4
  27. data/lib/kubernetes-deploy/kubernetes_resource/stateful_set.rb +2 -3
  28. data/lib/kubernetes-deploy/oj.rb +4 -0
  29. data/lib/kubernetes-deploy/options_helper.rb +27 -0
  30. data/lib/kubernetes-deploy/remote_logs.rb +10 -4
  31. data/lib/kubernetes-deploy/render_task.rb +119 -0
  32. data/lib/kubernetes-deploy/renderer.rb +1 -1
  33. data/lib/kubernetes-deploy/resource_cache.rb +64 -0
  34. data/lib/kubernetes-deploy/resource_watcher.rb +27 -6
  35. data/lib/kubernetes-deploy/restart_task.rb +5 -6
  36. data/lib/kubernetes-deploy/runner_task.rb +6 -10
  37. data/lib/kubernetes-deploy/statsd.rb +60 -7
  38. data/lib/kubernetes-deploy/template_discovery.rb +15 -0
  39. data/lib/kubernetes-deploy/version.rb +1 -1
  40. data/pull_request_template.md +8 -0
  41. metadata +47 -5
  42. data/lib/kubernetes-deploy/resource_discovery.rb +0 -19
  43. data/lib/kubernetes-deploy/sync_mediator.rb +0 -80
@@ -6,10 +6,9 @@ module KubernetesDeploy
6
6
  ONDELETE = 'OnDelete'
7
7
  attr_reader :pods
8
8
 
9
- SYNC_DEPENDENCIES = %w(Pod)
10
- def sync(mediator)
9
+ def sync(cache)
11
10
  super
12
- @pods = exists? ? find_pods(mediator) : []
11
+ @pods = exists? ? find_pods(cache) : []
13
12
  end
14
13
 
15
14
  def status
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ require 'oj'
3
+
4
+ Oj.mimic_JSON
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubernetesDeploy
4
+ module OptionsHelper
5
+ def self.default_and_check_template_dir(template_dir)
6
+ if !template_dir && ENV.key?("ENVIRONMENT")
7
+ template_dir = "config/deploy/#{ENV['ENVIRONMENT']}"
8
+ end
9
+
10
+ if !template_dir || template_dir.empty?
11
+ puts "Template directory is unknown. " \
12
+ "Either specify --template-dir argument or set $ENVIRONMENT to use config/deploy/$ENVIRONMENT " \
13
+ + "as a default path."
14
+ exit 1
15
+ end
16
+
17
+ template_dir
18
+ end
19
+
20
+ def self.revision_from_environment
21
+ ENV.fetch('REVISION') do
22
+ puts "ENV['REVISION'] is missing. Please specify the commit SHA"
23
+ exit 1
24
+ end
25
+ end
26
+ end
27
+ end
@@ -5,11 +5,17 @@ module KubernetesDeploy
5
5
  class RemoteLogs
6
6
  attr_reader :container_logs
7
7
 
8
- def initialize(logger:, parent_id:, container_names:)
8
+ def initialize(logger:, parent_id:, container_names:, namespace:, context:)
9
9
  @logger = logger
10
10
  @parent_id = parent_id
11
11
  @container_logs = container_names.map do |n|
12
- ContainerLogs.new(logger: logger, container_name: n, parent_id: parent_id)
12
+ ContainerLogs.new(
13
+ logger: logger,
14
+ container_name: n,
15
+ parent_id: parent_id,
16
+ namespace: namespace,
17
+ context: context
18
+ )
13
19
  end
14
20
  end
15
21
 
@@ -17,8 +23,8 @@ module KubernetesDeploy
17
23
  @container_logs.all?(&:empty?)
18
24
  end
19
25
 
20
- def sync(kubectl)
21
- @container_logs.each { |cl| cl.sync(kubectl) }
26
+ def sync
27
+ @container_logs.each(&:sync)
22
28
  end
23
29
 
24
30
  def print_latest
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+ require 'tempfile'
3
+
4
+ require 'kubernetes-deploy/renderer'
5
+ require 'kubernetes-deploy/template_discovery'
6
+
7
+ module KubernetesDeploy
8
+ class RenderTask
9
+ def initialize(logger:, current_sha:, template_dir:, bindings:)
10
+ @logger = logger
11
+ @template_dir = template_dir
12
+ @renderer = KubernetesDeploy::Renderer.new(
13
+ current_sha: current_sha,
14
+ bindings: bindings,
15
+ template_dir: @template_dir,
16
+ logger: @logger,
17
+ )
18
+ end
19
+
20
+ def run(*args)
21
+ run!(*args)
22
+ true
23
+ rescue KubernetesDeploy::FatalDeploymentError
24
+ false
25
+ end
26
+
27
+ def run!(stream, only_filenames = [])
28
+ @logger.reset
29
+ @logger.phase_heading("Initializing render task")
30
+
31
+ filenames = if only_filenames.empty?
32
+ TemplateDiscovery.new(@template_dir).templates
33
+ else
34
+ only_filenames
35
+ end
36
+
37
+ validate_configuration(filenames)
38
+ render_filenames(stream, filenames)
39
+
40
+ @logger.summary.add_action("Successfully rendered #{filenames.size} template(s)")
41
+ @logger.print_summary(:success)
42
+ rescue KubernetesDeploy::FatalDeploymentError
43
+ @logger.print_summary(:failure)
44
+ raise
45
+ end
46
+
47
+ private
48
+
49
+ def render_filenames(stream, filenames)
50
+ exceptions = []
51
+ @logger.phase_heading("Rendering template(s)")
52
+
53
+ filenames.each do |filename|
54
+ begin
55
+ render_filename(filename, stream)
56
+ rescue KubernetesDeploy::InvalidTemplateError => exception
57
+ exceptions << exception
58
+ log_invalid_template(exception)
59
+ end
60
+ end
61
+
62
+ unless exceptions.empty?
63
+ raise exceptions[0]
64
+ end
65
+ end
66
+
67
+ def render_filename(filename, stream)
68
+ @logger.info("Rendering #{File.basename(filename)} ...")
69
+ file_content = File.read(File.join(@template_dir, filename))
70
+ rendered_content = @renderer.render_template(filename, file_content)
71
+ YAML.load_stream(rendered_content, "<rendered> #{filename}") do |doc|
72
+ stream.puts YAML.dump(doc)
73
+ end
74
+ @logger.info("Rendered #{File.basename(filename)}")
75
+ rescue Psych::SyntaxError => exception
76
+ raise InvalidTemplateError.new("Template is not valid YAML. #{exception.message}", filename: filename)
77
+ end
78
+
79
+ def validate_configuration(filenames)
80
+ @logger.info("Validating configuration")
81
+ errors = []
82
+
83
+ if filenames.empty?
84
+ errors << "no templates found in template dir #{@template_dir}"
85
+ end
86
+
87
+ absolute_template_dir = File.expand_path(@template_dir)
88
+
89
+ filenames.each do |filename|
90
+ absolute_file = File.expand_path(File.join(@template_dir, filename))
91
+ if !File.exist?(absolute_file)
92
+ errors << "Filename \"#{absolute_file}\" could not be found"
93
+ elsif !File.file?(absolute_file)
94
+ errors << "Filename \"#{absolute_file}\" is not a file"
95
+ elsif !absolute_file.start_with?(absolute_template_dir)
96
+ errors << "Filename \"#{absolute_file}\" is outside the template directory," \
97
+ " which was resolved as #{absolute_template_dir}"
98
+ end
99
+ end
100
+
101
+ unless errors.empty?
102
+ @logger.summary.add_action("Configuration invalid")
103
+ @logger.summary.add_paragraph(errors.map { |err| "- #{err}" }.join("\n"))
104
+ raise KubernetesDeploy::TaskConfigurationError, "Configuration invalid: #{errors.join(', ')}"
105
+ end
106
+ end
107
+
108
+ def log_invalid_template(exception)
109
+ @logger.error("Failed to render #{exception.filename}")
110
+
111
+ debug_msg = ColorizedString.new("Invalid template: #{exception.filename}\n").red
112
+ debug_msg += "> Error message:\n#{FormattedLogger.indent_four(exception.to_s)}"
113
+ if exception.content
114
+ debug_msg += "\n> Template content:\n#{FormattedLogger.indent_four(exception.content)}"
115
+ end
116
+ @logger.summary.add_paragraph(debug_msg)
117
+ end
118
+ end
119
+ end
@@ -52,7 +52,7 @@ module KubernetesDeploy
52
52
  template = File.read(partial_path)
53
53
  expanded_template = ERB.new(template, nil, '-').result(erb_binding)
54
54
 
55
- docs = Psych.parse_stream(expanded_template)
55
+ docs = Psych.parse_stream(expanded_template, partial_path)
56
56
  # If the partial contains multiple documents or has an explicit document header,
57
57
  # we know it cannot validly be indented in the parent, so return it immediately.
58
58
  return expanded_template unless docs.children.one? && docs.children.first.implicit
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/hash'
4
+
5
+ module KubernetesDeploy
6
+ class ResourceCache
7
+ def initialize(namespace, context, logger)
8
+ @namespace = namespace
9
+ @context = context
10
+ @logger = logger
11
+
12
+ @kind_fetcher_locks = Concurrent::Hash.new { |hash, key| hash[key] = Mutex.new }
13
+ @data = Concurrent::Hash.new
14
+ @kubectl = Kubectl.new(namespace: @namespace, context: @context, logger: @logger, log_failure_by_default: false)
15
+ end
16
+
17
+ def get_instance(kind, resource_name, raise_if_not_found: false)
18
+ instance = use_or_populate_cache(kind).fetch(resource_name, {})
19
+ if instance.blank? && raise_if_not_found
20
+ raise KubernetesDeploy::Kubectl::ResourceNotFoundError, "Resource does not exist (used cache for kind #{kind})"
21
+ end
22
+ instance
23
+ rescue KubectlError
24
+ {}
25
+ end
26
+
27
+ def get_all(kind, selector = nil)
28
+ instances = use_or_populate_cache(kind).values
29
+ return instances unless selector
30
+
31
+ instances.select do |r|
32
+ labels = r.dig("metadata", "labels") || {}
33
+ labels >= selector
34
+ end
35
+ rescue KubectlError
36
+ []
37
+ end
38
+
39
+ private
40
+
41
+ def statsd_tags
42
+ { namespace: @namespace, context: @context }
43
+ end
44
+
45
+ def use_or_populate_cache(kind)
46
+ @kind_fetcher_locks[kind].synchronize do
47
+ return @data[kind] if @data.key?(kind)
48
+ @data[kind] = fetch_by_kind(kind)
49
+ end
50
+ end
51
+
52
+ def fetch_by_kind(kind)
53
+ raw_json, _, st = @kubectl.run("get", kind, "--chunk-size=0", "--output=json", attempts: 5)
54
+ raise KubectlError unless st.success?
55
+
56
+ instances = {}
57
+ JSON.parse(raw_json)["items"].each do |resource|
58
+ resource_name = resource.dig("metadata", "name")
59
+ instances[resource_name] = resource
60
+ end
61
+ instances
62
+ end
63
+ end
64
+ end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
  module KubernetesDeploy
3
3
  class ResourceWatcher
4
- def initialize(resources:, sync_mediator:, logger:, deploy_started_at: Time.now.utc,
5
- operation_name: "deploy", timeout: nil)
4
+ extend KubernetesDeploy::StatsD::MeasureMethods
5
+
6
+ def initialize(resources:, logger:, context:, namespace:,
7
+ deploy_started_at: Time.now.utc, operation_name: "deploy", timeout: nil, sha: nil)
6
8
  unless resources.is_a?(Enumerable)
7
9
  raise ArgumentError, <<~MSG
8
10
  ResourceWatcher expects Enumerable collection, got `#{resources.class}` instead
@@ -10,10 +12,12 @@ module KubernetesDeploy
10
12
  end
11
13
  @resources = resources
12
14
  @logger = logger
13
- @sync_mediator = sync_mediator
15
+ @namespace = namespace
16
+ @context = context
14
17
  @deploy_started_at = deploy_started_at
15
18
  @operation_name = operation_name
16
19
  @timeout = timeout
20
+ @sha = sha
17
21
  end
18
22
 
19
23
  def run(delay_sync: 3.seconds, reminder_interval: 30.seconds, record_summary: true)
@@ -24,8 +28,7 @@ module KubernetesDeploy
24
28
  report_and_give_up(remainder) if global_timeout?(monitoring_started)
25
29
  sleep_until_next_sync(delay_sync)
26
30
 
27
- @sync_mediator.sync(remainder)
28
- remainder.each(&:after_sync)
31
+ sync_resources(remainder)
29
32
 
30
33
  new_successes, remainder = remainder.partition(&:deploy_succeeded?)
31
34
  new_failures, remainder = remainder.partition(&:deploy_failed?)
@@ -45,6 +48,21 @@ module KubernetesDeploy
45
48
 
46
49
  private
47
50
 
51
+ def sync_resources(resources)
52
+ cache = ResourceCache.new(@namespace, @context, @logger)
53
+ KubernetesDeploy::Concurrency.split_across_threads(resources) { |r| r.sync(cache) }
54
+ resources.each(&:after_sync)
55
+ end
56
+ measure_method(:sync_resources, "sync.duration")
57
+
58
+ def statsd_tags
59
+ {
60
+ namespace: @namespace,
61
+ context: @context,
62
+ sha: @sha
63
+ }
64
+ end
65
+
48
66
  def global_timeout?(started_at)
49
67
  @timeout && (Time.now.utc - started_at > @timeout)
50
68
  end
@@ -118,9 +136,12 @@ module KubernetesDeploy
118
136
  "failed to #{@operation_name} #{failures.length} #{'resource'.pluralize(failures.length)}"
119
137
  )
120
138
  end
139
+
140
+ kubectl = Kubectl.new(namespace: @namespace, context: @context, logger: @logger, log_failure_by_default: false)
121
141
  KubernetesDeploy::Concurrency.split_across_threads(failed_resources + global_timeouts) do |r|
122
- r.sync_debug_info(@sync_mediator.kubectl)
142
+ r.sync_debug_info(kubectl)
123
143
  end
144
+
124
145
  failed_resources.each { |r| @logger.summary.add_paragraph(r.debug_message) }
125
146
  global_timeouts.each { |r| @logger.summary.add_paragraph(r.debug_message(:gave_up, timeout: @timeout)) }
126
147
  end
@@ -24,7 +24,6 @@ module KubernetesDeploy
24
24
  @context = context
25
25
  @namespace = namespace
26
26
  @logger = logger
27
- @sync_mediator = SyncMediator.new(namespace: @namespace, context: @context, logger: @logger)
28
27
  @max_watch_seconds = max_watch_seconds
29
28
  end
30
29
 
@@ -50,22 +49,22 @@ module KubernetesDeploy
50
49
 
51
50
  @logger.phase_heading("Waiting for rollout")
52
51
  resources = build_watchables(deployments, start)
53
- ResourceWatcher.new(resources: resources, sync_mediator: @sync_mediator,
54
- logger: @logger, operation_name: "restart", timeout: @max_watch_seconds).run
52
+ ResourceWatcher.new(resources: resources, logger: @logger, operation_name: "restart",
53
+ timeout: @max_watch_seconds, namespace: @namespace, context: @context).run
55
54
  failed_resources = resources.reject(&:deploy_succeeded?)
56
55
  success = failed_resources.empty?
57
56
  if !success && failed_resources.all?(&:deploy_timed_out?)
58
57
  raise DeploymentTimeoutError
59
58
  end
60
59
  raise FatalDeploymentError unless success
61
- ::StatsD.distribution('restart.duration', StatsD.duration(start), tags: tags('success', deployments))
60
+ StatsD.distribution('restart.duration', StatsD.duration(start), tags: tags('success', deployments))
62
61
  @logger.print_summary(:success)
63
62
  rescue DeploymentTimeoutError
64
- ::StatsD.distribution('restart.duration', StatsD.duration(start), tags: tags('timeout', deployments))
63
+ StatsD.distribution('restart.duration', StatsD.duration(start), tags: tags('timeout', deployments))
65
64
  @logger.print_summary(:timed_out)
66
65
  raise
67
66
  rescue FatalDeploymentError => error
68
- ::StatsD.distribution('restart.duration', StatsD.duration(start), tags: tags('failure', deployments))
67
+ StatsD.distribution('restart.duration', StatsD.duration(start), tags: tags('failure', deployments))
69
68
  @logger.summary.add_action(error.message) if error.message != error.class.to_s
70
69
  @logger.print_summary(:failure)
71
70
  raise
@@ -8,7 +8,6 @@ module KubernetesDeploy
8
8
  class RunnerTask
9
9
  include KubeclientBuilder
10
10
 
11
- class TaskConfigurationError < FatalDeploymentError; end
12
11
  class TaskTemplateMissingError < TaskConfigurationError; end
13
12
 
14
13
  attr_reader :pod_name
@@ -45,14 +44,14 @@ module KubernetesDeploy
45
44
  else
46
45
  record_status_once(pod)
47
46
  end
48
- ::StatsD.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('success'))
47
+ StatsD.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('success'))
49
48
  @logger.print_summary(:success)
50
49
  rescue DeploymentTimeoutError
51
- ::StatsD.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('timeout'))
50
+ StatsD.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('timeout'))
52
51
  @logger.print_summary(:timed_out)
53
52
  raise
54
53
  rescue FatalDeploymentError
55
- ::StatsD.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('failure'))
54
+ StatsD.distribution('task_runner.duration', StatsD.duration(start), tags: statsd_tags('failure'))
56
55
  @logger.print_summary(:failure)
57
56
  raise
58
57
  end
@@ -87,14 +86,15 @@ module KubernetesDeploy
87
86
 
88
87
  def watch_pod(pod)
89
88
  rw = ResourceWatcher.new(resources: [pod], logger: @logger, timeout: @max_watch_seconds,
90
- sync_mediator: sync_mediator, operation_name: "run")
89
+ operation_name: "run", namespace: @namespace, context: @context)
91
90
  rw.run(delay_sync: 1, reminder_interval: 30.seconds)
92
91
  raise DeploymentTimeoutError if pod.deploy_timed_out?
93
92
  raise FatalDeploymentError if pod.deploy_failed?
94
93
  end
95
94
 
96
95
  def record_status_once(pod)
97
- pod.sync(sync_mediator)
96
+ cache = ResourceCache.new(@namespace, @context, @logger)
97
+ pod.sync(cache)
98
98
  warning = <<~STRING
99
99
  #{ColorizedString.new('Result verification is disabled for this task.').yellow}
100
100
  The following status was observed immediately after pod creation:
@@ -197,10 +197,6 @@ module KubernetesDeploy
197
197
  @kubectl ||= Kubectl.new(namespace: @namespace, context: @context, logger: @logger, log_failure_by_default: true)
198
198
  end
199
199
 
200
- def sync_mediator
201
- @sync_mediator ||= SyncMediator.new(namespace: @namespace, context: @context, logger: @logger)
202
- end
203
-
204
200
  def kubeclient
205
201
  @kubeclient ||= build_v1_kubeclient(@context)
206
202
  end
@@ -4,23 +4,76 @@ require 'logger'
4
4
 
5
5
  module KubernetesDeploy
6
6
  class StatsD
7
+ extend ::StatsD
8
+
9
+ PREFIX = "KubernetesDeploy"
10
+
7
11
  def self.duration(start_time)
8
12
  (Time.now.utc - start_time).round(1)
9
13
  end
10
14
 
11
15
  def self.build
12
- ::StatsD.default_sample_rate = 1.0
13
- ::StatsD.prefix = "KubernetesDeploy"
14
-
15
16
  if ENV['STATSD_DEV'].present?
16
- ::StatsD.backend = ::StatsD::Instrument::Backends::LoggerBackend.new(Logger.new($stderr))
17
+ self.backend = ::StatsD::Instrument::Backends::LoggerBackend.new(Logger.new($stderr))
17
18
  elsif ENV['STATSD_ADDR'].present?
18
19
  statsd_impl = ENV['STATSD_IMPLEMENTATION'].present? ? ENV['STATSD_IMPLEMENTATION'] : "datadog"
19
- ::StatsD.backend = ::StatsD::Instrument::Backends::UDPBackend.new(ENV['STATSD_ADDR'], statsd_impl)
20
+ self.backend = ::StatsD::Instrument::Backends::UDPBackend.new(ENV['STATSD_ADDR'], statsd_impl)
20
21
  else
21
- ::StatsD.backend = ::StatsD::Instrument::Backends::NullBackend.new
22
+ self.backend = ::StatsD::Instrument::Backends::NullBackend.new
23
+ end
24
+ end
25
+
26
+ # It is not sufficient to set the prefix field on the KubernetesDeploy::StatsD singleton itself, since its value
27
+ # is overridden in the underlying calls to the ::StatsD library, hence the need to pass it in as a custom prefix
28
+ # via the metric_options hash. This is done since KubernetesDeploy may be included as a library and should not
29
+ # change the global StatsD configuration of the importing application.
30
+ def self.increment(key, value = 1, **metric_options)
31
+ metric_options[:prefix] = PREFIX
32
+ super
33
+ end
34
+
35
+ def self.distribution(key, value = nil, **metric_options, &block)
36
+ metric_options[:prefix] = PREFIX
37
+ super
38
+ end
39
+
40
+ module MeasureMethods
41
+ def measure_method(method_name, metric = nil)
42
+ unless method_defined?(method_name) || private_method_defined?(method_name)
43
+ raise NotImplementedError, "Cannot instrument undefined method #{method_name}"
44
+ end
45
+
46
+ unless const_defined?("InstrumentationProxy")
47
+ const_set("InstrumentationProxy", Module.new)
48
+ should_prepend = true
49
+ end
50
+
51
+ metric ||= "#{method_name}.duration"
52
+ self::InstrumentationProxy.send(:define_method, method_name) do |*args, &block|
53
+ begin
54
+ start_time = Time.now.utc
55
+ super(*args, &block)
56
+ rescue
57
+ error = true
58
+ raise
59
+ ensure
60
+ dynamic_tags = send(:statsd_tags) if respond_to?(:statsd_tags, true)
61
+ dynamic_tags ||= {}
62
+ if error
63
+ dynamic_tags[:error] = error if dynamic_tags.is_a?(Hash)
64
+ dynamic_tags << "error:#{error}" if dynamic_tags.is_a?(Array)
65
+ end
66
+
67
+ StatsD.distribution(
68
+ metric,
69
+ KubernetesDeploy::StatsD.duration(start_time),
70
+ tags: dynamic_tags
71
+ )
72
+ end
73
+ end
74
+
75
+ prepend(self::InstrumentationProxy) if should_prepend
22
76
  end
23
- ::StatsD.backend
24
77
  end
25
78
  end
26
79
  end