wavefront-sdk 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -1142
  3. data/Gemfile +1 -1
  4. data/Rakefile +1 -1
  5. data/lib/wavefront-sdk/alert.rb +3 -3
  6. data/lib/wavefront-sdk/base.rb +25 -25
  7. data/lib/wavefront-sdk/cloudintegration.rb +1 -2
  8. data/lib/wavefront-sdk/constants.rb +1 -1
  9. data/lib/wavefront-sdk/credentials.rb +4 -6
  10. data/lib/wavefront-sdk/dashboard.rb +2 -3
  11. data/lib/wavefront-sdk/event.rb +2 -3
  12. data/lib/wavefront-sdk/exception.rb +36 -36
  13. data/lib/wavefront-sdk/externallink.rb +2 -2
  14. data/lib/wavefront-sdk/integration.rb +1 -2
  15. data/lib/wavefront-sdk/maintenancewindow.rb +3 -3
  16. data/lib/wavefront-sdk/message.rb +1 -2
  17. data/lib/wavefront-sdk/mixins.rb +25 -20
  18. data/lib/wavefront-sdk/notificant.rb +1 -2
  19. data/lib/wavefront-sdk/parse_time.rb +14 -13
  20. data/lib/wavefront-sdk/proxy.rb +2 -3
  21. data/lib/wavefront-sdk/query.rb +4 -8
  22. data/lib/wavefront-sdk/response.rb +8 -16
  23. data/lib/wavefront-sdk/savedsearch.rb +3 -5
  24. data/lib/wavefront-sdk/search.rb +2 -3
  25. data/lib/wavefront-sdk/source.rb +1 -1
  26. data/lib/wavefront-sdk/user.rb +1 -3
  27. data/lib/wavefront-sdk/validators.rb +12 -14
  28. data/lib/wavefront-sdk/version.rb +1 -1
  29. data/lib/wavefront-sdk/webhook.rb +2 -2
  30. data/lib/wavefront-sdk/write.rb +48 -24
  31. data/spec/spec_helper.rb +6 -4
  32. data/spec/wavefront-sdk/credentials_spec.rb +9 -7
  33. data/spec/wavefront-sdk/externallink_spec.rb +2 -0
  34. data/spec/wavefront-sdk/mixins_spec.rb +4 -3
  35. data/spec/wavefront-sdk/parse_time_spec.rb +26 -20
  36. data/spec/wavefront-sdk/query_spec.rb +2 -1
  37. data/spec/wavefront-sdk/response_spec.rb +13 -1
  38. data/spec/wavefront-sdk/validators_spec.rb +9 -2
  39. data/spec/wavefront-sdk/write_spec.rb +18 -6
  40. data/wavefront-sdk.gemspec +8 -7
  41. metadata +29 -29
@@ -5,7 +5,6 @@ module Wavefront
5
5
  # Manage and query Wavefront notification targets.
6
6
  #
7
7
  class Notificant < Base
8
-
9
8
  # GET /api/v2/notificant
10
9
  # Get all notification targets for a customer
11
10
  #
@@ -13,7 +12,7 @@ module Wavefront
13
12
  # @param limit [Int] the number of notification targets to return
14
13
  #
15
14
  def list(offset = 0, limit = 100)
16
- api_get('', { offset: offset, limit: limit })
15
+ api_get('', offset: offset, limit: limit)
17
16
  end
18
17
 
19
18
  # POST /api/v2/notificant
@@ -1,4 +1,5 @@
1
1
  module Wavefront
2
+ #
2
3
  # Parse various times into integers. This class is not for direct
3
4
  # consumption: it's used by the mixins parse_time method, which
4
5
  # does all the type sanity stuff.
@@ -9,34 +10,34 @@ module Wavefront
9
10
  # param t [Numeric] a timestamp
10
11
  # param ms [Bool] whether the timestamp is in milliseconds
11
12
  #
12
- def initialize(t, ms = false)
13
- @t = t
14
- @ms = ms
13
+ def initialize(time, in_ms = false)
14
+ @t = time
15
+ @ms = in_ms
15
16
  end
16
17
 
17
18
  # @return [Fixnum] timestamp
18
19
  #
19
- def parse_time_Fixnum
20
+ def parse_time_fixnum
20
21
  t
