ddtrace 0.47.0 → 0.48.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 (100) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +4 -2
  3. data/.circleci/images/primary/Dockerfile-2.0.0 +11 -1
  4. data/.circleci/images/primary/Dockerfile-2.1.10 +11 -1
  5. data/.circleci/images/primary/Dockerfile-2.2.10 +11 -1
  6. data/.circleci/images/primary/Dockerfile-2.3.8 +10 -0
  7. data/.circleci/images/primary/Dockerfile-2.4.6 +10 -0
  8. data/.circleci/images/primary/Dockerfile-2.5.6 +10 -0
  9. data/.circleci/images/primary/Dockerfile-2.6.4 +10 -0
  10. data/.circleci/images/primary/Dockerfile-2.7.0 +10 -0
  11. data/.circleci/images/primary/Dockerfile-jruby-9.2-latest +10 -0
  12. data/.gitlab-ci.yml +18 -18
  13. data/.rubocop.yml +19 -0
  14. data/.rubocop_todo.yml +44 -3
  15. data/Appraisals +55 -1
  16. data/CHANGELOG.md +47 -1
  17. data/Gemfile +10 -0
  18. data/Rakefile +9 -0
  19. data/bin/ddtracerb +15 -0
  20. data/ddtrace.gemspec +4 -2
  21. data/docs/GettingStarted.md +36 -53
  22. data/docs/ProfilingDevelopment.md +88 -0
  23. data/integration/README.md +1 -2
  24. data/integration/apps/rack/Dockerfile +3 -0
  25. data/integration/apps/rack/script/build-images +1 -1
  26. data/integration/apps/rack/script/ci +1 -1
  27. data/integration/apps/rails-five/script/build-images +1 -1
  28. data/integration/apps/rails-five/script/ci +1 -1
  29. data/integration/apps/ruby/script/build-images +1 -1
  30. data/integration/apps/ruby/script/ci +1 -1
  31. data/integration/images/include/http-health-check +1 -1
  32. data/integration/images/wrk/scripts/entrypoint.sh +1 -1
  33. data/integration/script/build-images +1 -1
  34. data/lib/ddtrace.rb +1 -0
  35. data/lib/ddtrace/configuration.rb +39 -13
  36. data/lib/ddtrace/configuration/components.rb +85 -3
  37. data/lib/ddtrace/configuration/settings.rb +31 -0
  38. data/lib/ddtrace/contrib/active_record/configuration/makara_resolver.rb +30 -0
  39. data/lib/ddtrace/contrib/active_record/configuration/resolver.rb +9 -3
  40. data/lib/ddtrace/contrib/resque/configuration/settings.rb +17 -1
  41. data/lib/ddtrace/contrib/resque/patcher.rb +4 -4
  42. data/lib/ddtrace/contrib/resque/resque_job.rb +22 -1
  43. data/lib/ddtrace/contrib/shoryuken/configuration/settings.rb +1 -0
  44. data/lib/ddtrace/contrib/shoryuken/tracer.rb +7 -3
  45. data/lib/ddtrace/diagnostics/environment_logger.rb +1 -1
  46. data/lib/ddtrace/error.rb +2 -0
  47. data/lib/ddtrace/ext/profiling.rb +52 -0
  48. data/lib/ddtrace/ext/transport.rb +1 -0
  49. data/lib/ddtrace/metrics.rb +4 -0
  50. data/lib/ddtrace/profiling.rb +54 -0
  51. data/lib/ddtrace/profiling/backtrace_location.rb +32 -0
  52. data/lib/ddtrace/profiling/buffer.rb +41 -0
  53. data/lib/ddtrace/profiling/collectors/stack.rb +253 -0
  54. data/lib/ddtrace/profiling/encoding/profile.rb +31 -0
  55. data/lib/ddtrace/profiling/event.rb +13 -0
  56. data/lib/ddtrace/profiling/events/stack.rb +102 -0
  57. data/lib/ddtrace/profiling/exporter.rb +23 -0
  58. data/lib/ddtrace/profiling/ext/cpu.rb +54 -0
  59. data/lib/ddtrace/profiling/ext/cthread.rb +134 -0
  60. data/lib/ddtrace/profiling/ext/forking.rb +97 -0
  61. data/lib/ddtrace/profiling/flush.rb +41 -0
  62. data/lib/ddtrace/profiling/pprof/builder.rb +121 -0
  63. data/lib/ddtrace/profiling/pprof/converter.rb +85 -0
  64. data/lib/ddtrace/profiling/pprof/message_set.rb +12 -0
  65. data/lib/ddtrace/profiling/pprof/payload.rb +18 -0
  66. data/lib/ddtrace/profiling/pprof/pprof.proto +212 -0
  67. data/lib/ddtrace/profiling/pprof/pprof_pb.rb +81 -0
  68. data/lib/ddtrace/profiling/pprof/stack_sample.rb +90 -0
  69. data/lib/ddtrace/profiling/pprof/string_table.rb +10 -0
  70. data/lib/ddtrace/profiling/pprof/template.rb +114 -0
  71. data/lib/ddtrace/profiling/preload.rb +3 -0
  72. data/lib/ddtrace/profiling/profiler.rb +28 -0
  73. data/lib/ddtrace/profiling/recorder.rb +87 -0
  74. data/lib/ddtrace/profiling/scheduler.rb +84 -0
  75. data/lib/ddtrace/profiling/tasks/setup.rb +77 -0
  76. data/lib/ddtrace/profiling/transport/client.rb +12 -0
  77. data/lib/ddtrace/profiling/transport/http.rb +122 -0
  78. data/lib/ddtrace/profiling/transport/http/api.rb +43 -0
  79. data/lib/ddtrace/profiling/transport/http/api/endpoint.rb +90 -0
  80. data/lib/ddtrace/profiling/transport/http/api/instance.rb +36 -0
  81. data/lib/ddtrace/profiling/transport/http/api/spec.rb +40 -0
  82. data/lib/ddtrace/profiling/transport/http/builder.rb +28 -0
  83. data/lib/ddtrace/profiling/transport/http/client.rb +33 -0
  84. data/lib/ddtrace/profiling/transport/http/response.rb +21 -0
  85. data/lib/ddtrace/profiling/transport/io.rb +30 -0
  86. data/lib/ddtrace/profiling/transport/io/client.rb +27 -0
  87. data/lib/ddtrace/profiling/transport/io/response.rb +16 -0
  88. data/lib/ddtrace/profiling/transport/parcel.rb +17 -0
  89. data/lib/ddtrace/profiling/transport/request.rb +15 -0
  90. data/lib/ddtrace/profiling/transport/response.rb +8 -0
  91. data/lib/ddtrace/runtime/container.rb +11 -3
  92. data/lib/ddtrace/sampling/rule_sampler.rb +3 -9
  93. data/lib/ddtrace/tasks/exec.rb +48 -0
  94. data/lib/ddtrace/tasks/help.rb +14 -0
  95. data/lib/ddtrace/tracer.rb +21 -0
  96. data/lib/ddtrace/transport/io/client.rb +15 -8
  97. data/lib/ddtrace/transport/parcel.rb +4 -0
  98. data/lib/ddtrace/version.rb +3 -1
  99. data/lib/ddtrace/workers/runtime_metrics.rb +14 -1
  100. metadata +70 -9
