nurse_andrea 0.2.1 → 0.2.4
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/lib/nurse_andrea/configuration.rb +5 -1
- data/lib/nurse_andrea/continuous_scanner.rb +92 -0
- data/lib/nurse_andrea/deploy.rb +35 -0
- data/lib/nurse_andrea/instrumentation_subscriber.rb +20 -4
- data/lib/nurse_andrea/managed_service_scanner.rb +8 -2
- data/lib/nurse_andrea/platform_detector.rb +1 -1
- data/lib/nurse_andrea/railtie.rb +6 -0
- data/lib/nurse_andrea/self_filter.rb +39 -0
- data/lib/nurse_andrea/version.rb +1 -1
- data/lib/nurse_andrea.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7af261428fe8313fb24aa0c2a90ba94a14791040d1c83cd3bb52ee743f6a3e58
|
|
4
|
+
data.tar.gz: 0ef0cb18d33855bf8c510cc7da7ced9e0bfc7d67603add9bc739e1cb7b3809a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e7b22b9c82fb8f952148d7b5c30ba0cee5714f2b596631f5f89ef6618887a6cd86f4556ec456811b68eae92be802a32eb9cc86f360d93c5f9bba68dfe3e5385
|
|
7
|
+
data.tar.gz: 5c28c6a373e0c22582006ddfe5a237a5b99e57efc2f37bb786985db5a724d07894443891846043f5e29b41debde88cf969372e477fc4f80575d1cd81b02a6025
|
|
@@ -6,7 +6,8 @@ module NurseAndrea
|
|
|
6
6
|
:sdk_version, :sdk_language,
|
|
7
7
|
:hooks_enabled, :hooks_database, :hooks_cache,
|
|
8
8
|
:hooks_jobs, :hooks_mailer, :hook_interval_ms,
|
|
9
|
-
:platform_detection, :service_discovery, :auto_connect
|
|
9
|
+
:platform_detection, :service_discovery, :auto_connect,
|
|
10
|
+
:disable_continuous_scan, :continuous_scan_interval
|
|
10
11
|
|
|
11
12
|
LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }.freeze
|
|
12
13
|
DEFAULT_HOST = "https://nurseandrea.io"
|
|
@@ -33,6 +34,8 @@ module NurseAndrea
|
|
|
33
34
|
@platform_detection = true
|
|
34
35
|
@service_discovery = true
|
|
35
36
|
@auto_connect = false
|
|
37
|
+
@disable_continuous_scan = false
|
|
38
|
+
@continuous_scan_interval = 5 * 60 # seconds
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
alias_method :token, :api_key
|
|
@@ -43,6 +46,7 @@ module NurseAndrea
|
|
|
43
46
|
def metrics_url = "#{normalised_host}/api/v1/metrics"
|
|
44
47
|
def traces_url = "#{normalised_host}/api/v1/traces"
|
|
45
48
|
def handshake_url = "#{normalised_host}/api/v1/handshake"
|
|
49
|
+
def deploy_url = "#{normalised_host}/api/v1/deploy"
|
|
46
50
|
|
|
47
51
|
def enabled?
|
|
48
52
|
@enabled
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# PRIVACY: Same guarantees as ManagedServiceScanner — only derived
|
|
2
|
+
# metadata is transmitted, never raw env values. See
|
|
3
|
+
# DATA_PRIVACY_POLICY.rb for the full policy.
|
|
4
|
+
#
|
|
5
|
+
# Periodically re-runs the env-based discovery scan so dependencies
|
|
6
|
+
# added after boot (new env vars, services attached to running
|
|
7
|
+
# instances, configuration reloads) eventually surface as
|
|
8
|
+
# discoveries on the workspace dashboard.
|
|
9
|
+
#
|
|
10
|
+
# Properties guaranteed by the contract below:
|
|
11
|
+
# * Non-blocking — runs on a dedicated background thread, never on
|
|
12
|
+
# the request path.
|
|
13
|
+
# * Bounded — only one thread; calling start! twice is a no-op.
|
|
14
|
+
# * Fail-safe — any error inside the scan is swallowed; the host
|
|
15
|
+
# application never crashes from a discovery error.
|
|
16
|
+
# * Self-aware — short-circuits when SelfFilter.platform_self?
|
|
17
|
+
# because every URL would be NurseAndrea's own infra.
|
|
18
|
+
# * Stoppable — explicit stop! signals the thread to exit at the
|
|
19
|
+
# next sleep boundary; configuration.disable_continuous_scan
|
|
20
|
+
# prevents start! from creating the thread at all.
|
|
21
|
+
#
|
|
22
|
+
# Fork-safety note: the thread does NOT survive Process._fork (Puma
|
|
23
|
+
# clustered mode, Sidekiq workers). If the host forks after boot, the
|
|
24
|
+
# parent's scanner thread is gone and a new one is not auto-started.
|
|
25
|
+
# Address in a follow-up if it shows up in practice — for one-off
|
|
26
|
+
# webservers and single-process deployments the current design is
|
|
27
|
+
# sufficient.
|
|
28
|
+
|
|
29
|
+
module NurseAndrea
|
|
30
|
+
class ContinuousScanner
|
|
31
|
+
@thread = nil
|
|
32
|
+
@stop = false
|
|
33
|
+
@mutex = Mutex.new
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
attr_reader :thread
|
|
37
|
+
|
|
38
|
+
def start!
|
|
39
|
+
@mutex.synchronize do
|
|
40
|
+
return if @thread&.alive?
|
|
41
|
+
return if NurseAndrea.config.disable_continuous_scan
|
|
42
|
+
|
|
43
|
+
@stop = false
|
|
44
|
+
@thread = Thread.new { run_loop }
|
|
45
|
+
@thread.name = "nurse_andrea_continuous_scanner"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stop!
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
@stop = true
|
|
52
|
+
end
|
|
53
|
+
@thread&.wakeup rescue nil
|
|
54
|
+
@thread&.join(2)
|
|
55
|
+
@thread = nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def running?
|
|
59
|
+
@thread&.alive? == true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Public so specs can call it without spinning the loop.
|
|
63
|
+
def rescan_safely
|
|
64
|
+
return if NurseAndrea::SelfFilter.platform_self?
|
|
65
|
+
|
|
66
|
+
discoveries = NurseAndrea::ManagedServiceScanner.scan
|
|
67
|
+
return if discoveries.empty?
|
|
68
|
+
|
|
69
|
+
# Only push new items — ManagedServiceScanner already dedups
|
|
70
|
+
# by (type, tech, provider) within a single call, so we just
|
|
71
|
+
# need to avoid resubmitting items already queued for the
|
|
72
|
+
# current flush.
|
|
73
|
+
existing_keys = NurseAndrea.component_discoveries.map { |d| [ d[:type], d[:tech], d[:provider] ] }
|
|
74
|
+
discoveries.reject! { |d| existing_keys.include?([ d[:type], d[:tech], d[:provider] ]) }
|
|
75
|
+
NurseAndrea.component_discoveries.concat(discoveries) if discoveries.any?
|
|
76
|
+
rescue => e
|
|
77
|
+
NurseAndrea.debug("[ContinuousScanner] error: #{e.class}: #{e.message}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def run_loop
|
|
83
|
+
interval = NurseAndrea.config.continuous_scan_interval
|
|
84
|
+
until @stop
|
|
85
|
+
sleep interval
|
|
86
|
+
break if @stop
|
|
87
|
+
rescan_safely
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module NurseAndrea
|
|
2
|
+
# Public: ship a deploy event to the NurseAndrea backend so the
|
|
3
|
+
# dashboard can render it as a vertical marker on time-series charts
|
|
4
|
+
# and as a chip in the recent-deploys strip.
|
|
5
|
+
#
|
|
6
|
+
# Fire-and-forget: any failure (no token, network error, non-2xx) is
|
|
7
|
+
# logged in debug mode and swallowed so the host application never
|
|
8
|
+
# crashes from a deploy notification.
|
|
9
|
+
module Deploy
|
|
10
|
+
DESCRIPTION_LIMIT = 500
|
|
11
|
+
|
|
12
|
+
def self.call(version:, deployer: nil, environment: "production", description: nil)
|
|
13
|
+
return false unless NurseAndrea.config.valid?
|
|
14
|
+
return false if version.to_s.strip.empty?
|
|
15
|
+
|
|
16
|
+
payload = {
|
|
17
|
+
version: version.to_s,
|
|
18
|
+
deployer: deployer,
|
|
19
|
+
environment: environment,
|
|
20
|
+
description: description.is_a?(String) ? description[0, DESCRIPTION_LIMIT] : description,
|
|
21
|
+
deployed_at: Time.now.utc.iso8601
|
|
22
|
+
}.compact
|
|
23
|
+
|
|
24
|
+
HttpClient.new.post(NurseAndrea.config.deploy_url, payload)
|
|
25
|
+
rescue => e
|
|
26
|
+
NurseAndrea.debug("[NurseAndrea] deploy() error: #{e.class}: #{e.message}")
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Convenience top-level: NurseAndrea.deploy(version: "1.4.2")
|
|
32
|
+
def self.deploy(**kwargs)
|
|
33
|
+
Deploy.call(**kwargs)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -14,7 +14,7 @@ module NurseAndrea
|
|
|
14
14
|
"cache_delete.active_support" => :on_cache_delete,
|
|
15
15
|
"perform.active_job" => :on_job_perform,
|
|
16
16
|
"enqueue.active_job" => :on_job_enqueue,
|
|
17
|
-
"deliver.action_mailer" => :on_mailer
|
|
17
|
+
"deliver.action_mailer" => :on_mailer
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
20
|
attr_reader :telemetry, :discovered_components
|
|
@@ -48,13 +48,14 @@ module NurseAndrea
|
|
|
48
48
|
return if event.payload[:name] == "SCHEMA"
|
|
49
49
|
return if event.payload[:name]&.start_with?("EXPLAIN")
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
conn = event.payload[:connection]
|
|
52
|
+
adapter = conn&.adapter_name rescue nil
|
|
52
53
|
|
|
53
54
|
# Skip platform's own ClickHouse queries — not the customer's infrastructure
|
|
54
55
|
return if adapter&.downcase&.include?("clickhouse")
|
|
55
56
|
|
|
56
57
|
tech = adapter_to_tech(adapter)
|
|
57
|
-
register_discovery("database", tech) if tech
|
|
58
|
+
register_discovery("database", tech, connection: conn) if tech
|
|
58
59
|
|
|
59
60
|
table = extract_table(event.payload[:sql])
|
|
60
61
|
@telemetry.record_query(duration_ms: event.duration, table: table)
|
|
@@ -100,8 +101,9 @@ module NurseAndrea
|
|
|
100
101
|
register_discovery("external", "email")
|
|
101
102
|
end
|
|
102
103
|
|
|
103
|
-
def register_discovery(type, tech)
|
|
104
|
+
def register_discovery(type, tech, connection: nil)
|
|
104
105
|
return if tech.nil? || tech.empty? || tech == "unknown"
|
|
106
|
+
return if self_referential?(connection)
|
|
105
107
|
|
|
106
108
|
key = "#{type}:#{tech}"
|
|
107
109
|
return if @discovered_components.include?(key)
|
|
@@ -113,6 +115,20 @@ module NurseAndrea
|
|
|
113
115
|
)
|
|
114
116
|
end
|
|
115
117
|
|
|
118
|
+
# True when the SDK is running inside NurseAndrea itself, OR when
|
|
119
|
+
# the SQL event's connection points at NurseAndrea's own infra.
|
|
120
|
+
# Either way the discovery would be a self-reference, not a
|
|
121
|
+
# customer component. Process-level + connection-level checks are
|
|
122
|
+
# both consulted; the same module powers the env-scanner filter.
|
|
123
|
+
def self_referential?(connection)
|
|
124
|
+
return true if NurseAndrea::SelfFilter.platform_self?
|
|
125
|
+
return false unless connection
|
|
126
|
+
|
|
127
|
+
db_name = connection.current_database.to_s rescue ""
|
|
128
|
+
host = connection.pool&.db_config&.host.to_s rescue ""
|
|
129
|
+
NurseAndrea::SelfFilter.host_matches?(db_name, host)
|
|
130
|
+
end
|
|
131
|
+
|
|
116
132
|
def adapter_to_tech(adapter_name)
|
|
117
133
|
case adapter_name&.downcase
|
|
118
134
|
when "postgresql", "postgis" then "postgresql"
|
|
@@ -15,15 +15,21 @@ module NurseAndrea
|
|
|
15
15
|
"MONGODB_URI" => "database",
|
|
16
16
|
"MONGO_URL" => "database",
|
|
17
17
|
"ELASTICSEARCH_URL" => "search",
|
|
18
|
-
"KAFKA_BROKERS" => "queue"
|
|
18
|
+
"KAFKA_BROKERS" => "queue"
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
21
|
def self.scan
|
|
22
|
+
# Skip env-based discovery entirely when the SDK is loaded inside
|
|
23
|
+
# NurseAndrea itself — every URL we'd find belongs to the platform's
|
|
24
|
+
# own infrastructure, not a customer component.
|
|
25
|
+
return [] if NurseAndrea::SelfFilter.platform_self?
|
|
26
|
+
|
|
22
27
|
discoveries = []
|
|
23
28
|
|
|
24
29
|
SCAN_MAP.each do |var_name, component_type|
|
|
25
30
|
url = ENV[var_name]
|
|
26
31
|
next if url.nil? || url.strip.empty?
|
|
32
|
+
next if NurseAndrea::SelfFilter.host_matches?(url)
|
|
27
33
|
|
|
28
34
|
tech = Sanitizer.extract_tech(url)
|
|
29
35
|
provider = Sanitizer.extract_provider(url)
|
|
@@ -39,7 +45,7 @@ module NurseAndrea
|
|
|
39
45
|
discoveries << Sanitizer.sanitize_discovery(raw)
|
|
40
46
|
end
|
|
41
47
|
|
|
42
|
-
discoveries.uniq { |d| [d[:type], d[:tech], d[:provider]] }
|
|
48
|
+
discoveries.uniq { |d| [ d[:type], d[:tech], d[:provider] ] }
|
|
43
49
|
end
|
|
44
50
|
end
|
|
45
51
|
end
|
|
@@ -10,7 +10,7 @@ module NurseAndrea
|
|
|
10
10
|
fly: -> { ENV.key?("FLY_APP_NAME") },
|
|
11
11
|
heroku: -> { ENV.key?("DYNO") },
|
|
12
12
|
digitalocean: -> { ENV.key?("DIGITALOCEAN_APP_PLATFORM_COMPONENT_NAME") },
|
|
13
|
-
vercel: -> { ENV.key?("VERCEL") }
|
|
13
|
+
vercel: -> { ENV.key?("VERCEL") }
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
16
16
|
def self.detect
|
data/lib/nurse_andrea/railtie.rb
CHANGED
|
@@ -45,6 +45,11 @@ module NurseAndrea
|
|
|
45
45
|
discoveries = NurseAndrea::ManagedServiceScanner.scan
|
|
46
46
|
NurseAndrea.component_discoveries.concat(discoveries)
|
|
47
47
|
NurseAndrea.debug("[NurseAndrea] Discovered #{discoveries.size} managed services")
|
|
48
|
+
|
|
49
|
+
# Periodic rescan — picks up dependencies added after boot
|
|
50
|
+
# (env reloads, attached services). Respects
|
|
51
|
+
# disable_continuous_scan and SelfFilter short-circuit.
|
|
52
|
+
NurseAndrea::ContinuousScanner.start!
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
# Hook subscriptions (sql.active_record, cache_*, perform.active_job, etc.)
|
|
@@ -60,6 +65,7 @@ module NurseAndrea
|
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
at_exit do
|
|
68
|
+
NurseAndrea::ContinuousScanner.stop! rescue nil
|
|
63
69
|
NurseAndrea::MemorySampler.stop rescue nil
|
|
64
70
|
NurseAndrea::LogShipper.instance.flush! rescue nil
|
|
65
71
|
NurseAndrea::MetricsShipper.instance.flush! rescue nil
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Suppresses discovery emission when the SDK is loaded inside
|
|
2
|
+
# NurseAndrea itself. Both the InstrumentationSubscriber (hook-based)
|
|
3
|
+
# and the ManagedServiceScanner (env-based) must consult this filter
|
|
4
|
+
# before adding to NurseAndrea.component_discoveries — otherwise the
|
|
5
|
+
# platform's own infrastructure shows up as proposed components on
|
|
6
|
+
# every workspace dashboard.
|
|
7
|
+
|
|
8
|
+
module NurseAndrea
|
|
9
|
+
module SelfFilter
|
|
10
|
+
SELF_INDICATORS = %w[nurseandrea nurse-andrea nurse_andrea].freeze
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def platform_self?
|
|
14
|
+
return @platform_self if defined?(@platform_self)
|
|
15
|
+
@platform_self = compute_platform_self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def reset!
|
|
19
|
+
remove_instance_variable(:@platform_self) if defined?(@platform_self)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def host_matches?(*candidates)
|
|
23
|
+
candidates.compact.map(&:to_s).map(&:downcase).any? do |s|
|
|
24
|
+
SELF_INDICATORS.any? { |i| s.include?(i) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def compute_platform_self
|
|
31
|
+
return false unless defined?(Rails) && Rails.application
|
|
32
|
+
app_name = Rails.application.class.module_parent_name.to_s.downcase
|
|
33
|
+
SELF_INDICATORS.any? { |i| app_name.include?(i) }
|
|
34
|
+
rescue
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/nurse_andrea/version.rb
CHANGED
data/lib/nurse_andrea.rb
CHANGED
|
@@ -15,6 +15,9 @@ require "nurse_andrea/managed_service_scanner"
|
|
|
15
15
|
require "nurse_andrea/component_telemetry"
|
|
16
16
|
require "nurse_andrea/instrumentation_subscriber"
|
|
17
17
|
require "nurse_andrea/memory_sampler"
|
|
18
|
+
require "nurse_andrea/deploy"
|
|
19
|
+
require "nurse_andrea/self_filter"
|
|
20
|
+
require "nurse_andrea/continuous_scanner"
|
|
18
21
|
|
|
19
22
|
require "nurse_andrea/railtie" if defined?(Rails::Railtie)
|
|
20
23
|
require "nurse_andrea/engine" if defined?(Rails::Engine)
|
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: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ago AI LLC
|
|
@@ -28,6 +28,8 @@ files:
|
|
|
28
28
|
- lib/nurse_andrea/backfill.rb
|
|
29
29
|
- lib/nurse_andrea/component_telemetry.rb
|
|
30
30
|
- lib/nurse_andrea/configuration.rb
|
|
31
|
+
- lib/nurse_andrea/continuous_scanner.rb
|
|
32
|
+
- lib/nurse_andrea/deploy.rb
|
|
31
33
|
- lib/nurse_andrea/engine.rb
|
|
32
34
|
- lib/nurse_andrea/http_client.rb
|
|
33
35
|
- lib/nurse_andrea/instrumentation_subscriber.rb
|
|
@@ -43,6 +45,7 @@ files:
|
|
|
43
45
|
- lib/nurse_andrea/queue_depth_reporter.rb
|
|
44
46
|
- lib/nurse_andrea/railtie.rb
|
|
45
47
|
- lib/nurse_andrea/sanitizer.rb
|
|
48
|
+
- lib/nurse_andrea/self_filter.rb
|
|
46
49
|
- lib/nurse_andrea/version.rb
|
|
47
50
|
- nurse_andrea.gemspec
|
|
48
51
|
homepage: https://nurseandrea.io
|