21
22
  end
22
23
 
23
24
  # @return [Integer] timestamp
24
25
  #
25
- def parse_time_Integer
26
+ def parse_time_integer
26
27
  t
27
28
  end
28
29
 
29
30
  # @return [Fixnum] timestamp
30
31
  #
31
- def parse_time_String
32
+ def parse_time_string
32
33
  return t.to_i if t =~ /^\d+$/
33
- @t = DateTime.parse("#{t} #{Time.now.getlocal.zone}")
34
- parse_time_Time
34
+ @t = Time.parse("#{t} #{Time.now.getlocal.zone}")
35
+ parse_time_time
35
36
  end
36
37
 
37
38
  # @return [Integer] timestamp
38
39
  #
39
- def parse_time_Time
40
+ def parse_time_time
40
41
  if ms
41
42
  t.to_datetime.strftime('%Q').to_i
42
43
  else
@@ -44,14 +45,14 @@ module Wavefront
44
45
  end
45
46
  end
46
47
 
47
- def parse_time_DateTime
48
- parse_time_Time
48
+ def parse_time_datetime
49
+ parse_time_time
49
50
  end
50
51
 
51
52
  def parse!
52
- method = ('parse_time_' + t.class.name).to_sym
53
+ method = ('parse_time_' + t.class.name.downcase).to_sym
53
54
  send(method)
54
- rescue
55
+ rescue StandardError
55
56
  raise Wavefront::Exception::InvalidTimestamp
56
57
  end
57
58
  end
@@ -5,7 +5,6 @@ module Wavefront
5
5
  # Manage and query Wavefront proxies.
6
6
  #
7
7
  class Proxy < Base
8
-
9
8
  # GET /api/v2/proxy
10
9
  # Get all proxies for a customer
11
10
  #
@@ -13,7 +12,7 @@ module Wavefront
13
12
  # @param limit [Int] the number of proxies to return
14
13
  #
15
14
  def list(offset = 0, limit = 100)
16
- api_get('', { offset: offset, limit: limit })
15
+ api_get('', offset: offset, limit: limit)
17
16
  end
18
17
 
19
18
  # DELETE /api/v2/proxy/id
@@ -68,7 +67,7 @@ module Wavefront
68
67
  def rename(id, name)
69
68
  wf_proxy_id?(id)
70
69
  wf_string?(name)
71
- update(id, {name: name})
70
+ update(id, name: name)
72
71
  end
73
72
 
74
73
  # A generic function to change properties of an proxy. So far as I
@@ -29,7 +29,7 @@ module Wavefront
29
29
  # @return [Wavefront::Response]
30
30
  #
31
31
  def query(query, granularity = nil, t_start = nil, t_end = nil,
32
- options = {})
32
+ options = {})
33
33
 
34
34
  raise ArgumentError unless query.is_a?(String)
35
35
  wf_granularity?(granularity)
@@ -40,10 +40,7 @@ module Wavefront
40
40
  options[:s] = parse_time(t_start, true)
41
41
  options[:e] = parse_time(t_end, true) if t_end
42
42
 
43
- options.delete_if do |k, v|
44
- v == false && k != :i
45
- end
46
-
43
+ options.delete_if { |k, v| v == false && k != :i }
47
44
  api_get('api', options)
48
45
  end
49
46
 
@@ -63,7 +60,7 @@ module Wavefront
63
60
  def raw(metric, source = nil, t_start = nil, t_end = nil)
64
61
  raise ArgumentError unless metric.is_a?(String)
65
62
 
66
- options = { metric: metric, }
63
+ options = { metric: metric }
67
64
 
68
65
  if source
69
66
  wf_source_id?(source)
@@ -83,8 +80,7 @@ module Wavefront
83
80
  { response: JSON.parse(body),
84
81
  status: { result: status == 200 ? 'OK' : 'ERROR',
85
82
  message: '',
86
- code: status },
87
- }.to_json
83
+ code: status } }.to_json
88
84
  end
89
85
  end
90
86
  end
@@ -3,7 +3,6 @@ require 'map'
3
3
  require_relative './exception'
4
4
 
5
5
  module Wavefront
6
-
7
6
  # Every API path has its own response class, which allows us to
