watchforge-ruby-sdk 0.1.1
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +104 -0
- data/lib/watchforge/api/client.rb +59 -0
- data/lib/watchforge/api/dashboard.rb +33 -0
- data/lib/watchforge/api/issues.rb +33 -0
- data/lib/watchforge/api/monitors.rb +33 -0
- data/lib/watchforge/api/traces.rb +25 -0
- data/lib/watchforge/client.rb +77 -0
- data/lib/watchforge/context.rb +47 -0
- data/lib/watchforge/dsn.rb +45 -0
- data/lib/watchforge/tracing.rb +116 -0
- data/lib/watchforge/transport.rb +47 -0
- data/lib/watchforge/version.rb +6 -0
- data/lib/watchforge.rb +59 -0
- data/watchforge-ruby-sdk.gemspec +29 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6d1d05cc1cbefe1dcac7416fe8487041e18009bc9df6ae09d4c10a8cdbec425b
|
|
4
|
+
data.tar.gz: dcd0fcee33796a40c4e5a5d55cf8490bbd002f10d47b70d3caaef8584b833280
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 76caafd0ca6e1d1b38fc69aac06cf838c7bc0f27d28b4fa3e1a227047acf1f44c4e79e3965be2c259a0fde73052f8db755b8e5027ccb2e0ffd35d81050d9fd29
|
|
7
|
+
data.tar.gz: 2ec8d79857a2c587c3e356d2fd0eb0e370ff93a6f943a99a9722fc810436f5183648b6f9cb1dec609e117ff60c046fbb3a4cf15bd4a02315cef249df349ec52f
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WatchForge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# WatchForge Ruby SDK (`watchforge-ruby-sdk`)
|
|
2
|
+
|
|
3
|
+
Ruby SDK for WatchForge — errors, cron monitors, distributed tracing, and management APIs. Aligned with `watchforge-sdk` (Python) and `@watchforge/browser` (JavaScript).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install watchforge-ruby-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or in your `Gemfile`:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "watchforge-ruby-sdk", "~> 0.1.0"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "watchforge"
|
|
25
|
+
|
|
26
|
+
Watchforge.init(
|
|
27
|
+
dsn: "https://PUBLIC_KEY@dev.watchforges.com/PROJECT_ID",
|
|
28
|
+
environment: "production"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
Watchforge.context.add_breadcrumb("boot", "service started")
|
|
32
|
+
Watchforge.capture_message("hello", level: "info")
|
|
33
|
+
Watchforge.capture_exception(StandardError.new("boom"))
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Feature support
|
|
37
|
+
|
|
38
|
+
| Feature | API |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| Issues (ingestion) | `capture_exception`, `capture_message` |
|
|
41
|
+
| Context | `context.add_breadcrumb`, `set_user`, `set_tags` |
|
|
42
|
+
| Cron monitors | `capture_check_in` |
|
|
43
|
+
| Tracing | `start_transaction`, `start_span`, `finish_transaction` |
|
|
44
|
+
| Management API | `Watchforge::API::Client` |
|
|
45
|
+
|
|
46
|
+
## Cron monitor
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
check_in_id = Watchforge.capture_check_in(
|
|
50
|
+
monitor_slug: "nightly-job",
|
|
51
|
+
status: "in_progress",
|
|
52
|
+
monitor_config: {
|
|
53
|
+
schedule: { type: "crontab", value: "0 * * * *" },
|
|
54
|
+
timezone: "UTC",
|
|
55
|
+
check_margin: 5,
|
|
56
|
+
max_runtime: 30
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
Watchforge.capture_check_in(
|
|
61
|
+
monitor_slug: "nightly-job",
|
|
62
|
+
status: "ok",
|
|
63
|
+
check_in_id: check_in_id,
|
|
64
|
+
monitor_config: monitor_config
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Tracing
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
txn = Watchforge.start_transaction("GET /health", "Health Check")
|
|
72
|
+
span = txn.start_span("db.query", "SELECT 1")
|
|
73
|
+
span.finish
|
|
74
|
+
Watchforge.finish_transaction
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Management API (Bearer token)
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
api = Watchforge::API::Client.new("https://dev.watchforges.com", ENV["WATCHFORGE_TOKEN"])
|
|
81
|
+
api.list_issues(project_id, range: "7d", status: "unresolved")
|
|
82
|
+
api.dashboard_summary(project_id: project_id, range: "24h")
|
|
83
|
+
api.query_dashboard_widget(widget_payload)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Package naming (WatchForge SDK family)
|
|
87
|
+
|
|
88
|
+
| Language | Package | Install |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| Python | `watchforge-sdk` | `pip install watchforge-sdk` |
|
|
91
|
+
| JavaScript | `@watchforge/browser` | `npm install @watchforge/browser` |
|
|
92
|
+
| Go | `watchforge-go-sdk` | `go get github.com/watchforges/watchforge-go-sdk/watchforge` |
|
|
93
|
+
| **Ruby** | `watchforge-ruby-sdk` | `gem install watchforge-ruby-sdk` |
|
|
94
|
+
| PHP | `watchforge-php-sdk` | `composer require watchforges/watchforge-php-sdk` |
|
|
95
|
+
| Flutter | `watchforge_flutter_sdk` | `flutter pub add watchforge_flutter_sdk` |
|
|
96
|
+
| React Native | `@watchforge/react-native` | `npm install @watchforge/react-native` |
|
|
97
|
+
|
|
98
|
+
## Publishing (maintainers)
|
|
99
|
+
|
|
100
|
+
See [PUBLISHING.md](./PUBLISHING.md).
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Watchforge
|
|
8
|
+
module API
|
|
9
|
+
class Client
|
|
10
|
+
def initialize(base_url, token)
|
|
11
|
+
@base_url = base_url.to_s.chomp("/")
|
|
12
|
+
@base_url += "/api" unless @base_url.end_with?("/api")
|
|
13
|
+
@token = token
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(path, params = {})
|
|
17
|
+
request(:get, path, params: params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def post(path, body = {})
|
|
21
|
+
request(:post, path, body: body)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def patch(path, body = {})
|
|
25
|
+
request(:patch, path, body: body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete(path)
|
|
29
|
+
request(:delete, path)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def request(method, path, params: nil, body: nil)
|
|
35
|
+
uri = URI.parse("#{@base_url}/#{path.delete_prefix('/')}")
|
|
36
|
+
uri.query = URI.encode_www_form(params) if params && !params.empty?
|
|
37
|
+
|
|
38
|
+
klass = {
|
|
39
|
+
get: Net::HTTP::Get,
|
|
40
|
+
post: Net::HTTP::Post,
|
|
41
|
+
patch: Net::HTTP::Patch,
|
|
42
|
+
delete: Net::HTTP::Delete
|
|
43
|
+
}[method]
|
|
44
|
+
request = klass.new(uri)
|
|
45
|
+
request["Authorization"] = "Bearer #{@token}"
|
|
46
|
+
request["Content-Type"] = "application/json"
|
|
47
|
+
request.body = JSON.generate(body) if body
|
|
48
|
+
|
|
49
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
50
|
+
http.request(request)
|
|
51
|
+
end
|
|
52
|
+
raise "watchforge api failed: #{response.code} #{response.body}" if response.code.to_i >= 400
|
|
53
|
+
return nil if response.body.nil? || response.body.empty?
|
|
54
|
+
|
|
55
|
+
JSON.parse(response.body)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Watchforge
|
|
4
|
+
module API
|
|
5
|
+
module Dashboard
|
|
6
|
+
def dashboard_summary(params = {})
|
|
7
|
+
get("dashboard/", params)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def dashboard_analytics(params = {})
|
|
11
|
+
get("dashboard/analytics/", params)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def dashboard_reports(params = {})
|
|
15
|
+
get("dashboard/reports/", params)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def query_dashboard_widget(payload)
|
|
19
|
+
post("dashboard/custom/widgets/query/", payload)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def list_custom_dashboards(params = {})
|
|
23
|
+
get("dashboard/custom/", params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def project_dashboard(project_id, params = {})
|
|
27
|
+
get("projects/#{project_id}/dashboard/", params)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Watchforge::API::Client.include Watchforge::API::Dashboard
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Watchforge
|
|
4
|
+
module API
|
|
5
|
+
module Issues
|
|
6
|
+
def list_issues(project_id, params = {})
|
|
7
|
+
get("issues/", params.merge(project_id: project_id))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_issue(issue_id)
|
|
11
|
+
get("issues/#{issue_id}/")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def resolve_issue(issue_id)
|
|
15
|
+
post("issues/#{issue_id}/resolve/")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ignore_issue(issue_id)
|
|
19
|
+
post("issues/#{issue_id}/ignore/")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def update_issue(issue_id, payload)
|
|
23
|
+
patch("issues/#{issue_id}/update/", payload)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def issue_stats(issue_id, params = {})
|
|
27
|
+
get("issues/#{issue_id}/stats/", params)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Watchforge::API::Client.include Watchforge::API::Issues
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Watchforge
|
|
4
|
+
module API
|
|
5
|
+
module Monitors
|
|
6
|
+
def list_monitors(project_id)
|
|
7
|
+
get("monitors/projects/#{project_id}/monitors/")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_monitor(monitor_id)
|
|
11
|
+
get("monitors/monitors/#{monitor_id}/")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_monitor(project_id, payload)
|
|
15
|
+
post("monitors/projects/#{project_id}/monitors/", payload)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def update_monitor(monitor_id, payload)
|
|
19
|
+
patch("monitors/monitors/#{monitor_id}/", payload)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delete_monitor(monitor_id)
|
|
23
|
+
delete("monitors/monitors/#{monitor_id}/")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list_monitor_check_ins(monitor_id, params = {})
|
|
27
|
+
get("monitors/monitors/#{monitor_id}/checkins/", params)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Watchforge::API::Client.include Watchforge::API::Monitors
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Watchforge
|
|
4
|
+
module API
|
|
5
|
+
module Traces
|
|
6
|
+
def list_traces(project_id, params = {})
|
|
7
|
+
get("traces/", params.merge(project_id: project_id))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get_trace(trace_id)
|
|
11
|
+
get("traces/#{trace_id}/")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def list_trace_spans(trace_id)
|
|
15
|
+
get("traces/#{trace_id}/spans/")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def trace_performance(project_id, params = {})
|
|
19
|
+
get("traces/performance/", params.merge(project_id: project_id))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Watchforge::API::Client.include Watchforge::API::Traces
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Watchforge
|
|
7
|
+
class Client
|
|
8
|
+
attr_reader :context
|
|
9
|
+
|
|
10
|
+
def initialize(dsn:, environment: "production", release: nil, debug: false)
|
|
11
|
+
@dsn = dsn
|
|
12
|
+
@environment = environment
|
|
13
|
+
@release = release
|
|
14
|
+
@debug = debug
|
|
15
|
+
@transport = Transport.new(dsn, debug: debug)
|
|
16
|
+
@context = Context.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def capture_exception(error, extra: nil)
|
|
20
|
+
payload = base_event("error", error.message)
|
|
21
|
+
payload[:exception_type] = error.class.name
|
|
22
|
+
payload[:exception_value] = error.message
|
|
23
|
+
payload[:stacktrace] = { frames: [{ filename: "unknown", function: error.class.name, lineno: 0, in_app: true }] }
|
|
24
|
+
merge_extra(payload, extra)
|
|
25
|
+
@transport.send_event(payload)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def capture_message(message, level: "info", extra: nil)
|
|
29
|
+
payload = base_event(level, message)
|
|
30
|
+
merge_extra(payload, extra)
|
|
31
|
+
@transport.send_event(payload)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def capture_check_in(monitor_slug:, status: "ok", monitor_config: nil, check_in_id: nil, environment: nil)
|
|
35
|
+
check_in_id ||= SecureRandom.uuid if status == "in_progress"
|
|
36
|
+
payload = {
|
|
37
|
+
monitor_slug: monitor_slug,
|
|
38
|
+
status: status,
|
|
39
|
+
environment: environment || @environment
|
|
40
|
+
}
|
|
41
|
+
payload[:monitor_config] = monitor_config if monitor_config
|
|
42
|
+
payload[:check_in_id] = check_in_id if check_in_id
|
|
43
|
+
@transport.send_monitor_check_in(payload)
|
|
44
|
+
check_in_id
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def base_event(level, message)
|
|
50
|
+
user, tags, extra, request, breadcrumbs = @context.snapshot
|
|
51
|
+
payload = {
|
|
52
|
+
event_id: SecureRandom.uuid,
|
|
53
|
+
level: level,
|
|
54
|
+
environment: @environment,
|
|
55
|
+
message: message,
|
|
56
|
+
platform: "ruby",
|
|
57
|
+
timestamp: Time.now.utc.iso8601,
|
|
58
|
+
sdk: { name: SDK_NAME, version: VERSION },
|
|
59
|
+
data: {},
|
|
60
|
+
contexts: { runtime: { name: "ruby", version: RUBY_VERSION } }
|
|
61
|
+
}
|
|
62
|
+
payload[:release] = @release if @release
|
|
63
|
+
payload[:user] = user if user
|
|
64
|
+
payload[:request] = request if request
|
|
65
|
+
payload[:tags] = tags unless tags.empty?
|
|
66
|
+
payload[:data].merge!(extra) unless extra.empty?
|
|
67
|
+
payload[:breadcrumbs] = breadcrumbs unless breadcrumbs.empty?
|
|
68
|
+
payload
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def merge_extra(payload, extra)
|
|
72
|
+
return unless extra
|
|
73
|
+
|
|
74
|
+
payload[:data].merge!(extra)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Watchforge
|
|
7
|
+
class Context
|
|
8
|
+
def initialize
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@user = nil
|
|
11
|
+
@tags = {}
|
|
12
|
+
@extra = {}
|
|
13
|
+
@request = nil
|
|
14
|
+
@breadcrumbs = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def set_user(user) = synchronize { @user = user }
|
|
18
|
+
def set_tags(tags) = synchronize { @tags.merge!(tags) }
|
|
19
|
+
def set_extra(extra) = synchronize { @extra.merge!(extra) }
|
|
20
|
+
def set_request(request) = synchronize { @request = request }
|
|
21
|
+
|
|
22
|
+
def add_breadcrumb(category, message, data = nil)
|
|
23
|
+
synchronize do
|
|
24
|
+
@breadcrumbs << {
|
|
25
|
+
category: category,
|
|
26
|
+
message: message,
|
|
27
|
+
level: "info",
|
|
28
|
+
timestamp: Time.now.utc.iso8601,
|
|
29
|
+
data: data
|
|
30
|
+
}
|
|
31
|
+
@breadcrumbs = @breadcrumbs.last(100)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def snapshot
|
|
36
|
+
synchronize do
|
|
37
|
+
[@user, @tags.dup, @extra.dup, @request, @breadcrumbs.dup]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def synchronize(&block)
|
|
44
|
+
@mutex.synchronize(&block)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Watchforge
|
|
6
|
+
ParsedDSN = Struct.new(:scheme, :public_key, :host, :port, :project_id, :dsn, keyword_init: true) do
|
|
7
|
+
def events_url
|
|
8
|
+
ingestion_url("events/")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def trace_url
|
|
12
|
+
ingestion_url("trace/")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def monitor_url
|
|
16
|
+
ingestion_url("monitors/checkins/")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def ingestion_url(path)
|
|
22
|
+
host = port && !port.empty? ? "#{self.host}:#{port}" : self.host
|
|
23
|
+
"#{scheme}://#{host}/api/ingestion/#{path}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module DSN
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def parse(dsn)
|
|
31
|
+
uri = URI.parse(dsn)
|
|
32
|
+
raise ArgumentError, "invalid watchforge dsn" if uri.user.nil? || uri.host.nil? || uri.path.nil? || uri.path == "/"
|
|
33
|
+
|
|
34
|
+
scheme = %w[localhost 127.0.0.1].include?(uri.host) ? "http" : uri.scheme
|
|
35
|
+
ParsedDSN.new(
|
|
36
|
+
scheme: scheme,
|
|
37
|
+
public_key: uri.user,
|
|
38
|
+
host: uri.host,
|
|
39
|
+
port: uri.port,
|
|
40
|
+
project_id: uri.path.delete_prefix("/"),
|
|
41
|
+
dsn: dsn
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Watchforge
|
|
7
|
+
class Span
|
|
8
|
+
attr_reader :span_id, :op, :description, :parent_span_id, :data, :tags
|
|
9
|
+
attr_accessor :status, :status_code, :finish_timestamp, :duration_ms
|
|
10
|
+
|
|
11
|
+
def initialize(op, description = "", parent_span_id: nil, data: nil)
|
|
12
|
+
@span_id = SecureRandom.uuid
|
|
13
|
+
@op = op
|
|
14
|
+
@description = description
|
|
15
|
+
@parent_span_id = parent_span_id
|
|
16
|
+
@data = data || {}
|
|
17
|
+
@tags = {}
|
|
18
|
+
@start_timestamp = (Time.now.to_f * 1000).to_i
|
|
19
|
+
@status = "ok"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def finish(status = "ok", status_code: nil)
|
|
23
|
+
@status = status
|
|
24
|
+
@status_code = status_code
|
|
25
|
+
@finish_timestamp = (Time.now.to_f * 1000).to_i
|
|
26
|
+
@duration_ms = @finish_timestamp - @start_timestamp
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{
|
|
31
|
+
span_id: span_id,
|
|
32
|
+
parent_span_id: parent_span_id,
|
|
33
|
+
op: op,
|
|
34
|
+
description: description,
|
|
35
|
+
start_timestamp: Time.at(@start_timestamp / 1000.0).utc.iso8601,
|
|
36
|
+
finish_timestamp: finish_timestamp ? Time.at(finish_timestamp / 1000.0).utc.iso8601 : nil,
|
|
37
|
+
duration_ms: duration_ms,
|
|
38
|
+
status: status,
|
|
39
|
+
status_code: status_code,
|
|
40
|
+
data: data,
|
|
41
|
+
tags: tags,
|
|
42
|
+
timestamp: Time.at(@start_timestamp / 1000.0).utc.iso8601
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Transaction
|
|
48
|
+
attr_reader :trace_id, :transaction, :transaction_name, :op, :spans, :tags
|
|
49
|
+
|
|
50
|
+
def initialize(client, transaction, transaction_name, op)
|
|
51
|
+
@client = client
|
|
52
|
+
@trace_id = SecureRandom.uuid
|
|
53
|
+
@transaction = transaction
|
|
54
|
+
@transaction_name = transaction_name || transaction
|
|
55
|
+
@op = op || "http.server"
|
|
56
|
+
@spans = []
|
|
57
|
+
@tags = {}
|
|
58
|
+
@start_timestamp = (Time.now.to_f * 1000).to_i
|
|
59
|
+
@status = "ok"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def start_span(op, description = "", data: nil)
|
|
63
|
+
parent = spans.last&.span_id
|
|
64
|
+
span = Span.new(op, description, parent_span_id: parent, data: data)
|
|
65
|
+
spans << span
|
|
66
|
+
span
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def finish(status = "ok")
|
|
70
|
+
@status = status
|
|
71
|
+
finish_timestamp = (Time.now.to_f * 1000).to_i
|
|
72
|
+
spans.each { |span| span.finish if span.finish_timestamp.nil? }
|
|
73
|
+
payload = {
|
|
74
|
+
trace_id: trace_id,
|
|
75
|
+
transaction: transaction,
|
|
76
|
+
transaction_name: transaction_name,
|
|
77
|
+
op: op,
|
|
78
|
+
start_timestamp: Time.at(@start_timestamp / 1000.0).utc.iso8601,
|
|
79
|
+
finish_timestamp: Time.at(finish_timestamp / 1000.0).utc.iso8601,
|
|
80
|
+
duration_ms: finish_timestamp - @start_timestamp,
|
|
81
|
+
status: @status,
|
|
82
|
+
environment: @client.instance_variable_get(:@environment),
|
|
83
|
+
platform: "ruby",
|
|
84
|
+
sdk_name: SDK_NAME,
|
|
85
|
+
sdk_version: VERSION,
|
|
86
|
+
tags: tags,
|
|
87
|
+
spans: spans.map(&:to_h)
|
|
88
|
+
}
|
|
89
|
+
@client.instance_variable_get(:@transport).send_trace(payload)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
module Tracing
|
|
94
|
+
@stack = []
|
|
95
|
+
|
|
96
|
+
class << self
|
|
97
|
+
def start_transaction(transaction, transaction_name = nil, op = "http.server")
|
|
98
|
+
client = Watchforge.client
|
|
99
|
+
return nil unless client
|
|
100
|
+
|
|
101
|
+
txn = Transaction.new(client, transaction, transaction_name, op)
|
|
102
|
+
@stack << txn
|
|
103
|
+
txn
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def current_transaction
|
|
107
|
+
@stack.last
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def finish_transaction(status = "ok")
|
|
111
|
+
txn = @stack.pop
|
|
112
|
+
txn&.finish(status)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Watchforge
|
|
8
|
+
class Transport
|
|
9
|
+
def initialize(dsn, debug: false)
|
|
10
|
+
@dsn = dsn
|
|
11
|
+
@parsed = DSN.parse(dsn)
|
|
12
|
+
@debug = debug
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def send_event(payload)
|
|
16
|
+
post(@parsed.events_url, payload)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def send_trace(payload)
|
|
20
|
+
post(@parsed.trace_url, payload)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def send_monitor_check_in(payload)
|
|
24
|
+
post(@parsed.monitor_url, payload)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def post(url, payload)
|
|
30
|
+
override = ENV["WATCHFORGE_API_URL"]
|
|
31
|
+
url = override if override && url == @parsed.events_url
|
|
32
|
+
|
|
33
|
+
uri = URI.parse(url)
|
|
34
|
+
request = Net::HTTP::Post.new(uri)
|
|
35
|
+
request["Authorization"] = "DSN #{@dsn}"
|
|
36
|
+
request["Content-Type"] = "application/json"
|
|
37
|
+
request.body = JSON.generate(payload)
|
|
38
|
+
|
|
39
|
+
puts "[#{SDK_NAME}] POST #{url}" if @debug
|
|
40
|
+
|
|
41
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
42
|
+
response = http.request(request)
|
|
43
|
+
raise "watchforge ingestion failed: #{response.code}" if response.code.to_i >= 400
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/watchforge.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "watchforge/version"
|
|
4
|
+
require "watchforge/dsn"
|
|
5
|
+
require "watchforge/transport"
|
|
6
|
+
require "watchforge/context"
|
|
7
|
+
require "watchforge/client"
|
|
8
|
+
require "watchforge/tracing"
|
|
9
|
+
require "watchforge/api/client"
|
|
10
|
+
require "watchforge/api/issues"
|
|
11
|
+
require "watchforge/api/monitors"
|
|
12
|
+
require "watchforge/api/traces"
|
|
13
|
+
require "watchforge/api/dashboard"
|
|
14
|
+
|
|
15
|
+
module Watchforge
|
|
16
|
+
class << self
|
|
17
|
+
def init(dsn:, environment: "production", release: nil, debug: false)
|
|
18
|
+
@client = Client.new(dsn: dsn, environment: environment, release: release, debug: debug)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def client
|
|
22
|
+
@client
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def capture_exception(error, extra: nil)
|
|
26
|
+
client&.capture_exception(error, extra: extra)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def capture_message(message, level: "info", extra: nil)
|
|
30
|
+
client&.capture_message(message, level: level, extra: extra)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def capture_check_in(monitor_slug:, status: "ok", monitor_config: nil, check_in_id: nil, environment: nil)
|
|
34
|
+
client&.capture_check_in(
|
|
35
|
+
monitor_slug: monitor_slug,
|
|
36
|
+
status: status,
|
|
37
|
+
monitor_config: monitor_config,
|
|
38
|
+
check_in_id: check_in_id,
|
|
39
|
+
environment: environment
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def start_transaction(transaction, transaction_name = nil, op = "http.server")
|
|
44
|
+
Tracing.start_transaction(transaction, transaction_name, op)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_current_transaction
|
|
48
|
+
Tracing.current_transaction
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def finish_transaction(status = "ok")
|
|
52
|
+
Tracing.finish_transaction(status)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def context
|
|
56
|
+
client&.context || Context.new
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/watchforge/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "watchforge-ruby-sdk"
|
|
7
|
+
spec.version = ::Watchforge::VERSION
|
|
8
|
+
spec.authors = ["WatchForge"]
|
|
9
|
+
spec.email = ["support@watchforges.com"]
|
|
10
|
+
spec.summary = "WatchForge Ruby SDK for errors, traces, cron monitors, and management APIs"
|
|
11
|
+
spec.description = spec.summary
|
|
12
|
+
spec.homepage = "https://watchforges.com"
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.required_ruby_version = ">= 2.7.0"
|
|
15
|
+
|
|
16
|
+
spec.metadata = {
|
|
17
|
+
"source_code_uri" => "https://github.com/watchforges/watchforge-ruby-sdk",
|
|
18
|
+
"rubygems_mfa_required" => "true"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.start_with?("examples/") }
|
|
23
|
+
rescue StandardError
|
|
24
|
+
Dir["lib/**/*.rb", "README.md", "LICENSE", "PUBLISHING.md"]
|
|
25
|
+
end
|
|
26
|
+
spec.require_paths = ["lib"]
|
|
27
|
+
|
|
28
|
+
spec.add_dependency "json", "~> 2.6"
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: watchforge-ruby-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- WatchForge
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: json
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.6'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.6'
|
|
27
|
+
description: WatchForge Ruby SDK for errors, traces, cron monitors, and management
|
|
28
|
+
APIs
|
|
29
|
+
email:
|
|
30
|
+
- support@watchforges.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- lib/watchforge.rb
|
|
38
|
+
- lib/watchforge/api/client.rb
|
|
39
|
+
- lib/watchforge/api/dashboard.rb
|
|
40
|
+
- lib/watchforge/api/issues.rb
|
|
41
|
+
- lib/watchforge/api/monitors.rb
|
|
42
|
+
- lib/watchforge/api/traces.rb
|
|
43
|
+
- lib/watchforge/client.rb
|
|
44
|
+
- lib/watchforge/context.rb
|
|
45
|
+
- lib/watchforge/dsn.rb
|
|
46
|
+
- lib/watchforge/tracing.rb
|
|
47
|
+
- lib/watchforge/transport.rb
|
|
48
|
+
- lib/watchforge/version.rb
|
|
49
|
+
- watchforge-ruby-sdk.gemspec
|
|
50
|
+
homepage: https://watchforges.com
|
|
51
|
+
licenses:
|
|
52
|
+
- MIT
|
|
53
|
+
metadata:
|
|
54
|
+
source_code_uri: https://github.com/watchforges/watchforge-ruby-sdk
|
|
55
|
+
rubygems_mfa_required: 'true'
|
|
56
|
+
post_install_message:
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: 2.7.0
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 3.0.3.1
|
|
72
|
+
signing_key:
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: WatchForge Ruby SDK for errors, traces, cron monitors, and management APIs
|
|
75
|
+
test_files: []
|