-
Notifications
You must be signed in to change notification settings - Fork 88
/
common.rb
247 lines (218 loc) · 7.63 KB
/
common.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# Unless explicitly stated otherwise all files in this repository are licensed under the BSD-3-Clause License.
# This product includes software developed at Datadog (https://www.datadoghq.com/).
# Copyright 2011-Present Datadog, Inc.
require 'cgi'
require 'net/https'
require 'rbconfig'
require 'socket'
require 'uri'
require 'English'
require 'rubygems'
require 'multi_json'
require 'set'
require 'open3'
require 'dogapi/version'
module Dogapi
USER_AGENT = format(
'dogapi-rb/%<version>s (ruby %<ruver>s; os %<os>s; arch %<arch>s)',
version: VERSION,
ruver: RUBY_VERSION,
os: RbConfig::CONFIG['host_os'].downcase,
arch: RbConfig::CONFIG['host_cpu']
)
# Metadata class to hold the scope of an API call
class Scope
attr_reader :host, :device
def initialize(host=nil, device=nil)
@host = host
@device = device
end
end
# <b>DEPRECATED:</b> Going forward, use the newer APIService.
class Service
# <b>DEPRECATED:</b> Going forward, use the newer APIService.
def initialize(api_key, api_host=Dogapi.find_datadog_host)
@api_key = api_key
@host = api_host
end
# <b>DEPRECATED:</b> Going forward, use the newer APIService.
def connect
warn '[DEPRECATION] Dogapi::Service has been deprecated in favor of the newer V1 services'
uri = URI.parse(@host)
session = Net::HTTP.new(uri.host, uri.port)
if 'https' == uri.scheme
session.use_ssl = true
end
session.start do |conn|
yield(conn)
end
end
# <b>DEPRECATED:</b> Going forward, use the newer APIService.
def request(method, url, params)
warn '[DEPRECATION] Dogapi::Service has been deprecated in favor of the newer V1 services'
if !params.has_key? :api_key
params[:api_key] = @api_key
end
resp_obj = nil
connect do |conn|
req = method.new(url)
req.set_form_data params
resp = conn.request(req)
begin
resp_obj = MultiJson.load(resp.body)
rescue
raise 'Invalid JSON Response: ' + resp.body
end
if resp_obj.has_key? 'error'
request_string = params.pretty_inspect
error_string = resp_obj['error']
raise "Failed request\n#{request_string}#{error_string}"
end
end
resp_obj
end
end
# Superclass that deals with the details of communicating with the DataDog API
class APIService
attr_reader :api_key, :application_key
def initialize(api_key, application_key, silent=true, timeout=nil, endpoint=nil)
@api_key = api_key
@application_key = application_key
@api_host = endpoint || Dogapi.find_datadog_host()
@silent = silent
@timeout = timeout || 5
end
# Manages the HTTP connection
def connect
connection = Net::HTTP
# Expose using a proxy without setting the HTTPS_PROXY or HTTP_PROXY variables
proxy = Dogapi.find_proxy()
if proxy
proxy_uri = URI.parse(proxy)
connection = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
end
uri = URI.parse(@api_host)
session = connection.new(uri.host, uri.port)
session.open_timeout = @timeout
session.use_ssl = uri.scheme == 'https'
session.start do |conn|
conn.read_timeout = @timeout
yield conn
end
end
def suppress_error_if_silent(e)
raise e unless @silent
warn e
return -1, {}
end
# Prepares the request and handles the response
#
# +method+ is an implementation of Net::HTTP::Request (e.g. Net::HTTP::Post)
#
# +params+ is a Hash that will be converted to request parameters
def request(method, url, extra_params, body, send_json, with_app_key=true)
resp = nil
connect do |conn|
begin
params = prepare_params(extra_params, url, with_app_key)
req = prepare_request(method, url, params, body, send_json, with_app_key)
resp = conn.request(req)
if resp.code.to_i / 100 == 3
resp = handle_redirect(conn, req, resp)
end
return handle_response(resp)
rescue Exception => e
suppress_error_if_silent e
end
end
end
def prepare_request(method, url, params, body, send_json, with_app_key)
url_with_params = url + params
req = method.new(url_with_params)
req['User-Agent'] = USER_AGENT
unless should_set_api_and_app_keys_in_params?(url)
req['DD-API-KEY'] = @api_key
req['DD-APPLICATION-KEY'] = @application_key if with_app_key
end
if send_json
req.content_type = 'application/json'
req.body = MultiJson.dump(body)
end
return req
end
def prepare_params(extra_params, url, with_app_key)
if should_set_api_and_app_keys_in_params?(url)
params = { api_key: @api_key }
params[:application_key] = @application_key if with_app_key
else
params = {}
end
params = extra_params.merge params unless extra_params.nil?
qs_params = params.map { |k, v| CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s) }
qs = '?' + qs_params.join('&')
qs
end
def should_set_api_and_app_keys_in_params?(url)
set_of_urls = Set.new ['/api/v1/series', '/api/v1/check_run', '/api/v1/events', '/api/v1/screen']
return set_of_urls.include?(url)
end
def handle_response(resp)
if resp.code == 204 || resp.body == '' || resp.body == 'null' || resp.body.nil?
return resp.code, {}
end
begin
return resp.code, MultiJson.load(resp.body)
rescue
is_json = resp.content_type == 'application/json'
raise "Response Content-Type is not application/json but is #{resp.content_type}: " + resp.body unless is_json
raise 'Invalid JSON Response: ' + resp.body
end
end
def handle_redirect(conn, req, resp, retries=10)
req.uri = URI.parse(resp.header['location'])
new_response = conn.request(req)
if retries > 1 && new_response.code / 100 == 3
new_response = handle_redirect(conn, req, new_response, retries - 1)
end
new_response
end
end
def Dogapi.find_datadog_host
# allow env-based overriding, useful for tests
ENV['DATADOG_HOST'] || 'https://api.datadoghq.com'
end
# Memoize the hostname as a module variable
@@hostname = nil
def Dogapi.find_localhost
unless @@hostname
out, status = Open3.capture2('hostname', '-f', err: File::NULL)
# Get status to check if the call was successful
raise SystemCallError, 'Could not get hostname with `hostname -f`' unless status.exitstatus.zero?
@@hostname = out.strip
end
rescue SystemCallError
@@hostname = Addrinfo.getaddrinfo(Socket.gethostname, nil, nil, nil, nil, Socket::AI_CANONNAME).first.canonname
end
def Dogapi.find_proxy
ENV['DD_PROXY_HTTPS'] || ENV['dd_proxy_https'] ||
ENV['DD_PROXY_HTTP'] || ENV['dd_proxy_http'] ||
ENV['HTTPS_PROXY'] || ENV['https_proxy'] || ENV['HTTP_PROXY'] || ENV['http_proxy']
end
def Dogapi.validate_tags(tags)
unless tags.is_a? Array
raise ArgumentError, "The tags parameter needs to be an array of string. Current value: #{tags}"
end
tags.each do |tag|
raise ArgumentError, "Each tag needs to be a string. Current value: #{tag}" unless tag.is_a? String
end
end
# Very simplified hash with indifferent access - access to string or symbol
# keys via symbols. E.g.:
# my_hash = { 'foo' => 1 }
# Dogapi.symbolized_access(my_hash)
# my_hash[:foo] # => 1
def Dogapi.symbolized_access(hash)
hash.default_proc = proc { |h, k| h.key?(k.to_s) ? h[k.to_s] : nil }
hash
end
end