8
7
  # provide a stable interface. If the API changes underneath us,
9
8
  # the SDK will break in a predictable way, throwing a
@@ -36,7 +35,7 @@ module Wavefront
36
35
  @response = build_response(raw)
37
36
 
38
37
  p self if debug
39
- rescue => e
38
+ rescue StandardError => e
40
39
  puts "could not parse:\n#{json}" if debug
41
40
  puts e.message if debug
42
41
  raise Wavefront::Exception::UnparseableResponse
@@ -44,7 +43,7 @@ module Wavefront
44
43
 
45
44
  def raw_response(json, status)
46
45
  json.empty? ? {} : JSON.parse(json, symbolize_names: true)
47
- rescue
46
+ rescue StandardError
48
47
  { message: json, code: status }
49
48
  end
50
49
 
@@ -53,22 +52,15 @@ module Wavefront
53
52
  end
54
53
 
55
54
  def build_response(raw)
56
- if raw.is_a?(Hash)
57
- if raw.key?(:response)
58
- if raw[:response].is_a?(Hash)
59
- Map(raw[:response])
60
- else
61
- raw[:response]
62
- end
63
- else
64
- Map.new(raw)
65
- end
66
- else
67
- Map.new
68
- end
55
+ return Map.new unless raw.is_a?(Hash)
56
+ return Map.new(raw) unless raw.key?(:response)
57
+ return raw[:response] unless raw[:response].is_a?(Hash)
58
+ Map(raw[:response])
69
59
  end
70
60
  end
71
61
 
62
+ # Status tests
63
+ #
72
64
  class Type
73
65
  #
74
66
  # An object which provides information about whether the request
@@ -6,7 +6,6 @@ module Wavefront
6
6
  # a UUID.
7
7
  #
8
8
  class SavedSearch < Base
9
-
10
9
  # GET /api/v2/savedsearch
11
10
  # Get all saved searches for a user.
12
11
  #
@@ -15,7 +14,7 @@ module Wavefront
15
14
  # @return [Wavefront::Response]
16
15
  #
17
16
  def list(offset = 0, limit = 100)
18
- api_get('', { offset: offset, limit: limit })
17
+ api_get('', offset: offset, limit: limit)
19
18
  end
20
19
 
21
20
  # POST /api/v2/savedsearch
@@ -74,9 +73,8 @@ module Wavefront
74
73
  #
75
74
  def entity(entitytype, offset = 0, limit = 100)
76
75
  wf_savedsearch_entity?(entitytype)
77
- api_get(['type', entitytype].uri_concat, { offset: offset,
78
- limit: limit })
79
-
76
+ api_get(['type', entitytype].uri_concat, offset: offset,
77
+ limit: limit)
80
78
  end
81
79
  end
82
80
  end
@@ -8,7 +8,6 @@ module Wavefront
8
8
  # lot up to the user. It may grow, with convenience methods.
9
9
  #
10
10
  class Search < Base
11
-
12
11
  # POST /api/v2/search/entity
13
12
  # POST /api/v2/search/entity/deleted
14
13
  # Run a search query. This single method maps to many API paths.
@@ -73,7 +72,7 @@ module Wavefront
73
72
  #
74
73
  def raw_search(entity = nil, body = nil, deleted = false)
75
74
  unless (entity.is_a?(String) || entity.is_a?(Symbol)) &&
76
- body.is_a?(Hash)
75
+ body.is_a?(Hash)
77
76
  raise ArgumentError
78
77
  end
79
78
 
@@ -94,7 +93,7 @@ module Wavefront
94
93
  # information.
95
94
  #
96
95
  def raw_facet_search(entity = nil, body = nil, deleted = false,
97
- facet = false)
96
+ facet = false)
98
97
  raise ArgumentError unless entity.is_a?(String)
99
98
  raise ArgumentError unless body.is_a?(Hash)
100
99
  path = [entity]
@@ -6,7 +6,7 @@ module Wavefront
6
6
  #
7
7
  class Source < Base
8
8
  def update_keys
9
- %i(sourceName tags description)
9
+ %i[sourceName tags description]
10
10
  end
11
11
 
12
12
  # GET /api/v2/source
@@ -5,7 +5,6 @@ module Wavefront
5
5
  # Manage and query Wavefront users
