watchfor 0.5.0 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9af0dcb3158ef210d30f76f4535fa1af2febdd195c1e89c3356492839326d008
4
- data.tar.gz: fc12ab07a54877f0d098ac7ac76612c31d37188ed3b9f9283819b078e6f2caed
3
+ metadata.gz: 0fa3724c68935a0f14be699f9b232640b29b75811132dcf9e96cdec986d17132
4
+ data.tar.gz: 27b90a62ac79058fb401d4c0f75e7fbb59bc0c5622cb33b41fed8bb71ea49d2d
5
5
  SHA512:
6
- metadata.gz: de5374a23086013159a4526dc02f0c262525902a2a00ebdcf515797459c8ef26b52d351ff603705187674e5710c3b3097f3fffa98390dfdb745aa794475c20b3
7
- data.tar.gz: 0ccb9565903bca08b71d857ab9604357e6f337655bddb2d200d7f7015d501403ebf867eb9aee7074b95f496f46ce35198f145de9d41afd2ea95ac951433638f7
6
+ metadata.gz: 8fdfa7930ac0000827e8f5325e08bd5e99a10ec8ef0409b04144273e284cd16cbb6f1c9f37aaa2fda22df74998711c860bf1b93ac4e53136ca5acd5cd4daf114
7
+ data.tar.gz: 0a326ed15feab7394d1ab2f159cdab8a3bc2f402f3b47c0302dc8b1b5868ccfbe749860ca5ac70f83f5130d8f21a6248f4370df738f33a8cd220a73f05c016f4
data/README.md CHANGED
@@ -39,10 +39,45 @@ wf.incidents.exclude(19351, reason: "Probe-side DNS hiccup")
39
39
 
40
40
  # Irreversible: wipe a monitor's checks and incidents; uptime restarts "since reset"
41
41
  wf.monitors.reset(mon["id"])
42
+
43
+ # Older checks: pass next_cursor back as cursor (keep the same hours/success)
44
+ page = wf.monitors.checks(mon["id"], hours: 24, limit: 100)
45
+ wf.monitors.checks(mon["id"], hours: 24, limit: 100, cursor: page["next_cursor"]) if page["next_cursor"]
46
+
47
+ # Delete a contact group that alert rules still use (409 without force)
48
+ wf.contact_groups.delete("<group-id>", force: true)
49
+ ```
50
+
51
+ ### Live diagnostics
52
+
53
+ Run any of 18 checks from WatchFor's probe fleet against **any public
54
+ target**, monitored or not — this measures right now, rather than reading
55
+ what WatchFor recorded:
56
+
57
+ ```ruby
58
+ # What can I run, and how much budget is left?
59
+ catalog = wf.diagnostics.list
60
+
61
+ # One check, optionally from a location you choose (plan-gated)
62
+ dns = wf.diagnostics.run("dns-lookup", "example.com",
63
+ options: { recordType: "A", resolver: "8.8.8.8" })
64
+ # A closed port / NXDOMAIN / failed handshake does NOT raise:
65
+ puts "finding: #{dns['error']}" unless dns["success"]
66
+ puts "runs left this hour: #{dns['remaining']}"
67
+
68
+ # The whole picture in one call: DNS + propagation + TLS + HTTP + ping
69
+ # from up to 3 regions, aggregated into one verdict
70
+ report = wf.diagnostics.diagnose_target("example.com")
71
+ puts "#{report['verdict']['status']}: #{report['verdict']['summary']}"
42
72
  ```
43
73
 
74
+ Runs need a `write` key (a probe sends real traffic from WatchFor's IPs)
75
+ and spend the same per-plan hourly allowance as the dashboard Toolbox.
76
+ Details: <https://watchfor.io/docs/api/diagnostics>
77
+
44
78
  Namespaces: `wf.monitors`, `wf.alert_rules`, `wf.incidents`,
45
- `wf.maintenance_windows`, `wf.contacts`, `wf.contact_groups`. Top-level:
79
+ `wf.maintenance_windows`, `wf.contacts`, `wf.contact_groups`,
80
+ `wf.diagnostics`. Top-level:
46
81
  `wf.summary`, `wf.plan`, `wf.me`, `wf.locations`, `wf.monitor_types`,
47
82
  `wf.incident_stats(period)`, `wf.notifications`, `wf.activity`. Errors raise
48
83
  `Watchfor::Error` with `#status`, `#code`, `#message`.