@@ -0,0 +1,77 @@
1
+ require 'ddtrace'
2
+ require 'ddtrace/utils/only_once'
3
+ require 'ddtrace/profiling'
4
+ require 'ddtrace/profiling/ext/cpu'
5
+ require 'ddtrace/profiling/ext/forking'
6
+
7
+ module Datadog
8
+ module Profiling
9
+ module Tasks
10
+ # Takes care of loading our extensions/monkey patches to handle fork() and CPU profiling.
11
+ class Setup
12
+ ACTIVATE_EXTENSIONS_ONLY_ONCE = Datadog::Utils::OnlyOnce.new
13
+
14
+ def run
15
+ ACTIVATE_EXTENSIONS_ONLY_ONCE.run do
16
+ begin
17
+ activate_forking_extensions
18
+ activate_cpu_extensions
19
+ setup_at_fork_hooks
20
+ rescue StandardError, ScriptError => e
21
+ log "[DDTRACE] Main extensions unavailable. Cause: #{e.message} Location: #{e.backtrace.first}"
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def activate_forking_extensions
29
+ if Ext::Forking.supported?
30
+ Ext::Forking.apply!
31
+ elsif Datadog.configuration.profiling.enabled
32
+ # Log warning if profiling was supposed to be activated.
33
+ log '[DDTRACE] Forking extensions skipped; forking not supported.'
34
+ end
35
+ rescue StandardError, ScriptError => e
36
+ log "[DDTRACE] Forking extensions unavailable. Cause: #{e.message} Location: #{e.backtrace.first}"
37
+ end
38
+
39
+ def activate_cpu_extensions
40
+ if Ext::CPU.supported?
41
+ Ext::CPU.apply!
42
+ elsif Datadog.configuration.profiling.enabled
43
+ # Log warning if profiling was supposed to be activated.
44
+ log "[DDTRACE] CPU profiling skipped because native CPU time is not supported: #{Ext::CPU.unsupported_reason}."
45
+ end
46
+ rescue StandardError, ScriptError => e
47
+ log "[DDTRACE] CPU profiling unavailable. Cause: #{e.message} Location: #{e.backtrace.first}"
48
+ end
49
+
50
+ def setup_at_fork_hooks
51
+ if Process.respond_to?(:at_fork)
52
+ Process.at_fork(:child) do
53
+ begin
54
+ # When Ruby forks, clock IDs for each of the threads
55
+ # will change. We can only update these IDs from the
56
+ # execution context of the thread that owns it.
57
+ # This hook will update the IDs for the main thread
58
+ # after a fork occurs.
59
+ Thread.current.send(:update_native_ids) if Thread.current.respond_to?(:update_native_ids, true)
60
+
61
+ # Restart profiler, if enabled
62
+ Datadog.profiler.start if Datadog.profiler
63
+ rescue StandardError => e
64
+ log "[DDTRACE] Error during post-fork hooks. Cause: #{e.message} Location: #{e.backtrace.first}"
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def log(message)
71
+ # Print to STDOUT for now because logging may not be setup yet...
72
+ puts message
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,12 @@
1
+ module Datadog
2
+ module Profiling
3
+ module Transport
4
+ # Generic interface for profiling transports
5
+ module Client
6
+ def send_profiling_flush(flush)
7
+ raise NotImplementedError
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,122 @@
1
+ require 'ddtrace/ext/runtime'
2
+ require 'ddtrace/ext/transport'
3
+
4
+ require 'ddtrace/runtime/container'
5
+
6
+ require 'ddtrace/profiling/transport/http/builder'
7
+ require 'ddtrace/profiling/transport/http/api'
8
+
9
+ require 'ddtrace/transport/http/adapters/net'
10
+ require 'ddtrace/transport/http/adapters/test'
11
+ require 'ddtrace/transport/http/adapters/unix_socket'
12
+
13
+ module Datadog
14
+ module Profiling
15
+ module Transport
16
+ # TODO: Consolidate with Dataog::Transport::HTTP
17
+ # Namespace for HTTP transport components
18
+ module HTTP
19
+ module_function
20
+
21
+ # Builds a new Transport::HTTP::Client
22
+ def new(&block)
23
+ Builder.new(&block).to_transport
24
+ end
25
+
26
+ # Builds a new Transport::HTTP::Client with default settings
27
+ # Pass a block to override any settings.
28
+ def default(options = {})
29
+ new do |transport|
30
+ transport.headers default_headers
31
+
32
+ # Configure adapter & API
33
+ if options[:site] && options[:api_key]
34
+ configure_for_agentless(transport, options)
35
+ else
36
+ configure_for_agent(transport, options)
37
+ end
38
+
39
+ # Additional options
40
+ unless options.empty?
41
+ # Change default API
42
+ transport.default_api = options[:api_version] if options.key?(:api_version)
43
+
44
+ # Add headers
45
+ transport.headers options[:headers] if options.key?(:headers)
46
+
47
+ # Execute on_build callback
48
+ options[:on_build].call(transport) if options[:on_build].is_a?(Proc)
49
+ end
50
+
51
+ # Call block to apply any customization, if provided.
52
+ yield(transport) if block_given?
53
+ end
54
+ end
55
+
56
+ def default_headers
57
+ {
58
+ Datadog::Ext::Transport::HTTP::HEADER_META_LANG => Datadog::Ext::Runtime::LANG,
59
+ Datadog::Ext::Transport::HTTP::HEADER_META_LANG_VERSION => Datadog::Ext::Runtime::LANG_VERSION,
60
+ Datadog::Ext::Transport::HTTP::HEADER_META_LANG_INTERPRETER => Datadog::Ext::Runtime::LANG_INTERPRETER,
61
+ Datadog::Ext::Transport::HTTP::HEADER_META_TRACER_VERSION => Datadog::Ext::Runtime::TRACER_VERSION
62
+ }.tap do |headers|
63
+ # Add container ID, if present.
64
+ container_id = Datadog::Runtime::Container.container_id
65
+ headers[Datadog::Ext::Transport::HTTP::HEADER_CONTAINER_ID] = container_id unless container_id.nil?
66
+ end
67
+ end
68
+
69
+ def default_adapter
70
+ :net_http
71
+ end
72
+
73
+ def default_hostname
74
+ ENV.fetch(Datadog::Ext::Transport::HTTP::ENV_DEFAULT_HOST, Datadog::Ext::Transport::HTTP::DEFAULT_HOST)
75
+ end
76
+
77
+ def default_port
78
+ ENV.fetch(Datadog::Ext::Transport::HTTP::ENV_DEFAULT_PORT, Datadog::Ext::Transport::HTTP::DEFAULT_PORT).to_i
79
+ end
80
+
81
+ def configure_for_agent(transport, options = {})
82
+ apis = API.agent_defaults
83
+
84
+ hostname = options[:hostname] || default_hostname
85
+ port = options[:port] || default_port
86
+
87
+ adapter_options = {}
88
+ adapter_options[:timeout] = options[:timeout] if options.key?(:timeout)
89
+ adapter_options[:ssl] = options[:ssl] if options.key?(:ssl)
90
+
91
+ transport.adapter default_adapter, hostname, port, adapter_options
92
+ transport.api API::V1, apis[API::V1], default: true
93
+ end
94
+
95
+ def configure_for_agentless(transport, options = {})
96
+ apis = API.api_defaults
97
+
98
+ site_uri = URI(format(Datadog::Ext::Profiling::Transport::HTTP::URI_TEMPLATE_DD_API, options[:site]))
99
+ hostname = options[:hostname] || site_uri.host
100
+ port = options[:port] || site_uri.port
101
+
102
+ adapter_options = {}
103
+ adapter_options[:timeout] = options[:timeout] if options.key?(:timeout)
104
+ adapter_options[:ssl] = options[:ssl] || (site_uri.scheme == 'https'.freeze)
105
+
106
+ transport.adapter default_adapter, hostname, port, adapter_options
107
+ transport.api API::V1, apis[API::V1], default: true
108
+ transport.headers(Datadog::Ext::Transport::HTTP::HEADER_DD_API_KEY => options[:api_key])
109
+ end
110
+
111
+ # Add adapters to registry
112
+ Builder::REGISTRY.set(Datadog::Transport::HTTP::Adapters::Net, :net_http)
113
+ Builder::REGISTRY.set(Datadog::Transport::HTTP::Adapters::Test, :test)
114
+ Builder::REGISTRY.set(Datadog::Transport::HTTP::Adapters::UnixSocket, :unix)
115
+
116
+ private \
117
+ :configure_for_agent,
118
+ :configure_for_agentless
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,43 @@
1
+ require 'ddtrace/transport/http/api/map'
2
+ require 'ddtrace/profiling/encoding/profile'
3
+ require 'ddtrace/profiling/transport/http/api/spec'
4
+ require 'ddtrace/profiling/transport/http/api/instance'
5
+ require 'ddtrace/profiling/transport/http/api/endpoint'
6
+
7
+ module Datadog
8
+ module Profiling
9
+ module Transport
10
+ module HTTP
11
+ # Extensions for HTTP API Spec
12
+ module API
13
+ # Default API versions
14
+ V1 = 'v1'.freeze
15
+
16
+ module_function
17
+
18
+ def agent_defaults
19
+ @agent_defaults ||= Datadog::Transport::HTTP::API::Map[
20
+ V1 => Spec.new do |s|
21
+ s.profiles = Endpoint.new(
22
+ '/profiling/v1/input'.freeze,
23
+ Profiling::Encoding::Profile::Protobuf
24
+ )
25
+ end
26
+ ]
27
+ end
28
+
29
+ def api_defaults
30
+ @api_defaults ||= Datadog::Transport::HTTP::API::Map[
31
+ V1 => Spec.new do |s|
32
+ s.profiles = Endpoint.new(
33
+ '/v1/input'.freeze,
34
+ Profiling::Encoding::Profile::Protobuf
35
+ )
36
+ end
37
+ ]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,90 @@
1
+ require 'ddtrace/ext/profiling'
2
+ require 'ddtrace/utils/compression'
3
+ require 'ddtrace/vendor/multipart-post/multipart/post/composite_read_io'
4
+
5
+ require 'ddtrace/transport/http/api/endpoint'
6
+ require 'ddtrace/profiling/transport/http/response'
7
+
8
+ module Datadog
9
+ module Profiling
10
+ module Transport
11
+ module HTTP
12
+ module API
13
+ # Datadog API endpoint for profiling
14
+ class Endpoint < Datadog::Transport::HTTP::API::Endpoint
15
+ include Datadog::Ext::Profiling::Transport::HTTP
16
+
17
+ attr_reader \
18
+ :encoder
19
+
20
+ def initialize(path, encoder, options = {})
21
+ super(:post, path)
22
+ @encoder = encoder
23
+ end
24
+
25
+ def call(env, &block)
26
+ # Build request
27
+ env.form = build_form(env)
28
+
29
+ # Send request
30
+ http_response = super(env, &block)
31
+
32
+ # Build response
33
+ Profiling::Transport::HTTP::Response.new(http_response)
34
+ end
35
+
36
+ def build_form(env)
37
+ flush = env.request.parcel.data
38
+ pprof_file, types = build_pprof(flush)
39
+
40
+ form = {
41
+ # NOTE: Redundant w/ 'runtime-id' tag below; may want to remove this later.
42
+ FORM_FIELD_RUNTIME_ID => flush.runtime_id,
43
+ FORM_FIELD_RECORDING_START => flush.start.utc.iso8601,
44
+ FORM_FIELD_RECORDING_END => flush.finish.utc.iso8601,
45
+ FORM_FIELD_TAGS => [
46
+ "#{FORM_FIELD_TAG_RUNTIME}:#{flush.language}",
47
+ "#{FORM_FIELD_TAG_RUNTIME_ID}:#{flush.runtime_id}",
48
+ "#{FORM_FIELD_TAG_RUNTIME_ENGINE}:#{flush.runtime_engine}",
49
+ "#{FORM_FIELD_TAG_RUNTIME_PLATFORM}:#{flush.runtime_platform}",
50
+ "#{FORM_FIELD_TAG_RUNTIME_VERSION}:#{flush.runtime_version}",
51
+ "#{FORM_FIELD_TAG_PROFILER_VERSION}:#{flush.profiler_version}",
52
+ # NOTE: Redundant w/ 'runtime'; may want to remove this later.
53
+ "#{FORM_FIELD_TAG_LANGUAGE}:#{flush.language}",
54
+ "#{FORM_FIELD_TAG_HOST}:#{flush.host}"
55
+ ],
56
+ FORM_FIELD_DATA => pprof_file,
57
+ FORM_FIELD_RUNTIME => flush.language,
58
+ FORM_FIELD_FORMAT => FORM_FIELD_FORMAT_PPROF
59
+ }
60
+
61
+ # Add types
62
+ form[FORM_FIELD_TYPES] = types.join(',')
63
+
64
+ # Optional fields
65
+ form[FORM_FIELD_TAGS] << "#{FORM_FIELD_TAG_SERVICE}:#{flush.service}" unless flush.service.nil?
66
+ form[FORM_FIELD_TAGS] << "#{FORM_FIELD_TAG_ENV}:#{flush.env}" unless flush.env.nil?
67
+ form[FORM_FIELD_TAGS] << "#{FORM_FIELD_TAG_VERSION}:#{flush.version}" unless flush.version.nil?
68
+
69
+ form
70
+ end
71
+
72
+ def build_pprof(flush)
73
+ pprof = encoder.encode(flush)
74
+
75
+ # Wrap pprof as a gzipped file
76
+ gzipped_data = Datadog::Utils::Compression.gzip(pprof.data)
77
+ pprof_file = Datadog::Vendor::Multipart::Post::UploadIO.new(
78
+ StringIO.new(gzipped_data),
79
+ HEADER_CONTENT_TYPE_OCTET_STREAM,
80
+ PPROF_DEFAULT_FILENAME
81
+ )
82
+
83
+ [pprof_file, [FORM_FIELD_TYPES_AUTO]]
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,36 @@
1
+ require 'ddtrace/transport/http/api/instance'
2
+ require 'ddtrace/profiling/transport/http/api/spec'
3
+
4
+ module Datadog
5
+ module Profiling
6
+ module Transport
7
+ module HTTP
8
+ module API
9
+ # API instance for profiling
10
+ class Instance < Datadog::Transport::HTTP::API::Instance
11
+ def send_profiling_flush(env)
12
+ raise ProfilesNotSupportedError, spec unless spec.is_a?(Spec)
13
+
14
+ spec.send_profiling_flush(env) do |request_env|
15
+ call(request_env)
16
+ end
17
+ end
18
+
19
+ # Raised when profiles sent to API that does not support profiles
20
+ class ProfilesNotSupportedError < StandardError
21
+ attr_reader :spec
22
+
23
+ def initialize(spec)
24
+ @spec = spec
25
+ end
26
+
27
+ def message
28
+ 'Profiles not supported for this API!'
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ require 'ddtrace/transport/http/api/spec'
2
+
3
+ module Datadog
4
+ module Profiling
5
+ module Transport
6
+ module HTTP
7
+ module API
8
+ # API specification for profiling
9
+ class Spec < Datadog::Transport::HTTP::API::Spec
10
+ attr_accessor \
11
+ :profiles
12
+
13
+ def send_profiling_flush(env, &block)
14
+ raise NoProfilesEndpointDefinedError, self if profiles.nil?
15
+
16
+ profiles.call(env, &block)
17
+ end
18
+
19
+ def encoder
20
+ profiles.encoder
21
+ end
22
+
23
+ # Raised when profiles sent but no profiles endpoint is defined
24
+ class NoProfilesEndpointDefinedError < StandardError
25
+ attr_reader :spec
26
+
27
+ def initialize(spec)
28
+ @spec = spec
29
+ end
30
+
31
+ def message
32
+ 'No profiles endpoint is defined for API specification!'
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ require 'ddtrace/transport/http/builder'
2
+
3
+ require 'ddtrace/profiling/transport/http/api'
4
+ require 'ddtrace/profiling/transport/http/client'
5
+
6
+ module Datadog
7
+ module Profiling
8
+ module Transport
9
+ module HTTP
10
+ # Builds new instances of Transport::HTTP::Client
11
+ class Builder < Datadog::Transport::HTTP::Builder
12
+ def api_instance_class
13
+ API::Instance
14
+ end
15
+
16
+ def to_transport
17
+ raise Datadog::Transport::HTTP::Builder::NoDefaultApiError if @default_api.nil?
18
+
19
+ # TODO: Profiling doesn't have multiple APIs yet.
20
+ # When it does, we should build it out with these APIs.
21
+ # Just use :default_api for now.
22
+ Client.new(to_api_instances[@default_api])
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end