6
6
  #
7
7
  class User < Base
8
-
9
8
  # GET /api/v2/user
10
9
  # Get all users.
11
10
  #
@@ -92,8 +91,7 @@ module Wavefront
92
91
  { response: JSON.parse(body),
93
92
  status: { result: status == 200 ? 'OK' : 'ERROR',
94
93
  message: '',
95
- code: status },
96
- }.to_json
94
+ code: status } }.to_json
97
95
  end
98
96
 
99
97
  # the user API class does not support pagination. Be up-front
@@ -1,3 +1,4 @@
1
+ # rubocop:disable Naming/UncommunicativeMethodParamName
1
2
  require_relative './constants'
2
3
  require_relative './exception'
3
4
 
@@ -10,18 +11,15 @@ module Wavefront
10
11
  # Some comes from the Swagger API documentation, some has come
11
12
  # directly from Wavefront engineers.
12
13
  #
14
+ # rubocop:disable Metrics/ModuleLength
13
15
  module Validators
14
-
15
16
  # Ensure the given argument is a valid external link template
16
17
  #
17
18
  # @return true if it is valid
18
19
  # @raise Wavefront::Exception::InvalidTemplate if not
19
20
  #
20
21
  def wf_link_template?(v)
21
- if v.is_a?(String) && (v.start_with?('http://') ||
22
- v.start_with?('https://'))
23
- return true
24
- end
22
+ return true if v.is_a?(String) && v.start_with?('http://', 'https://')
25
23
 
26
24
  raise Wavefront::Exception::InvalidLinkTemplate
27
25
  end
@@ -148,11 +146,13 @@ module Wavefront
148
146
  # @raise Wavefront::Exception::InvalidVersion if the alert ID is
149
147
  # not valid
150
148
  #
149
+ # rubocop:disable Style/NumericPredicate
151
150
  def wf_version?(v)
152
151
  v = v.to_i if v.is_a?(String) && v =~ /^\d+$/
153
152
  return true if v.is_a?(Integer) && v > 0
154
153
  raise Wavefront::Exception::InvalidVersion
155
154
  end
155
+ # rubocop:enable Style/NumericPredicate
156
156
 
157
157
  # Ensure a hash of key:value point tags are value. Not to be
158
158
  # confused with source tags.
@@ -175,7 +175,7 @@ module Wavefront
175
175
  # @return nil
176
176
  #
177
177
  def wf_point_tag?(k, v)
178
- return if k && v && (k.size + v.size < 254) && k =~ /^[\w\-\.:]+$/
178
+ return if k && v && (k.size + v.size < 254) && k =~ /^[\w\-\.:]+$/
179
179
  raise Wavefront::Exception::InvalidTag
180
180
  end
181
181
 
@@ -292,7 +292,7 @@ module Wavefront
292
292
  # valid
293
293
  #
294
294
  def wf_alert_severity?(v)
295
- return true if %w(INFO SMOKE WARN SEVERE).include?(v)
295
+ return true if %w[INFO SMOKE WARN SEVERE].include?(v)
296
296
  raise Wavefront::Exception::InvalidAlertSeverity
297
297
  end
298
298
 
@@ -316,7 +316,7 @@ module Wavefront
316
316
  # valid
317
317
  #
318
318
  def wf_granularity?(v)
319
- return true if %w(d h m s).include?(v.to_s)
319
+ return true if %w[d h m s].include?(v.to_s)
320
320
  raise Wavefront::Exception::InvalidGranularity
321
321
  end
322
322
 
@@ -339,9 +339,9 @@ module Wavefront
339
339
  # valid
340
340
  #
341
341
  def wf_savedsearch_entity?(v)
342
- return true if %w(DASHBOARD ALERT MAINTENANCE_WINDOW
342
+ return true if %w[DASHBOARD ALERT MAINTENANCE_WINDOW
343
343
  NOTIFICANT EVENT SOURCE EXTERNAL_LINK AGENT
344
- CLOUD_INTEGRATION).include?(v)
344
+ CLOUD_INTEGRATION].include?(v)
345
345
  raise Wavefront::Exception::InvalidSavedSearchEntity
346
346
  end
347
347
 
@@ -353,9 +353,7 @@ module Wavefront
353
353
  # is not valid
