nurse_andrea 0.2.5 → 1.4.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 +4 -4
- data/CHANGELOG.md +147 -0
- data/README.md +95 -12
- data/app/controllers/nurse_andrea/status_controller.rb +5 -1
- data/lib/generators/nurse_andrea/install/templates/nurse_andrea.rb.tt +18 -10
- data/lib/nurse_andrea/boot_diagnostics.rb +47 -0
- data/lib/nurse_andrea/configuration.rb +75 -14
- data/lib/nurse_andrea/environment_detector.rb +90 -0
- data/lib/nurse_andrea/errors.rb +7 -0
- data/lib/nurse_andrea/http_client.rb +106 -16
- data/lib/nurse_andrea/platform_detector.rb +10 -0
- data/lib/nurse_andrea/railtie.rb +7 -5
- data/lib/nurse_andrea/self_filter.rb +52 -9
- data/lib/nurse_andrea/slug_validator.rb +15 -0
- data/lib/nurse_andrea/version.rb +1 -1
- data/lib/nurse_andrea.rb +46 -5
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd771253f25db130adb2d9565f1cbd957419437f2469bf7c204f61762d2b1b0d
|
|
4
|
+
data.tar.gz: fb910b0a5c545a61bd9e4194e4aee7056563a56cf2fc7cf33e1197d113d9c487
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 55eac3c3846e51e4707b3973bd1e1350731379a96d03cb0753338d5c79eb7ffd49ac8f35517e5d368ff2f67991d2d6934eb4fd1d521500c7a13c4d79096227e7
|
|
7
|
+
data.tar.gz: 69e5a5207e49ad08570a3ac25c842a958892e40ebc86bd39b3df2a81e4c1648ff0aa2b117494b550973db3a1f58e8db8f2acb92806729528d85b1a4b22b46434
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,150 @@
|
|
|
1
|
+
## [1.4.0] - 2026-05-19
|
|
2
|
+
|
|
3
|
+
### Fixed — Self-filter R-14 compliance (False-Discoveries Patch)
|
|
4
|
+
- `SelfFilter.host_matches?` previously matched against hard-coded
|
|
5
|
+
`SELF_INDICATORS = %w[nurseandrea nurse-andrea nurse_andrea]`. Customer
|
|
6
|
+
connections whose host / URL / database name happened to contain any
|
|
7
|
+
of those substrings were silently dropped from discovery emission —
|
|
8
|
+
a false-negative on customer telemetry that had been live since the
|
|
9
|
+
original fix shipped (vintage `da4b3e4`).
|
|
10
|
+
- `host_matches?` now derives its match indicator from
|
|
11
|
+
`NurseAndrea.config.host` (strips scheme/port/path). Per environment:
|
|
12
|
+
prod → `nurseandrea.io`; staging → `staging.nurseandrea.io`;
|
|
13
|
+
development → `localhost`. Customer databases literally named
|
|
14
|
+
`nurse_andrea_*` are no longer affected.
|
|
15
|
+
- `platform_self?` is unchanged. It matches the SDK's own Rails app
|
|
16
|
+
name (process-level self-identification), not user-supplied URLs,
|
|
17
|
+
so the R-14 anti-pattern does not apply there. File header
|
|
18
|
+
documents the distinction.
|
|
19
|
+
|
|
20
|
+
### Cross-runtime parity (R-15(b))
|
|
21
|
+
- All 4 SDKs have functional parity on the published feature surface
|
|
22
|
+
as of v1.3. See `docs/sdk/v1.4.0-feature-parity-audit.md` in the
|
|
23
|
+
monorepo for the full matrix and the methodology behind the claim.
|
|
24
|
+
Job/queue execution instrumentation outside Ruby ActiveJob is a
|
|
25
|
+
known pre-rule gap and is scheduled for a separate post-v1.4.0
|
|
26
|
+
sprint. HTTP outbound capture of customer code is **not** claimed
|
|
27
|
+
by v1.4.0 — no SDK currently implements it.
|
|
28
|
+
|
|
29
|
+
## [1.3.0] - 2026-05-14
|
|
30
|
+
|
|
31
|
+
### Added — Rack-compatible core (GAP-09, Sprint D D1)
|
|
32
|
+
- The gem now loads cleanly in non-Rails Ruby processes (Sinatra,
|
|
33
|
+
plain Rack, background workers, CLI tools). Previously
|
|
34
|
+
`require "nurse_andrea"` raised `LoadError` outside a Rails
|
|
35
|
+
context because `job_instrumentation.rb`'s top-level
|
|
36
|
+
`require "active_support/concern"` failed without ActiveSupport
|
|
37
|
+
installed.
|
|
38
|
+
- Public surface available without Rails:
|
|
39
|
+
`NurseAndrea.configure`, `NurseAndrea.config.valid?`,
|
|
40
|
+
`NurseAndrea::LogShipper.instance`,
|
|
41
|
+
`NurseAndrea::MetricsShipper.instance`,
|
|
42
|
+
`NurseAndrea::MetricsMiddleware` (Rack middleware),
|
|
43
|
+
`NurseAndrea.deploy(...)`. See the README's "Non-Rails usage"
|
|
44
|
+
section for the minimal Sinatra / Rack setup.
|
|
45
|
+
- New spec at `spec/nurse_andrea/rack_compat_spec.rb` spawns a
|
|
46
|
+
subprocess with the Bundler env stripped and asserts the gem
|
|
47
|
+
loads, configures, and exposes the shippers without pulling in
|
|
48
|
+
Rails or ActiveSupport. Standing rule #12: load behavior is now
|
|
49
|
+
asserted by a spec, not by manual verification.
|
|
50
|
+
|
|
51
|
+
### Internal
|
|
52
|
+
- `lib/nurse_andrea.rb` now separates Rack-compatible requires
|
|
53
|
+
(unconditional) from Rails-only requires (guarded by
|
|
54
|
+
`defined?(ActiveSupport::Concern)` / `defined?(Rails::Railtie)` /
|
|
55
|
+
`defined?(Rails::Engine)`). No changes to the internal
|
|
56
|
+
implementation of any individual file — only their require's
|
|
57
|
+
load-time position.
|
|
58
|
+
|
|
59
|
+
## [1.2.0] - 2026-05-14
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
- **`X-NurseAndrea-Timestamp` header on every outbound POST.** Set
|
|
63
|
+
to `Time.now.to_i.to_s` (unix-seconds integer) on logs, metrics,
|
|
64
|
+
and deploy. Server-side window validation (±5 minutes) lands in
|
|
65
|
+
the matching NurseAndrea Rails release; requests outside the
|
|
66
|
+
window are rejected with HTTP 401
|
|
67
|
+
`error: "timestamp_out_of_window"`.
|
|
68
|
+
This is GAP-07 Phase 2 of the replay-mitigation work — see
|
|
69
|
+
`SECURITY.md` and `docs/sdk/payload-format.md` §2.1 for the
|
|
70
|
+
threat-model rationale and Phase 3 (HMAC) roadmap.
|
|
71
|
+
- Sprint B parity test extended to assert the timestamp header is
|
|
72
|
+
present and within drift on logs / metrics / deploy.
|
|
73
|
+
|
|
74
|
+
### Notes
|
|
75
|
+
- **Backward compatibility.** Servers running the matching Rails
|
|
76
|
+
release accept requests **without** the timestamp header (older
|
|
77
|
+
SDKs), so upgrading the SDK without coordinating the server
|
|
78
|
+
upgrade is safe. The accept-when-absent behavior goes away in
|
|
79
|
+
Phase 3.
|
|
80
|
+
|
|
81
|
+
## [1.1.0] - 2026-05-14
|
|
82
|
+
|
|
83
|
+
### Fixed (cross-runtime parity — wire spec at docs/sdk/payload-format.md)
|
|
84
|
+
- Status endpoint regression: `GET /nurse_andrea/status` returned
|
|
85
|
+
500 since the 1.0 release because the controller's `masked_token`
|
|
86
|
+
helper referenced the pre-1.0 `config.api_key` field, which raises
|
|
87
|
+
`MigrationError` in 1.0. The endpoint now reads `config.org_token`.
|
|
88
|
+
Caught by the new Sprint A host-app CI fixture; in-the-wild
|
|
89
|
+
installs running 1.0 should upgrade to restore the health check.
|
|
90
|
+
|
|
91
|
+
### Notes
|
|
92
|
+
- No behavior changes in this release for Ruby host apps beyond the
|
|
93
|
+
status-endpoint fix above. Ruby was already aligned with the
|
|
94
|
+
cross-runtime payload spec (Sprint B audit confirmed Ruby
|
|
95
|
+
emits the canonical log and metric field names —
|
|
96
|
+
`occurred_at` / `source` / `payload` and `occurred_at` —
|
|
97
|
+
that the spec adopts).
|
|
98
|
+
- Ruby-only optional emissions documented in
|
|
99
|
+
docs/sdk/payload-format.md and excluded from the parity contract:
|
|
100
|
+
the `User-Agent: nurse_andrea-ruby/<version>` header, per-log
|
|
101
|
+
`batch_id` for request tracing, top-level `platform` on metrics
|
|
102
|
+
(when platform detection is enabled), `component_discoveries`
|
|
103
|
+
(when service discovery is enabled), `component_metrics` (from
|
|
104
|
+
the InstrumentationSubscriber).
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
- `NurseAndrea::BootDiagnostics` — Sprint A D6 module that provides
|
|
108
|
+
per-cause warn messages from the Railtie when configuration is
|
|
109
|
+
incomplete. Replaces the generic "Configuration incomplete at
|
|
110
|
+
logger wrap time" warn with messages naming the specific failure
|
|
111
|
+
mode and the env var or setter to update.
|
|
112
|
+
|
|
113
|
+
## [1.0.0] - 2026-05-06
|
|
114
|
+
|
|
115
|
+
### Breaking
|
|
116
|
+
- Replaced `api_key` / `token` / `ingest_token` with three required
|
|
117
|
+
fields: `org_token`, `workspace_slug`, `environment`. Old field
|
|
118
|
+
setters and getters now raise `NurseAndrea::MigrationError` at boot
|
|
119
|
+
with a link to the migration guide. There is no compatibility shim.
|
|
120
|
+
- New required request headers: `Authorization: Bearer <org_token>`,
|
|
121
|
+
`X-NurseAndrea-Workspace: <slug>`, `X-NurseAndrea-Environment: <env>`.
|
|
122
|
+
Single-token-per-workspace auth is gone.
|
|
123
|
+
- `environment` must be one of `production`, `staging`, `development`.
|
|
124
|
+
Anything else raises a `ConfigurationError` at `validate!` time.
|
|
125
|
+
`RAILS_ENV=test` auto-detects to `production` with a one-time warning.
|
|
126
|
+
- `workspace_slug` is validated locally (lowercase a-z, 0-9, hyphens;
|
|
127
|
+
must start with a letter; 1-64 chars). Reserved-word enforcement
|
|
128
|
+
remains server-authoritative.
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
- `NurseAndrea::SlugValidator` — local format validation.
|
|
132
|
+
- `NurseAndrea::EnvironmentDetector` — auto-detection from `RAILS_ENV`
|
|
133
|
+
/ `RACK_ENV` with a one-time stderr warning on unsupported values.
|
|
134
|
+
- `NurseAndrea::MigrationError` (descends from `ConfigurationError`).
|
|
135
|
+
- Structured rejection handling: after 5 consecutive `401`/`403`/`422`/
|
|
136
|
+
`429` responses with the same error code, the SDK prints one stderr
|
|
137
|
+
warning per process lifecycle with actionable guidance keyed off the
|
|
138
|
+
server's `error` field (e.g. `invalid_org_token`, `workspace_rejected`,
|
|
139
|
+
`auto_create_disabled`, `similar_slug_exists`).
|
|
140
|
+
- `X-NurseAndrea-SDK: ruby/<version>` identity header on every request.
|
|
141
|
+
|
|
142
|
+
### Migration
|
|
143
|
+
- See https://docs.nurseandrea.io/sdk/migration. Short version: replace
|
|
144
|
+
the single `c.token` line with `c.org_token`, `c.workspace_slug`,
|
|
145
|
+
`c.environment`. Pull `org_token` from the org settings page (was
|
|
146
|
+
`account.token`); pick a slug for each app/service.
|
|
147
|
+
|
|
1
148
|
## [0.1.7] - 2026-04-06
|
|
2
149
|
|
|
3
150
|
### Added
|
data/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# NurseAndrea Ruby SDK
|
|
2
2
|
|
|
3
|
-
The official Ruby gem for [NurseAndrea](https://nurseandrea.
|
|
3
|
+
The official Ruby gem for [NurseAndrea](https://nurseandrea.io) —
|
|
4
|
+
observability for Rails startups and any plain-Ruby service. Version
|
|
5
|
+
`1.3.0`.
|
|
6
|
+
|
|
7
|
+
As of 1.3.0 the gem loads cleanly in non-Rails Ruby processes
|
|
8
|
+
(Sinatra, plain Rack, background workers, CLI tools). See
|
|
9
|
+
[Non-Rails usage](#non-rails-usage) below.
|
|
4
10
|
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
@@ -17,30 +23,107 @@ bundle install
|
|
|
17
23
|
rails generate nurse_andrea:install
|
|
18
24
|
```
|
|
19
25
|
|
|
20
|
-
Set
|
|
26
|
+
Set the required environment variable:
|
|
21
27
|
|
|
22
28
|
```bash
|
|
23
|
-
export
|
|
29
|
+
export NURSE_ANDREA_ORG_TOKEN="org_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
24
30
|
```
|
|
25
31
|
|
|
26
32
|
## What it does
|
|
27
33
|
|
|
28
|
-
- **Log shipping** — captures
|
|
29
|
-
|
|
30
|
-
- **
|
|
31
|
-
|
|
34
|
+
- **Log shipping** — captures `Rails.logger` calls and ships them to
|
|
35
|
+
the NurseAndrea dashboard.
|
|
36
|
+
- **Request metrics** — measures every HTTP request (duration,
|
|
37
|
+
status, path) via Rack middleware.
|
|
38
|
+
- **Backfill** — ships the last 24h of your Rails log file on first
|
|
39
|
+
startup.
|
|
40
|
+
- **Health endpoint** — mounts `/nurse_andrea/status` so the
|
|
41
|
+
dashboard can verify the integration.
|
|
32
42
|
|
|
33
43
|
## Configuration
|
|
34
44
|
|
|
45
|
+
The `rails generate nurse_andrea:install` generator drops
|
|
46
|
+
`config/initializers/nurse_andrea.rb`:
|
|
47
|
+
|
|
35
48
|
```ruby
|
|
36
|
-
NurseAndrea.configure do |
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
NurseAndrea.configure do |c|
|
|
50
|
+
c.org_token = ENV["NURSE_ANDREA_ORG_TOKEN"]
|
|
51
|
+
c.workspace_slug = "your-app"
|
|
52
|
+
c.environment = ENV.fetch("RAILS_ENV", "production")
|
|
53
|
+
c.host = ENV.fetch("NURSE_ANDREA_HOST", "https://nurseandrea.io")
|
|
54
|
+
c.enabled = !Rails.env.test?
|
|
55
|
+
c.log_level = :warn
|
|
41
56
|
end
|
|
42
57
|
```
|
|
43
58
|
|
|
59
|
+
All three of `org_token`, `workspace_slug`, and `environment` are
|
|
60
|
+
required. Missing any of them silently disables the SDK and emits a
|
|
61
|
+
`stderr` warn — see [SECURITY.md](../../SECURITY.md) for the
|
|
62
|
+
misconfiguration contract.
|
|
63
|
+
|
|
64
|
+
## Non-Rails usage
|
|
65
|
+
|
|
66
|
+
The gem's core ships logs and metrics through plain Ruby — Rails is
|
|
67
|
+
only needed for the Rails-specific glue listed below. Sinatra,
|
|
68
|
+
plain-Rack, background workers, and non-web services can `require`
|
|
69
|
+
the gem directly:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
require "nurse_andrea"
|
|
73
|
+
|
|
74
|
+
NurseAndrea.configure do |c|
|
|
75
|
+
c.org_token = ENV["NURSE_ANDREA_ORG_TOKEN"]
|
|
76
|
+
c.workspace_slug = "my-service"
|
|
77
|
+
c.environment = ENV.fetch("RACK_ENV", "production")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Ship logs directly:
|
|
81
|
+
NurseAndrea::LogShipper.instance.enqueue(
|
|
82
|
+
level: "info",
|
|
83
|
+
message: "hello from sinatra",
|
|
84
|
+
timestamp: Time.now.utc.iso8601(3)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Or wrap the HTTP-metrics middleware in a Rack app:
|
|
88
|
+
use NurseAndrea::MetricsMiddleware
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### What's available without Rails
|
|
92
|
+
|
|
93
|
+
- `NurseAndrea.configure`, `NurseAndrea.config`,
|
|
94
|
+
`NurseAndrea.config.valid?`
|
|
95
|
+
- `NurseAndrea::LogShipper.instance` — direct log ingest API
|
|
96
|
+
- `NurseAndrea::MetricsShipper.instance` — direct metric ingest API
|
|
97
|
+
- `NurseAndrea::MetricsMiddleware` — Rack middleware for HTTP
|
|
98
|
+
request duration / status / path
|
|
99
|
+
- `NurseAndrea.deploy(...)` — deploy markers
|
|
100
|
+
- Platform / managed-service / continuous discovery scanners
|
|
101
|
+
|
|
102
|
+
### What requires Rails
|
|
103
|
+
|
|
104
|
+
These features auto-wire when Rails is present and are inactive in
|
|
105
|
+
non-Rails processes. If you need any of them, install in a Rails
|
|
106
|
+
app instead:
|
|
107
|
+
|
|
108
|
+
- `NurseAndrea::Engine` mount at `/nurse_andrea/status` — the
|
|
109
|
+
dashboard's health-check endpoint
|
|
110
|
+
- `NurseAndrea::Railtie` boot hooks — automatic
|
|
111
|
+
`Rails.logger` wrapping, middleware installation,
|
|
112
|
+
initializer-time validation, per-cause boot warnings
|
|
113
|
+
- `rails generate nurse_andrea:install` — drops the initializer
|
|
114
|
+
- `ActiveSupport::Notifications`-based instrumentation
|
|
115
|
+
(`sql.active_record`, `cache_*`, `perform.active_job`,
|
|
116
|
+
`deliver.action_mailer`)
|
|
117
|
+
- `NurseAndrea::JobInstrumentation` — `around_perform` hook for
|
|
118
|
+
ActiveJob jobs
|
|
119
|
+
- Automatic backfill of the Rails log file on first boot
|
|
120
|
+
|
|
121
|
+
## Migration from 0.x
|
|
122
|
+
|
|
123
|
+
`api_key`, `token`, and `ingest_token` are no longer supported.
|
|
124
|
+
Setting any of them raises `NurseAndrea::MigrationError` at boot.
|
|
125
|
+
See [`docs/sdk/migration.md`](../../docs/sdk/migration.md).
|
|
126
|
+
|
|
44
127
|
## Version history
|
|
45
128
|
|
|
46
129
|
See [CHANGELOG.md](CHANGELOG.md).
|
|
@@ -18,7 +18,11 @@ module NurseAndrea
|
|
|
18
18
|
private
|
|
19
19
|
|
|
20
20
|
def masked_token
|
|
21
|
-
|
|
21
|
+
# SDK Sprint A D3 (GAP-03 surfaced this) — pre-1.0 referenced
|
|
22
|
+
# config.api_key, which now raises MigrationError. The 1.0
|
|
23
|
+
# field is org_token; the status controller was missed during
|
|
24
|
+
# the auth-contract rewrite. Host-app fixture smoke caught it.
|
|
25
|
+
token = NurseAndrea.config.org_token.to_s
|
|
22
26
|
return "not_configured" if token.empty?
|
|
23
27
|
"#{token[0..7]}..."
|
|
24
28
|
end
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
# NurseAndrea Observability SDK
|
|
2
2
|
# Docs: https://nurseandrea.io/docs
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
4
|
+
# Required environment variable:
|
|
5
|
+
# NURSE_ANDREA_ORG_TOKEN — your organization's ingest token
|
|
6
|
+
# (from nurseandrea.io/dashboard/settings)
|
|
7
|
+
# Optional environment variables:
|
|
8
|
+
# NURSE_ANDREA_HOST — NurseAndrea endpoint (default: https://nurseandrea.io)
|
|
9
|
+
# Set to http://localhost:4500 for local development
|
|
10
|
+
# Set to https://staging.nurseandrea.io for staging
|
|
11
|
+
#
|
|
12
|
+
# workspace_slug identifies which workspace within your org receives ingest.
|
|
13
|
+
# Lowercase letters, numbers, hyphens; starts with a letter; 1-64 chars.
|
|
14
|
+
# A new slug auto-creates as a pending workspace on first ingest.
|
|
9
15
|
|
|
10
16
|
NurseAndrea.configure do |c|
|
|
11
|
-
c.
|
|
12
|
-
c.
|
|
13
|
-
c.
|
|
14
|
-
c.
|
|
15
|
-
c.
|
|
17
|
+
c.org_token = ENV["NURSE_ANDREA_ORG_TOKEN"]
|
|
18
|
+
c.workspace_slug = "<%= app_name %>"
|
|
19
|
+
c.environment = ENV.fetch("RAILS_ENV", "production")
|
|
20
|
+
c.host = ENV.fetch("NURSE_ANDREA_HOST", "https://nurseandrea.io")
|
|
21
|
+
c.service_name = ENV.fetch("NURSE_ANDREA_SERVICE_NAME", "<%= app_name %>")
|
|
22
|
+
c.enabled = !Rails.env.test?
|
|
23
|
+
c.log_level = :warn # Set to :debug to see all intercepted logs
|
|
16
24
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module NurseAndrea
|
|
2
|
+
# Sprint A D6 (GAP-10) — per-failure-mode boot messages. Lives
|
|
3
|
+
# outside the Railtie so the message map can be tested without
|
|
4
|
+
# booting Rails. The Railtie calls .message_for(config) from its
|
|
5
|
+
# warn-and-disable branches; the unit specs target this module
|
|
6
|
+
# directly.
|
|
7
|
+
#
|
|
8
|
+
# Pre-Sprint-A the Railtie emitted a single generic warn
|
|
9
|
+
# ("Configuration incomplete at logger wrap time — monitoring
|
|
10
|
+
# disabled. Ensure NurseAndrea.configure is called in
|
|
11
|
+
# config/initializers/nurse_andrea.rb with a valid token.")
|
|
12
|
+
# regardless of which field was missing. Operators had to grep
|
|
13
|
+
# the codebase to know what to fix. The per-cause messages below
|
|
14
|
+
# name the exact failure mode and the env var or setter to update.
|
|
15
|
+
module BootDiagnostics
|
|
16
|
+
GUIDANCE = {
|
|
17
|
+
missing_org_token:
|
|
18
|
+
"[NurseAndrea] org_token is not set — monitoring disabled. " \
|
|
19
|
+
"Set NURSE_ANDREA_ORG_TOKEN in your environment.",
|
|
20
|
+
missing_workspace_slug:
|
|
21
|
+
"[NurseAndrea] workspace_slug is not set — monitoring disabled. " \
|
|
22
|
+
"Set c.workspace_slug in config/initializers/nurse_andrea.rb.",
|
|
23
|
+
missing_environment:
|
|
24
|
+
"[NurseAndrea] environment is not set — monitoring disabled. " \
|
|
25
|
+
"Set c.environment in config/initializers/nurse_andrea.rb " \
|
|
26
|
+
"(production / staging / development).",
|
|
27
|
+
invalid_environment:
|
|
28
|
+
"[NurseAndrea] environment is invalid — monitoring disabled. " \
|
|
29
|
+
"Must be one of: production, staging, development.",
|
|
30
|
+
invalid_workspace_slug:
|
|
31
|
+
"[NurseAndrea] workspace_slug is invalid — monitoring disabled. " \
|
|
32
|
+
"Slug must be lowercase letters, numbers, and hyphens; " \
|
|
33
|
+
"start with a letter; 1-64 characters.",
|
|
34
|
+
missing_host:
|
|
35
|
+
"[NurseAndrea] host is not set — monitoring disabled. " \
|
|
36
|
+
"Set NURSE_ANDREA_HOST or leave the default (https://nurseandrea.io)."
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
DISABLED =
|
|
40
|
+
"[NurseAndrea] monitoring is disabled (c.enabled = false).".freeze
|
|
41
|
+
|
|
42
|
+
def self.message_for(config)
|
|
43
|
+
return DISABLED unless config.enabled?
|
|
44
|
+
GUIDANCE[config.validation_diagnostic]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
module NurseAndrea
|
|
2
2
|
class Configuration
|
|
3
|
-
|
|
3
|
+
SUPPORTED_ENVIRONMENTS = EnvironmentDetector::SUPPORTED
|
|
4
|
+
|
|
5
|
+
MIGRATION_MESSAGE =
|
|
6
|
+
"%<field>s is no longer supported in NurseAndrea SDK 1.0. " \
|
|
7
|
+
"Migrate to org_token + workspace_slug + environment. " \
|
|
8
|
+
"See https://docs.nurseandrea.io/sdk/migration"
|
|
9
|
+
|
|
10
|
+
attr_accessor :org_token, :workspace_slug, :environment, :host,
|
|
11
|
+
:timeout, :log_level, :batch_size,
|
|
4
12
|
:flush_interval, :backfill_hours, :log_file_path,
|
|
5
13
|
:enabled, :debug, :service_name,
|
|
6
14
|
:sdk_version, :sdk_language,
|
|
@@ -14,6 +22,7 @@ module NurseAndrea
|
|
|
14
22
|
|
|
15
23
|
def initialize
|
|
16
24
|
@host = DEFAULT_HOST
|
|
25
|
+
@environment = EnvironmentDetector.detect
|
|
17
26
|
@timeout = 5
|
|
18
27
|
@log_level = :debug
|
|
19
28
|
@batch_size = 100
|
|
@@ -35,13 +44,19 @@ module NurseAndrea
|
|
|
35
44
|
@service_discovery = true
|
|
36
45
|
@auto_connect = false
|
|
37
46
|
@disable_continuous_scan = false
|
|
38
|
-
@continuous_scan_interval = 5 * 60
|
|
47
|
+
@continuous_scan_interval = 5 * 60
|
|
39
48
|
end
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
%i[api_key token ingest_token].each do |legacy|
|
|
51
|
+
define_method(legacy) do
|
|
52
|
+
raise MigrationError, format(MIGRATION_MESSAGE, field: legacy)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
define_method("#{legacy}=") do |_|
|
|
56
|
+
raise MigrationError, format(MIGRATION_MESSAGE, field: legacy)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
43
59
|
|
|
44
|
-
# All endpoint URLs derived from host
|
|
45
60
|
def ingest_url = "#{normalised_host}/api/v1/ingest"
|
|
46
61
|
def metrics_url = "#{normalised_host}/api/v1/metrics"
|
|
47
62
|
def traces_url = "#{normalised_host}/api/v1/traces"
|
|
@@ -57,29 +72,75 @@ module NurseAndrea
|
|
|
57
72
|
end
|
|
58
73
|
|
|
59
74
|
def valid?
|
|
60
|
-
|
|
75
|
+
validation_diagnostic.nil?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Sprint A D6 (GAP-10) — returns nil when configuration is valid,
|
|
79
|
+
# otherwise returns a symbol identifying the first failure mode.
|
|
80
|
+
# The Railtie maps these symbols to operator-actionable stderr
|
|
81
|
+
# messages so a missing-org_token miss tells the operator to
|
|
82
|
+
# set the env var rather than emitting the generic
|
|
83
|
+
# "Configuration incomplete at logger wrap time" line.
|
|
84
|
+
#
|
|
85
|
+
# Order matters: most-likely-missing field first. Operators
|
|
86
|
+
# debugging from logs benefit from the first message being the
|
|
87
|
+
# most common cause.
|
|
88
|
+
def validation_diagnostic
|
|
89
|
+
return :missing_org_token if blank?(org_token)
|
|
90
|
+
return :missing_workspace_slug if blank?(workspace_slug)
|
|
91
|
+
return :missing_environment if blank?(environment)
|
|
92
|
+
return :invalid_environment unless SUPPORTED_ENVIRONMENTS.include?(environment)
|
|
93
|
+
return :invalid_workspace_slug unless SlugValidator.valid?(workspace_slug)
|
|
94
|
+
return :missing_host if host.nil?
|
|
95
|
+
nil
|
|
61
96
|
end
|
|
62
97
|
|
|
63
98
|
def validate!
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
raise_config_error("org_token is required") if blank?(org_token)
|
|
100
|
+
raise_config_error("workspace_slug is required") if blank?(workspace_slug)
|
|
101
|
+
raise_config_error("environment is required") if blank?(environment)
|
|
102
|
+
|
|
103
|
+
unless SUPPORTED_ENVIRONMENTS.include?(environment)
|
|
104
|
+
raise_config_error(
|
|
105
|
+
"environment must be one of #{SUPPORTED_ENVIRONMENTS.join(', ')} " \
|
|
106
|
+
"(got #{environment.inspect})"
|
|
107
|
+
)
|
|
69
108
|
end
|
|
109
|
+
|
|
110
|
+
unless SlugValidator.valid?(workspace_slug)
|
|
111
|
+
raise_config_error(
|
|
112
|
+
"workspace_slug #{workspace_slug.inspect} is invalid. " \
|
|
113
|
+
"#{SlugValidator::HUMAN_READABLE_RULES}"
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
70
117
|
self
|
|
71
118
|
end
|
|
72
119
|
|
|
73
120
|
private
|
|
74
121
|
|
|
122
|
+
def blank?(value)
|
|
123
|
+
value.nil? || value.to_s.strip.empty?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def raise_config_error(message)
|
|
127
|
+
raise ConfigurationError, "[NurseAndrea] #{message}"
|
|
128
|
+
end
|
|
129
|
+
|
|
75
130
|
def normalised_host
|
|
76
131
|
host.to_s.chomp("/")
|
|
77
132
|
end
|
|
78
133
|
|
|
79
134
|
def default_service_name
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
135
|
+
first_present_env("RAILWAY_SERVICE_NAME", "NURSE_ANDREA_SERVICE_NAME") || rails_app_name
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def first_present_env(*names)
|
|
139
|
+
names.each do |n|
|
|
140
|
+
v = ENV[n].to_s.strip
|
|
141
|
+
return v unless v.empty?
|
|
142
|
+
end
|
|
143
|
+
nil
|
|
83
144
|
end
|
|
84
145
|
|
|
85
146
|
def rails_app_name
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module NurseAndrea
|
|
2
|
+
# Resolves the deployment environment via a fixed precedence chain
|
|
3
|
+
# (high → low, first match wins):
|
|
4
|
+
# 1. explicit value set in SDK init (the override — most intentional)
|
|
5
|
+
# 2. hosting-platform metadata (Railway env name — deployment-tier truth)
|
|
6
|
+
# 3. process declared env (RAILS_ENV / RACK_ENV / APP_ENV — weakest)
|
|
7
|
+
# 4. default: "unknown" — NEVER masquerade as production
|
|
8
|
+
#
|
|
9
|
+
# "unknown" is a display-only value: when nothing resolves, the SDK
|
|
10
|
+
# transmits "unknown" verbatim; the server stores it; the UI shows
|
|
11
|
+
# "UNKNOWN". It is NOT an operational environment.
|
|
12
|
+
#
|
|
13
|
+
# The taxonomy is development / staging / production (local ⇒ development).
|
|
14
|
+
# Unmappable values (custom platform env names, "test", etc.) do not
|
|
15
|
+
# masquerade — they fall through the chain toward "unknown".
|
|
16
|
+
class EnvironmentDetector
|
|
17
|
+
SUPPORTED = %w[production staging development].freeze
|
|
18
|
+
UNKNOWN = "unknown"
|
|
19
|
+
# Process-env vars consulted, in order (Ruby / Rack conventions).
|
|
20
|
+
PROCESS_VARS = %w[RAILS_ENV RACK_ENV APP_ENV].freeze
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# The resolved environment value (string). Backward-compatible entry
|
|
24
|
+
# point used by Configuration#initialize. `explicit` is the optional
|
|
25
|
+
# tier-1 override (Configuration also supports a post-init override).
|
|
26
|
+
def detect(explicit = nil)
|
|
27
|
+
resolve(explicit).fetch(:value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Full resolution detail: { value:, source:, variable_name: }.
|
|
31
|
+
# `source` is which tier won (explicit/platform/process/default);
|
|
32
|
+
# `variable_name` is the env-var NAME read (never its value). Both are
|
|
33
|
+
# privacy-safe metadata for the ingest payload — see DATA_PRIVACY_POLICY.
|
|
34
|
+
def resolve(explicit = nil)
|
|
35
|
+
if (value = normalize(explicit))
|
|
36
|
+
return detail(value, "explicit", nil)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
platform_env = PlatformDetector.context[:environment]
|
|
40
|
+
if (value = normalize(platform_env))
|
|
41
|
+
return detail(value, "platform", PlatformDetector.environment_variable_name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
PROCESS_VARS.each do |var|
|
|
45
|
+
raw = ENV[var]
|
|
46
|
+
next if raw.nil? || raw.empty?
|
|
47
|
+
if (value = normalize(raw))
|
|
48
|
+
return detail(value, "process", var)
|
|
49
|
+
end
|
|
50
|
+
warn_unsupported(raw, var) # present but unmappable — keep resolving
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
detail(UNKNOWN, "default", nil)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def reset_warning!
|
|
57
|
+
@warned = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def detail(value, source, variable_name)
|
|
63
|
+
{ value: value, source: source, variable_name: variable_name }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Map a raw value onto the taxonomy, or nil if unmappable (so the
|
|
67
|
+
# chain falls through rather than masquerading).
|
|
68
|
+
def normalize(raw)
|
|
69
|
+
return nil if raw.nil? || raw.to_s.strip.empty?
|
|
70
|
+
|
|
71
|
+
case raw.to_s.strip.downcase
|
|
72
|
+
when "production", "prod" then "production"
|
|
73
|
+
when "staging", "stage" then "staging"
|
|
74
|
+
when "development", "dev", "local" then "development"
|
|
75
|
+
else nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def warn_unsupported(value, var)
|
|
80
|
+
return if @warned
|
|
81
|
+
|
|
82
|
+
@warned = true
|
|
83
|
+
$stderr.puts(
|
|
84
|
+
"[NurseAndrea] #{var}='#{value}' is not a recognized environment " \
|
|
85
|
+
"(#{SUPPORTED.join('/')}); continuing resolution (may resolve to 'unknown')."
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -4,35 +4,125 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module NurseAndrea
|
|
6
6
|
class HttpClient
|
|
7
|
+
REJECTION_WARNING_THRESHOLD = 5
|
|
8
|
+
REJECTION_STATUSES = [ 401, 403, 422, 429 ].freeze
|
|
9
|
+
|
|
10
|
+
@@consecutive_rejections = 0
|
|
11
|
+
@@warned_for_error = nil
|
|
12
|
+
@@mutex = Mutex.new
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def reset_rejection_state!
|
|
16
|
+
@@mutex.synchronize do
|
|
17
|
+
@@consecutive_rejections = 0
|
|
18
|
+
@@warned_for_error = nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
7
23
|
def initialize
|
|
8
|
-
@
|
|
9
|
-
@timeout = NurseAndrea.config.timeout
|
|
24
|
+
@config = NurseAndrea.config
|
|
10
25
|
end
|
|
11
26
|
|
|
12
27
|
def post(url, body)
|
|
13
|
-
uri
|
|
28
|
+
uri = URI.parse(url)
|
|
14
29
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
15
|
-
http.use_ssl
|
|
16
|
-
http.open_timeout
|
|
17
|
-
http.read_timeout
|
|
30
|
+
http.use_ssl = uri.scheme == "https"
|
|
31
|
+
http.open_timeout = @config.timeout
|
|
32
|
+
http.read_timeout = @config.timeout
|
|
18
33
|
|
|
19
34
|
request = Net::HTTP::Post.new(uri.path)
|
|
20
|
-
request[
|
|
21
|
-
request["Authorization"] = "Bearer #{@api_key}"
|
|
22
|
-
request["User-Agent"] = "nurse_andrea-ruby/#{NurseAndrea::VERSION}"
|
|
35
|
+
build_headers.each { |k, v| request[k] = v }
|
|
23
36
|
request.body = body.to_json
|
|
24
37
|
|
|
25
38
|
response = http.request(request)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if NurseAndrea.config.debug && !success
|
|
29
|
-
warn "[NurseAndrea] HTTP #{response.code} from #{uri}: #{response.body.to_s[0..200]}"
|
|
30
|
-
end
|
|
39
|
+
handle_response(response, uri)
|
|
31
40
|
|
|
32
|
-
|
|
41
|
+
response.code.to_i.between?(200, 299)
|
|
33
42
|
rescue => e
|
|
34
|
-
warn "[NurseAndrea] HTTP error posting to #{url}: #{e.class}: #{e.message}" if
|
|
43
|
+
warn "[NurseAndrea] HTTP error posting to #{url}: #{e.class}: #{e.message}" if @config.debug
|
|
35
44
|
false
|
|
36
45
|
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_headers
|
|
50
|
+
{
|
|
51
|
+
"Content-Type" => "application/json",
|
|
52
|
+
"Authorization" => "Bearer #{@config.org_token}",
|
|
53
|
+
"X-NurseAndrea-Workspace" => @config.workspace_slug.to_s,
|
|
54
|
+
"X-NurseAndrea-Environment" => @config.environment.to_s,
|
|
55
|
+
"X-NurseAndrea-SDK" => "#{@config.sdk_language}/#{@config.sdk_version}",
|
|
56
|
+
# Sprint C — replay-mitigation timestamp. Server validates the
|
|
57
|
+
# value is within ±5 minutes when the header is present; SDKs
|
|
58
|
+
# older than 1.2.0 don't send it and the server accepts
|
|
59
|
+
# gracefully. See docs/sdk/payload-format.md §2 + SECURITY.md.
|
|
60
|
+
"X-NurseAndrea-Timestamp" => Time.now.to_i.to_s,
|
|
61
|
+
"User-Agent" => "nurse_andrea-ruby/#{NurseAndrea::VERSION}"
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def handle_response(response, uri)
|
|
66
|
+
status = response.code.to_i
|
|
67
|
+
|
|
68
|
+
if status.between?(200, 299)
|
|
69
|
+
@@mutex.synchronize do
|
|
70
|
+
@@consecutive_rejections = 0
|
|
71
|
+
@@warned_for_error = nil
|
|
72
|
+
end
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if @config.debug
|
|
77
|
+
warn "[NurseAndrea] HTTP #{status} from #{uri}: #{response.body.to_s[0..200]}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
return unless REJECTION_STATUSES.include?(status)
|
|
81
|
+
|
|
82
|
+
@@mutex.synchronize do
|
|
83
|
+
@@consecutive_rejections += 1
|
|
84
|
+
if @@consecutive_rejections >= REJECTION_WARNING_THRESHOLD
|
|
85
|
+
surface_rejection_warning(response, status)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def surface_rejection_warning(response, status)
|
|
91
|
+
body = JSON.parse(response.body) rescue {}
|
|
92
|
+
error = body.is_a?(Hash) ? body["error"].to_s : ""
|
|
93
|
+
return if @@warned_for_error == error
|
|
94
|
+
|
|
95
|
+
@@warned_for_error = error
|
|
96
|
+
message = body.is_a?(Hash) ? body["message"].to_s : ""
|
|
97
|
+
|
|
98
|
+
$stderr.puts(
|
|
99
|
+
"[NurseAndrea] Ingest rejected (#{REJECTION_WARNING_THRESHOLD}+ consecutive). " \
|
|
100
|
+
"Status: #{status} Error: #{error.empty? ? '(unknown)' : error}. " \
|
|
101
|
+
"#{guidance_for(error)}#{message.empty? ? '' : " Details: #{message}"}"
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def guidance_for(error)
|
|
106
|
+
case error
|
|
107
|
+
when "invalid_org_token"
|
|
108
|
+
"Check NURSE_ANDREA_ORG_TOKEN."
|
|
109
|
+
when "workspace_rejected"
|
|
110
|
+
"Restore the workspace in the dashboard or change workspace_slug."
|
|
111
|
+
when "workspace_limit_exceeded"
|
|
112
|
+
"Org has reached its workspace limit. Reject unused workspaces or upgrade plan."
|
|
113
|
+
when "auto_create_disabled"
|
|
114
|
+
"Auto-create disabled. Create the workspace explicitly in the dashboard before ingesting."
|
|
115
|
+
when "environment_not_accepted_by_this_install"
|
|
116
|
+
"Environment '#{@config.environment}' not accepted by NurseAndrea at #{@config.host}. Check NURSE_ANDREA_HOST."
|
|
117
|
+
when "invalid_workspace_slug"
|
|
118
|
+
SlugValidator::HUMAN_READABLE_RULES
|
|
119
|
+
when "similar_slug_exists"
|
|
120
|
+
"A similar slug already exists in this org. Did you mean an existing one?"
|
|
121
|
+
when "creation_rate_limit_exceeded", "rate_limited"
|
|
122
|
+
"Workspace creation rate limit hit. Existing workspaces still ingesting normally."
|
|
123
|
+
else
|
|
124
|
+
""
|
|
125
|
+
end
|
|
126
|
+
end
|
|
37
127
|
end
|
|
38
128
|
end
|
|
@@ -18,6 +18,16 @@ module NurseAndrea
|
|
|
18
18
|
"unknown"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# The env-var NAME (never its value) that supplies the platform's
|
|
22
|
+
# environment tier, when the detected platform exposes one. Used by
|
|
23
|
+
# EnvironmentDetector to tag the resolution source. nil when the
|
|
24
|
+
# platform has no environment-name var.
|
|
25
|
+
def self.environment_variable_name
|
|
26
|
+
case detect
|
|
27
|
+
when "railway" then "RAILWAY_ENVIRONMENT"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
21
31
|
def self.context
|
|
22
32
|
platform = detect
|
|
23
33
|
ctx = { platform: platform }
|
data/lib/nurse_andrea/railtie.rb
CHANGED
|
@@ -2,6 +2,11 @@ require "rails/railtie"
|
|
|
2
2
|
|
|
3
3
|
module NurseAndrea
|
|
4
4
|
class Railtie < Rails::Railtie
|
|
5
|
+
# Sprint A D6 (GAP-10) — per-failure-mode boot messages. The
|
|
6
|
+
# message map + resolver live in NurseAndrea::BootDiagnostics so
|
|
7
|
+
# they can be unit-tested without booting Rails. The Railtie
|
|
8
|
+
# just calls into it from each warn-and-disable branch.
|
|
9
|
+
|
|
5
10
|
# Runs after ALL initializers — including config/initializers/
|
|
6
11
|
# This means NurseAndrea.configure works from config/initializers/ as expected
|
|
7
12
|
initializer "nurse_andrea.wrap_logger", after: :load_config_initializers do
|
|
@@ -14,9 +19,7 @@ module NurseAndrea
|
|
|
14
19
|
"(host: #{NurseAndrea.config.host}, " \
|
|
15
20
|
"service: #{NurseAndrea.config.service_name || 'auto'})")
|
|
16
21
|
else
|
|
17
|
-
warn
|
|
18
|
-
"monitoring disabled. Ensure NurseAndrea.configure is called " \
|
|
19
|
-
"in config/initializers/nurse_andrea.rb with a valid token."
|
|
22
|
+
warn NurseAndrea::BootDiagnostics.message_for(NurseAndrea.config)
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
|
|
@@ -25,8 +28,7 @@ module NurseAndrea
|
|
|
25
28
|
app.middleware.use NurseAndrea::MetricsMiddleware
|
|
26
29
|
Rails.logger.info("[NurseAndrea] MetricsMiddleware inserted")
|
|
27
30
|
else
|
|
28
|
-
warn
|
|
29
|
-
"Ensure NurseAndrea.configure is called in config/initializers/nurse_andrea.rb"
|
|
31
|
+
warn NurseAndrea::BootDiagnostics.message_for(NurseAndrea.config)
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
@@ -1,13 +1,41 @@
|
|
|
1
|
-
#
|
|
2
|
-
# NurseAndrea
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# platform
|
|
6
|
-
#
|
|
1
|
+
# PRIVACY POLICY: This file derives platform-identifying values from
|
|
2
|
+
# NurseAndrea.config (a user-supplied configuration). No raw env
|
|
3
|
+
# values, connection strings, credentials, or tokens are stored or
|
|
4
|
+
# transmitted. The configured host is the source of truth for what
|
|
5
|
+
# counts as "platform infrastructure"; nothing is hard-coded except
|
|
6
|
+
# the Rails-app-name pattern in platform_self?, which is a process-
|
|
7
|
+
# level self-identification (the SDK detecting it's been loaded
|
|
8
|
+
# inside the NurseAndrea Rails app for self-instrumentation) and
|
|
9
|
+
# does not match against user-supplied URLs.
|
|
7
10
|
|
|
11
|
+
# Suppresses discovery emission when the SDK is loaded inside
|
|
12
|
+
# NurseAndrea itself, OR when a discovered connection target equals
|
|
13
|
+
# NurseAndrea's own configured infrastructure host. Both the
|
|
14
|
+
# InstrumentationSubscriber (hook-based) and the ManagedServiceScanner
|
|
15
|
+
# (env-based) must consult this filter before adding to
|
|
16
|
+
# NurseAndrea.component_discoveries — otherwise the platform's own
|
|
17
|
+
# infrastructure shows up as proposed components on every workspace
|
|
18
|
+
# dashboard.
|
|
19
|
+
#
|
|
20
|
+
# R-14 compliance: host_matches? derives its match indicator from
|
|
21
|
+
# NurseAndrea.config.host rather than hard-coding "nurseandrea.io"
|
|
22
|
+
# substring matching. This means:
|
|
23
|
+
# - prod (host=https://nurseandrea.io) → indicator "nurseandrea.io"
|
|
24
|
+
# - staging (host=https://staging.nurseandrea.io) → indicator "staging.nurseandrea.io"
|
|
25
|
+
# - dev (host=http://localhost:4500) → indicator "localhost"
|
|
26
|
+
# - tenant-specific (host=https://acme.example) → indicator "acme.example"
|
|
27
|
+
# The hard-coded substring approach used pre-R-14 broke dev (no
|
|
28
|
+
# "nurseandrea" in "localhost:4500") and would have broken any
|
|
29
|
+
# future tenant-specific ingest URLs.
|
|
8
30
|
module NurseAndrea
|
|
9
31
|
module SelfFilter
|
|
10
|
-
|
|
32
|
+
# Hard-coded fallback retained ONLY for platform_self? — that
|
|
33
|
+
# check matches the SDK's OWN Rails app name (process-level self-
|
|
34
|
+
# identification), which has no configured-URL equivalent. The
|
|
35
|
+
# R-14 anti-pattern (hard-coding customer URL substrings) does
|
|
36
|
+
# not apply here because the comparison target is the SDK's own
|
|
37
|
+
# environment, not a user-supplied URL.
|
|
38
|
+
PLATFORM_NAME_INDICATORS = %w[nurseandrea nurse-andrea nurse_andrea].freeze
|
|
11
39
|
|
|
12
40
|
class << self
|
|
13
41
|
def platform_self?
|
|
@@ -20,17 +48,32 @@ module NurseAndrea
|
|
|
20
48
|
end
|
|
21
49
|
|
|
22
50
|
def host_matches?(*candidates)
|
|
51
|
+
indicator = configured_host_indicator
|
|
52
|
+
return false if indicator.nil? || indicator.empty?
|
|
53
|
+
|
|
23
54
|
candidates.compact.map(&:to_s).map(&:downcase).any? do |s|
|
|
24
|
-
|
|
55
|
+
s.include?(indicator)
|
|
25
56
|
end
|
|
26
57
|
end
|
|
27
58
|
|
|
28
59
|
private
|
|
29
60
|
|
|
61
|
+
def configured_host_indicator
|
|
62
|
+
return nil unless NurseAndrea.respond_to?(:config)
|
|
63
|
+
|
|
64
|
+
host_url = NurseAndrea.config&.host
|
|
65
|
+
return nil if host_url.nil? || host_url.to_s.empty?
|
|
66
|
+
|
|
67
|
+
# Extract just the hostname: strip scheme, port, path.
|
|
68
|
+
host_url.to_s.downcase
|
|
69
|
+
.sub(%r{\Ahttps?://}, "")
|
|
70
|
+
.sub(%r{[:/].*\z}, "")
|
|
71
|
+
end
|
|
72
|
+
|
|
30
73
|
def compute_platform_self
|
|
31
74
|
return false unless defined?(Rails) && Rails.application
|
|
32
75
|
app_name = Rails.application.class.module_parent_name.to_s.downcase
|
|
33
|
-
|
|
76
|
+
PLATFORM_NAME_INDICATORS.any? { |i| app_name.include?(i) }
|
|
34
77
|
rescue
|
|
35
78
|
false
|
|
36
79
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module NurseAndrea
|
|
2
|
+
class SlugValidator
|
|
3
|
+
PATTERN = /\A[a-z][a-z0-9\-]{0,63}\z/
|
|
4
|
+
|
|
5
|
+
HUMAN_READABLE_RULES =
|
|
6
|
+
"Workspace slugs must be lowercase letters, numbers, or hyphens. " \
|
|
7
|
+
"Must start with a letter. 1-64 characters."
|
|
8
|
+
|
|
9
|
+
def self.valid?(slug)
|
|
10
|
+
return false if slug.nil? || slug.to_s.empty?
|
|
11
|
+
|
|
12
|
+
slug.to_s.match?(PATTERN)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/nurse_andrea/version.rb
CHANGED
data/lib/nurse_andrea.rb
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
|
+
# Sprint D D1 (GAP-09) — Rack-compatible core. The requires below
|
|
2
|
+
# divide into two layers:
|
|
3
|
+
#
|
|
4
|
+
# 1. Rack-compatible core (this block).
|
|
5
|
+
# Loads in any Ruby process — Sinatra, plain Rack, background
|
|
6
|
+
# worker, non-web service. Files in this layer reach into Rails
|
|
7
|
+
# ONLY behind `defined?(Rails)` / `defined?(ActiveSupport)`
|
|
8
|
+
# runtime guards inside methods, so requiring them in a non-
|
|
9
|
+
# Rails context succeeds and the methods short-circuit at call
|
|
10
|
+
# time.
|
|
11
|
+
#
|
|
12
|
+
# 2. Rails-only layer (the second block, guarded by Rails presence).
|
|
13
|
+
# Files here unconditionally require `rails/*` or
|
|
14
|
+
# `active_support/*` at the top of the file and therefore would
|
|
15
|
+
# raise LoadError outside a Rails app.
|
|
16
|
+
#
|
|
17
|
+
# Audit findings that drove this layout — surprises documented for
|
|
18
|
+
# future maintainers:
|
|
19
|
+
#
|
|
20
|
+
# * `log_interceptor`, `instrumentation_subscriber`, `query_subscriber`,
|
|
21
|
+
# `backfill`, `queue_depth_reporter`, `self_filter` all *use*
|
|
22
|
+
# Rails / ActiveSupport / SolidQueue / Sidekiq but only behind
|
|
23
|
+
# `defined?` guards inside methods. They are Rack-compatible at
|
|
24
|
+
# load time.
|
|
25
|
+
# * `metrics_middleware` is plain Rack middleware — it touches the
|
|
26
|
+
# env hash, never a Rails request object.
|
|
27
|
+
# * `job_instrumentation` is the only file in the historical
|
|
28
|
+
# unconditional list whose top-level requires (active_support/
|
|
29
|
+
# concern) actually require Rails. It moves below.
|
|
30
|
+
#
|
|
31
|
+
# The install generator at `lib/generators/nurse_andrea/install` is
|
|
32
|
+
# auto-discovered by Rails when `rails generate` runs — it is never
|
|
33
|
+
# required from this file, so it stays where it is and no guard is
|
|
34
|
+
# needed here.
|
|
1
35
|
require "nurse_andrea/version"
|
|
36
|
+
require "nurse_andrea/errors"
|
|
37
|
+
require "nurse_andrea/slug_validator"
|
|
38
|
+
require "nurse_andrea/environment_detector"
|
|
2
39
|
require "nurse_andrea/configuration"
|
|
3
40
|
require "nurse_andrea/http_client"
|
|
4
41
|
require "nurse_andrea/log_interceptor"
|
|
@@ -6,7 +43,6 @@ require "nurse_andrea/log_shipper"
|
|
|
6
43
|
require "nurse_andrea/metrics_middleware"
|
|
7
44
|
require "nurse_andrea/metrics_shipper"
|
|
8
45
|
require "nurse_andrea/backfill"
|
|
9
|
-
require "nurse_andrea/job_instrumentation"
|
|
10
46
|
require "nurse_andrea/queue_depth_reporter"
|
|
11
47
|
require "nurse_andrea/query_subscriber"
|
|
12
48
|
require "nurse_andrea/sanitizer"
|
|
@@ -18,13 +54,18 @@ require "nurse_andrea/memory_sampler"
|
|
|
18
54
|
require "nurse_andrea/deploy"
|
|
19
55
|
require "nurse_andrea/self_filter"
|
|
20
56
|
require "nurse_andrea/continuous_scanner"
|
|
57
|
+
require "nurse_andrea/boot_diagnostics"
|
|
21
58
|
|
|
22
|
-
|
|
23
|
-
|
|
59
|
+
# Rails-only layer. Engine and Railtie pull in `rails/engine` /
|
|
60
|
+
# `rails/railtie` at the top of their files; job_instrumentation
|
|
61
|
+
# pulls in `active_support/concern`. Each guard checks for the
|
|
62
|
+
# corresponding base class so the require is skipped cleanly in
|
|
63
|
+
# non-Rails processes.
|
|
64
|
+
require "nurse_andrea/job_instrumentation" if defined?(ActiveSupport::Concern)
|
|
65
|
+
require "nurse_andrea/railtie" if defined?(Rails::Railtie)
|
|
66
|
+
require "nurse_andrea/engine" if defined?(Rails::Engine)
|
|
24
67
|
|
|
25
68
|
module NurseAndrea
|
|
26
|
-
class ConfigurationError < StandardError; end
|
|
27
|
-
|
|
28
69
|
class << self
|
|
29
70
|
def configure
|
|
30
71
|
yield(config)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nurse_andrea
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ago AI LLC
|
|
@@ -26,11 +26,14 @@ files:
|
|
|
26
26
|
- lib/nurse_andrea.rb
|
|
27
27
|
- lib/nurse_andrea/DATA_PRIVACY_POLICY.rb
|
|
28
28
|
- lib/nurse_andrea/backfill.rb
|
|
29
|
+
- lib/nurse_andrea/boot_diagnostics.rb
|
|
29
30
|
- lib/nurse_andrea/component_telemetry.rb
|
|
30
31
|
- lib/nurse_andrea/configuration.rb
|
|
31
32
|
- lib/nurse_andrea/continuous_scanner.rb
|
|
32
33
|
- lib/nurse_andrea/deploy.rb
|
|
33
34
|
- lib/nurse_andrea/engine.rb
|
|
35
|
+
- lib/nurse_andrea/environment_detector.rb
|
|
36
|
+
- lib/nurse_andrea/errors.rb
|
|
34
37
|
- lib/nurse_andrea/http_client.rb
|
|
35
38
|
- lib/nurse_andrea/instrumentation_subscriber.rb
|
|
36
39
|
- lib/nurse_andrea/job_instrumentation.rb
|
|
@@ -46,6 +49,7 @@ files:
|
|
|
46
49
|
- lib/nurse_andrea/railtie.rb
|
|
47
50
|
- lib/nurse_andrea/sanitizer.rb
|
|
48
51
|
- lib/nurse_andrea/self_filter.rb
|
|
52
|
+
- lib/nurse_andrea/slug_validator.rb
|
|
49
53
|
- lib/nurse_andrea/version.rb
|
|
50
54
|
- nurse_andrea.gemspec
|
|
51
55
|
homepage: https://nurseandrea.io
|