@@ -54,7 +89,7 @@ export WATCHFOR_API_KEY=wf_live_...
54
89
  watchfor summary
55
90
  watchfor monitors
56
91
  watchfor incidents
57
- watchfor checks <monitor_id>
92
+ watchfor checks <monitor_id> [--cursor <next_cursor>]
58
93
  watchfor report --period 30d
59
94
  ```
60
95
 
data/exe/watchfor CHANGED
@@ -31,10 +31,13 @@ begin
31
31
  when "locations" then wf.locations
32
32
  when "monitor-types" then wf.monitor_types
33
33
  when "incidents" then wf.incidents.list
34
- when "checks" then wf.monitors.checks(args.shift)
34
+ when "checks"
35
+ monitor_id = args.shift
36
+ cursor = args.include?("--cursor") ? args[args.index("--cursor") + 1] : nil
37
+ wf.monitors.checks(monitor_id, cursor: cursor)
35
38
  when "report" then wf.report(period: args.include?("--period") ? args[args.index("--period") + 1] : nil)
36
39
  else
37
- warn "Usage: watchfor [summary|plan|monitors|locations|monitor-types|incidents|checks <monitor_id>|report [--period 7d|30d]]"
40
+ warn "Usage: watchfor [summary|plan|monitors|locations|monitor-types|incidents|checks <monitor_id> [--cursor c]|report [--period 7d|30d]]"
38
41
  exit 2
39
42
  end
40
43
  puts JSON.pretty_generate(result)
@@ -26,7 +26,8 @@ module Watchfor
26
26
  # wf.monitors.list["data"].each { |m| puts "#{m['name']} #{m['status']}" }
27
27
  class Client
28
28
  attr_reader :monitors, :alert_rules, :incidents,
29
- :maintenance_windows, :contacts, :contact_groups
29
+ :maintenance_windows, :contacts, :contact_groups,
30
+ :diagnostics
30
31
 
31
32
  def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: 30)
32
33
  raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
@@ -40,6 +41,7 @@ module Watchfor
40
41
  @maintenance_windows = MaintenanceWindows.new(self)
41
42
  @contacts = Contacts.new(self)
42
43
  @contact_groups = ContactGroups.new(self)
44
+ @diagnostics = Diagnostics.new(self)
43
45
  end
44
46
 
45
47
  # Core request. +query+ and +body+ are optional hashes.
@@ -113,19 +115,26 @@ module Watchfor
113
115
 
114
116
  class Monitors < Namespace
115
117
  def list(**query) = @c.request("GET", "/monitors", query: query)
118
+ # The monitor plus available_metrics (alert metrics valid for its type).
116
119
  def get(id) = @c.request("GET", "/monitors/#{id}")
117
120
  def create(body, idempotency_key: nil) = @c.request("POST", "/monitors", body: body, idempotency_key: idempotency_key)
118
121
  def update(id, body) = @c.request("PATCH", "/monitors/#{id}", body: body)
119
122
  def delete(id) = @c.request("DELETE", "/monitors/#{id}")
120
123
  def pause(id) = @c.request("POST", "/monitors/#{id}/pause")
121
124
  def resume(id) = @c.request("POST", "/monitors/#{id}/resume")
125
+ # Dispatch one out-of-schedule check now (202); the result lands in #checks moments later.
122
126
  def check_now(id) = @c.request("POST", "/monitors/#{id}/check-now")
123
127
  # IRREVERSIBLE: deletes every check, incident, note and post-mortem of the
124
128
  # monitor and starts uptime over ("since reset"); the monitor and its
125
129
  # configuration, alert rules, tags and locations stay.
126
130
  def reset(id) = @c.request("POST", "/monitors/#{id}/reset")
127
131
  def uptime(id, period = nil) = @c.request("GET", "/monitors/#{id}/uptime", query: { period: period })
128
- def checks(id, **query) = @c.request("GET", "/monitors/#{id}/checks", query: query)
132
+ # Recent probe results, newest first. Page with the response's next_cursor -> cursor:
133
+ # (keep the same hours/success); "locations" is the per-location breakdown for the whole window.
134
+ def checks(id, hours: nil, limit: nil, success: nil, cursor: nil, **query)
135
+ @c.request("GET", "/monitors/#{id}/checks",
136
+ query: { hours: hours, limit: limit, success: success, cursor: cursor, **query })
137
+ end
129
138
  end
130
139
 
131
140
  class AlertRules < Namespace
@@ -139,6 +148,7 @@ module Watchfor
139
148
  class Incidents < Namespace
140
149
  def list(**query) = @c.request("GET", "/incidents", query: query)
141
150
  def get(id) = @c.request("GET", "/incidents/#{id}")
151
+ # Every action returns the full, updated incident.
142
152
  def acknowledge(id) = @c.request("POST", "/incidents/#{id}/acknowledge")
143
153
  def resolve(id) = @c.request("POST", "/incidents/#{id}/resolve")
144
154
  # Mark a RESOLVED incident as a false positive: kept on record, left out of
@@ -169,7 +179,48 @@ module Watchfor
169
179
  def get(id) = @c.request("GET", "/contact-groups/#{id}")
170
180
  def create(body, idempotency_key: nil) = @c.request("POST", "/contact-groups", body: body, idempotency_key: idempotency_key)
171
181
  def update(id, body) = @c.request("PATCH", "/contact-groups/#{id}", body: body)
172
- def delete(id) = @c.request("DELETE", "/contact-groups/#{id}")
182
+ # 409 for the default group, and 409 while alert rules, monitors or notification channels
183
+ # still route through it; force: true deletes anyway (references are removed; affected
184
+ # alert rules fall back to their monitor's notification groups).
185
+ def delete(id, force: false) = @c.request("DELETE", "/contact-groups/#{id}", query: force ? { force: true } : nil)
186
+ end
187
+
188
+ # Live checks run from WatchFor's probe fleet against any public target,
189
+ # monitored or not — these MEASURE right now, unlike the rest of the
190
+ # client, which reads what WatchFor already recorded.
191
+ #
192
+ # +run+ and +diagnose_target+ need a write-scope key: a probe sends real
193
+ # traffic from WatchFor's IPs and spends a metered, per-plan allowance
194
+ # (the same hourly budget the dashboard Toolbox spends). Every response
195
+ # carries "remaining" and "distinct_targets" so a long investigation can
196
+ # pace itself. A check that ran and found a problem (closed port,
197
+ # NXDOMAIN, failed handshake) returns normally with "success" => false;
198
+ # only calls that could not run raise.
199
+ class Diagnostics < Namespace
200
+ # Every check, the question it answers, its options and this
201
+ # organization's remaining allowance. Read scope.
202
+ def list = @c.request("GET", "/diagnostics")
203
+
204
+ # Run one check (dns-lookup, ping, tls-grade, ...). +location_id+ is
205
+ # honoured on plans whose location_select is true; otherwise the
206
+ # nearest probe answers and the response says which. Write scope.
207
+ def run(slug, target, options: nil, location_id: nil, idempotency_key: nil)
208
+ body = { target: target }
209
+ body[:options] = options unless options.nil?
210
+ body[:location_id] = location_id unless location_id.nil?
211
+ @c.request("POST", "/diagnostics/#{slug}", body: body, idempotency_key: idempotency_key)
212
+ end
213
+
214
+ # DNS, propagation, TLS grade, HTTP headers and ping from up to 3
215
+ # regions in one call, aggregated into a single verdict. Each
216
+ # underlying check spends its own allowance; needs a plan with
217
+ # location selection. Write scope.
218
+ def diagnose_target(target, max_locations: nil, location_ids: nil, idempotency_key: nil)
219
+ body = { target: target }
220
+ body[:max_locations] = max_locations unless max_locations.nil?
221
+ body[:location_ids] = location_ids unless location_ids.nil?
222
+ @c.request("POST", "/diagnostics/diagnose-target", body: body, idempotency_key: idempotency_key)
223
+ end
173
224
  end
174
225
  end
175
226
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Watchfor
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: watchfor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - WatchFor
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-04 00:00:00.000000000 Z
11
+ date: 2026-09-07 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Zero-dependency Ruby client for the WatchFor REST API (monitors, alert
14
14
  rules, incidents, maintenance windows) plus a `watchfor` CLI.