354
354
  #
355
355
  def wf_source_id?(v)
356
- if v.is_a?(String) && v.match(/^[\w\.\-]+$/) && v.size < 1024
357
- return true
358
- end
356
+ return true if v.is_a?(String) && v.match(/^[\w\.\-]+$/) && v.size < 1024
359
357
 
360
358
  raise Wavefront::Exception::InvalidSourceId
361
359
  end
@@ -368,7 +366,7 @@ module Wavefront
368
366
  #
369
367
  def wf_user_id?(v)
370
368
  return true if v.is_a?(String) &&
371
- v =~ /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
369
+ v =~ /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
372
370
  raise Wavefront::Exception::InvalidUserId
373
371
  end
374
372
 
@@ -1 +1 @@
1
- WF_SDK_VERSION = '1.3.0'.freeze
1
+ WF_SDK_VERSION = '1.3.1'.freeze
@@ -6,7 +6,7 @@ module Wavefront
6
6
  #
7
7
  class Webhook < Base
8
8
  def update_keys
9
- %i(title description template title triggers recipient)
9
+ %i[title description template title triggers recipient]
10
10
  end
11
11
 
12
12
  # GET /api/v2/webhook
@@ -16,7 +16,7 @@ module Wavefront
16
16
  # @param limit [Integer] the number of webhooks to return
17
17
  #
18
18
  def list(offset = 0, limit = 100)
19
- api_get('', { offset: offset, limit: limit })
19
+ api_get('', offset: offset, limit: limit)
20
20
  end
21
21
 
22
22
  # POST /api/v2/webhook
@@ -9,6 +9,7 @@ module Wavefront
9
9
  # This class helps you send points to a Wavefront proxy in native
10
10
  # format. Usually this is done on port 2878.
11
11
  #
12
+ # rubocop:disable Metrics/ClassLength
12
13
  class Write < Base
13
14
  attr_reader :sock, :summary
14
15
 
@@ -62,7 +63,7 @@ module Wavefront
62
63
  open if openclose
63
64
 
64
65
  begin
65
- [points].flatten.each{ |p| send_point(p) }
66
+ [points].flatten.each { |p| send_point(p) }
66
67
  ensure
67
68
  close if openclose
68
69
  end
@@ -84,24 +85,21 @@ module Wavefront
84
85
  # @param openclose [Bool] if this is false, you must have
85
86
  # already opened a socket to the proxy. If it is true, a
86
87
  # connection will be opened for you, used, and closed.
88
+ # @param prefix [String] prefix all metrics with this string. No
89
+ # trailing dot is required.
87
90
  # @raise any unhandled point validation error is passed through
88
91
  # @return true if no points are rejected, otherwise false
89
92
  #
90
- def write(points = [], openclose = true)
93
+ def write(points = [], openclose = true, prefix = nil)
91
94
  open if openclose
92
95
 
93
96
  begin
94
- [points].flatten.each do |p|
95
- p[:ts] = p[:ts].to_i if p[:ts].is_a?(Time)
96
- valid_point?(p)
97
- send_point(hash_to_wf(p))
98
- end
97
+ _write_loop(prepped_points(points, prefix))
99
98
  ensure
100
99
  close if openclose
101
100
  end
102
101
 
103
- s_str = summary[:unsent] == 0 && summary[:rejected] == 0 ? 'OK' :
104
- 'ERROR'
102
+ s_str = summary_string(summary)
105
103
 
106
104
  resp = { status: { result: s_str, message: nil, code: nil },
107
105
  response: summary }.to_json
@@ -109,10 +107,27 @@ module Wavefront
109
107
  Wavefront::Response.new(resp, nil)
110
108
  end
111
109
 
110
+ def summary_string(summary)
111
+ summary[:unsent].zero? && summary[:rejected].zero? ? 'OK' : 'ERROR'
112
+ end
113
+
114
+ # @return [Array] of points
115
+ #
116
+ def prepped_points(points, prefix = nil)
117
+ ret = [points].flatten
118
+
119
+ if prefix
120
+ ret.map! { |pt| pt.tap { |p| p[:path] = prefix + '.' + p[:path] } }
121
+ end
122
+
123
+ ret
124
+ end
125
+
112
126
  # A wrapper method around #write() which guarantees all points
113
127
  # will be sent as deltas. You can still manually prefix any
114
- # metric with a Δ and use #write(), but depending on your
115
- # use-case, this method may be safer. It's easy to forget the Δ.
128
+ # metric with a delta symbol and use #write(), but depending on
129
+ # your use-case, this method may be safer. It's easy to forget
130
+ # the delta.
116
131
  #
117
132
  # @param points [Array[Hash]] see #write()
118
133
  # @param openclose [Bool] see #write()
@@ -131,11 +146,12 @@ module Wavefront
131
146
  [points].flatten.map { |p| p.tap { p[:path] = DELTA + p[:path] } }
132
147
  end
133
148
 
134
- def valid_point?(p)
149
+ # rubocop:disable Metrics/MethodLength
150
+ def valid_point?(point)
135
151
  return true if opts[:novalidate]
136
152
 
137
153
  begin
138
- wf_point?(p)
154
+ wf_point?(point)
139
155
  return true
140
156
  rescue Wavefront::Exception::InvalidMetricName,
141
157
  Wavefront::Exception::InvalidMetricValue,
@@ -143,7 +159,7 @@ module Wavefront
143
159
  Wavefront::Exception::InvalidSourceId,
144
160
  Wavefront::Exception::InvalidTag => e
145
161
  log('Invalid point, skipping.', :info)
146
- log("Invalid point: #{p}. (#{e})", :debug)
162
+ log("Invalid point: #{point}. (#{e})", :debug)
147
163
  summary[:rejected] += 1
148
164
  return false
149
165
  end
@@ -153,21 +169,22 @@ module Wavefront
153
169
  # https://community.wavefront.com/docs/DOC-1031. No validation
154
170
  # is done here.
155
171
  #
156
- # @param p [Hash] a hash describing a point. See #write() for
172
+ # @param point [Hash] a hash describing a point. See #write() for
157
173
  # the format.
158
174
  #
175
+ # rubocop:disable Metrics/AbcSize
159
176
  # rubocop:disable Metrics/CyclomaticComplexity
160
- def hash_to_wf(p)
161
- unless p.key?(:path) && p.key?(:value)
177
+ def hash_to_wf(point)
178
+ unless point.key?(:path) && point.key?(:value)
162
179
  raise Wavefront::Exception::InvalidPoint
163
180
  end
164
181
 
165
- p[:source] = HOSTNAME unless p.key?(:source)
182
+ p[:source] = HOSTNAME unless point.key?(:source)
166
183
 
167
- m = [p[:path], p[:value]]
168
- m.<< p[:ts] if p[:ts]
169
- m.<< 'source=' + p[:source]
170
- m.<< p[:tags].to_wf_tag if p[:tags]
184
+ m = [point[:path], point[:value]]
185
+ m.<< point[:ts] if point[:ts]
186
+ m.<< 'source=' + point[:source]
187
+ m.<< point[:tags].to_wf_tag if point[:tags]
171
188
  m.<< opts[:tags].to_wf_tag if opts[:tags]
172
189
  m.join(' ')
173
190
  end
@@ -187,7 +204,7 @@ module Wavefront
187
204
 
188
205
  begin
189
206
  sock.puts(point)
190
- rescue => e
207
+ rescue StandardError => e
191
208
  summary[:unsent] += 1
192
209
  log('WARNING: failed to send point.')
193
210
  log(e.to_s, :debug)
@@ -213,7 +230,7 @@ module Wavefront
213
230
 
214
231
  begin
215
232
  @sock = TCPSocket.new(net[:proxy], port)
216
- rescue => e
233
+ rescue StandardError => e
217
234
  log(e, :error)
218
235
  raise Wavefront::Exception::InvalidEndpoint
219
236
  end
@@ -229,6 +246,13 @@ module Wavefront
229
246
 
230
247
  private
231
248
 
249
+ def _write_loop(points)
250
+ points.each do |p|
251
+ p[:ts] = p[:ts].to_i if p[:ts].is_a?(Time)
252
+ send_point(hash_to_wf(p))
253
+ end
254
+ end
255
+
232
256
  # Overload the method which sets an API endpoint. A proxy
233
257
  # endpoint has an address and a port, rather than an address and
234
258
  # a token.