hyperprobe-agent 1.2.27.pre.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 +13 -0
- data/README.md +157 -0
- data/lib/hyperprobe/agent.rb +438 -0
- data/lib/hyperprobe/core/broker.rb +213 -0
- data/lib/hyperprobe/core/evaluator.rb +128 -0
- data/lib/hyperprobe/core/logger.rb +177 -0
- data/lib/hyperprobe/core/monitoring_engine.rb +683 -0
- data/lib/hyperprobe/core/quota.rb +111 -0
- data/lib/hyperprobe/core/safe_ast_validator.rb +91 -0
- data/lib/hyperprobe/core/safety.rb +179 -0
- data/lib/hyperprobe/core/serializer.rb +425 -0
- data/lib/hyperprobe/core/trace_extractor.rb +158 -0
- data/lib/hyperprobe/lambda.rb +90 -0
- data/lib/hyperprobe/protos/agent_pb.rb +33 -0
- data/lib/hyperprobe/protos/agent_services_pb.rb +31 -0
- data/lib/hyperprobe/protos.rb +14 -0
- data/lib/hyperprobe/railtie.rb +16 -0
- data/lib/hyperprobe/version.rb +5 -0
- data/lib/hyperprobe-agent.rb +3 -0
- data/lib/hyperprobe.rb +153 -0
- metadata +149 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0fc98f264e7d9cd4fe3c902f686ba8e6b95e7c6f272126d4b6a811400f0eff39
|
|
4
|
+
data.tar.gz: 67b10ac7149644e1a4644693e016b31b48dc8f6892fcc9c1c5e360e5604495a5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 44c3a8718c1df4b1b843f9320b4deb3237ab722af67128cb38670c80ebf1bb74e2a6a5f81ebb7d9b5d4f12af92b1ea43b6abcc13ec01f6a4a3f34500cf42006f
|
|
7
|
+
data.tar.gz: 47e7bb3084e5700db38c353cf351046c7082abf417ab30a41350270c72329194b89cceba596df0cdc9cd53d0f40c3e05cc02ef712290895b9e1d302f39360f69
|
data/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Hyperprobe Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hyperprobe. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and its associated documentation are proprietary to Hyperprobe.
|
|
6
|
+
No permission is granted to use, copy, modify, distribute, sublicense, or
|
|
7
|
+
create derivative works except as expressly authorized under a written
|
|
8
|
+
commercial license agreement with Hyperprobe.
|
|
9
|
+
|
|
10
|
+
A valid commercial license from Hyperprobe is required to operate this
|
|
11
|
+
software.
|
|
12
|
+
|
|
13
|
+
For licensing inquiries, visit https://hyperprobe.co/.
|
data/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# HyperProbe Ruby Agent
|
|
2
|
+
|
|
3
|
+
Production-grade, non-breaking live debugger and telemetry agent for Ruby and JRuby applications.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The HyperProbe Ruby Agent allows developers to set non-breaking live probes (snapshots, logs, counters, metrics, and durations) in production environments without pausing execution or modifying source code.
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
|
|
11
|
+
- **Snapshots**: Capture local variables across stack frames and evaluate watch expressions without pausing threads.
|
|
12
|
+
- **Dynamic Logs**: Evaluate log templates (`${var}`) in context, with optional stdout emission and full value redaction.
|
|
13
|
+
- **Metrics & Counters**: Measure event counts, custom expressions, and block/loop durations.
|
|
14
|
+
- **Safety Shield**: Continuous thread lag monitoring and pause duration budgets with automated circuit breaker shedding (`GREEN`/`YELLOW`/`RED`).
|
|
15
|
+
- **Quota Governance**: Token bucket rate limiting for hits per second and network bandwidth.
|
|
16
|
+
- **Distributed Tracing**: Integrations with OpenTelemetry, Datadog (`ddtrace`), New Relic (`newrelic_rpm`), Elastic APM, and custom trace getters.
|
|
17
|
+
- **Multi-Process & Serverless**: Native support for Puma/Unicorn worker forking and AWS Lambda execution.
|
|
18
|
+
- **JRuby on JVM**: First-class JRuby support with native JVM HTTP/2 gRPC transport and hybrid Java object serialization.
|
|
19
|
+
|
|
20
|
+
## Runtime Compatibility
|
|
21
|
+
|
|
22
|
+
| Platform | Supported Versions | Prerequisites |
|
|
23
|
+
| :--- | :--- | :--- |
|
|
24
|
+
| **Standard Ruby (CRuby / MRI)** | **Ruby 3.0, 3.1, 3.2, 3.3, 3.4, 4.0+** | None (Zero launch flags needed) |
|
|
25
|
+
| **JRuby (Ruby on JVM)** | **JRuby 9.3+, 9.4+, 10.x+** | **Java 11+** and `JRUBY_OPTS="--debug"` |
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Add to your `Gemfile`:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem 'hyperprobe-agent'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
And run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bundle install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
Create a dedicated `hyperprobe.rb` file in your application root:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# hyperprobe.rb
|
|
47
|
+
require 'hyperprobe'
|
|
48
|
+
|
|
49
|
+
HyperProbe.start(
|
|
50
|
+
service_id: '<service-uuid-from-dashboard>',
|
|
51
|
+
environment: ENV['HYPERPROBE_ENVIRONMENT'] || 'production',
|
|
52
|
+
broker_url: 'https://logger.app.hyperprobe.co',
|
|
53
|
+
commit_sha: ENV['GIT_COMMIT']
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Require it as early as possible in your application entrypoint (e.g. `app.rb`, `server.rb`, `config/environment.rb`):
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# app.rb
|
|
61
|
+
require_relative 'hyperprobe'
|
|
62
|
+
|
|
63
|
+
# ... start your web server / application ...
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Environment Variables
|
|
67
|
+
|
|
68
|
+
| Variable | Description | Default |
|
|
69
|
+
| :--- | :--- | :--- |
|
|
70
|
+
| `HYPERPROBE_SERVICE_ID` | Unique UUID for the service | (Required) |
|
|
71
|
+
| `HYPERPROBE_ENVIRONMENT` | Deployment environment (`production`, `staging`, etc.) | (Required) |
|
|
72
|
+
| `HYPERPROBE_BROKER_URL` | gRPC Broker endpoint | (Required) |
|
|
73
|
+
| `GIT_COMMIT` / `HYPERPROBE_COMMIT_SHA` | Current deployment commit SHA | (Required) |
|
|
74
|
+
| `HYPERPROBE_DISABLED` | Set to `YES` to disable the agent | `NO` |
|
|
75
|
+
| `HYPERPROBE_SYNC_INTERVAL_MS` | Probe synchronization interval | `60000` |
|
|
76
|
+
| `HYPERPROBE_FLUSH_INTERVAL_MS` | Telemetry batch flush interval | `1000` |
|
|
77
|
+
| `HYPERPROBE_HITS_PER_SEC` | Maximum probe hits evaluated per second | `10` |
|
|
78
|
+
| `HYPERPROBE_BANDWIDTH_KB_PER_SEC` | Maximum telemetry bandwidth in KB/s | `1024` |
|
|
79
|
+
| `HYPERPROBE_MAX_LAG_MS` | Safety threshold for thread lag | `50` |
|
|
80
|
+
| `HYPERPROBE_PAUSE_BUDGET_MS` | Maximum probe execution pause per second | `15` |
|
|
81
|
+
| `HYPERPROBE_REDACT_KEYS` | Comma-separated list of sensitive key patterns | `password,secret,token,...` |
|
|
82
|
+
| `HYPERPROBE_REDACT_VALUES` | Comma-separated list of sensitive value regexes | `""` |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## ☕ JRuby Support (Ruby on the JVM)
|
|
87
|
+
|
|
88
|
+
HyperProbe provides native, first-class support for JRuby applications running on the JVM.
|
|
89
|
+
|
|
90
|
+
### Prerequisites for JRuby
|
|
91
|
+
1. **JRuby Version**: JRuby 9.3+, 9.4+, or 10.x+.
|
|
92
|
+
2. **Java Runtime**: Java 11, 17, 21, or 25+.
|
|
93
|
+
3. **Debug Flag**: The JVM must be launched with `--debug` or `JRUBY_OPTS="--debug"`.
|
|
94
|
+
|
|
95
|
+
### Why is `--debug` Required on JRuby?
|
|
96
|
+
JRuby's JIT (Just-In-Time) compiler aggressively optimizes hot methods into raw JVM bytecode, stripping out line-level checkpoints and local variable names by default.
|
|
97
|
+
|
|
98
|
+
Running with `--debug` instructs JRuby's JVM compiler to:
|
|
99
|
+
* Retain line-by-line tracing checkpoints so probes trigger reliably on every execution.
|
|
100
|
+
* Preserve local variable tables in memory for snapshot capture.
|
|
101
|
+
|
|
102
|
+
> **Pre-Flight Protection**: If JRuby is started without `--debug`, HyperProbe detects this at startup, logs an informative error message, and safely aborts initialization without attaching hooks or degrading performance.
|
|
103
|
+
|
|
104
|
+
### How to Run JRuby with HyperProbe
|
|
105
|
+
|
|
106
|
+
#### 1. In Docker / Kubernetes
|
|
107
|
+
Add the environment variable in your `Dockerfile` or Deployment manifest:
|
|
108
|
+
```dockerfile
|
|
109
|
+
ENV JRUBY_OPTS="--debug"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### 2. Command Line / Local
|
|
113
|
+
Launch JRuby with the `--debug` flag:
|
|
114
|
+
```bash
|
|
115
|
+
jruby --debug app.rb
|
|
116
|
+
# or:
|
|
117
|
+
JRUBY_OPTS="--debug" jruby app.rb
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### JRuby Architecture Highlights
|
|
121
|
+
* **Zero C-Extensions**: On JRuby, HyperProbe automatically uses Java's built-in `java.net.http.HttpClient` (HTTP/2) for gRPC communication to the Logger. No native compilation or C-extensions are required.
|
|
122
|
+
* **Hybrid Object Serialization**: Automatically detects and serializes both native Ruby objects and Java collections (`java.util.Map`, `java.util.List`, `java.util.Set`, and Java POJOs).
|
|
123
|
+
* **Multi-Threaded Safety**: Thread-safe synchronization across parallel JVM worker threads.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Puma / Unicorn Multi-Worker Integration
|
|
128
|
+
|
|
129
|
+
When using multi-worker servers like Puma or Unicorn in cluster mode on CRuby:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# config/puma.rb
|
|
133
|
+
on_worker_boot do
|
|
134
|
+
HyperProbe.after_fork
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
*(Note: On JRuby, Puma runs in multi-threaded mode on the JVM without process forking, so `after_fork` is not required).*
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## AWS Lambda Integration
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
require 'hyperprobe'
|
|
146
|
+
|
|
147
|
+
handler = ->(event:, context:) {
|
|
148
|
+
# Handler logic
|
|
149
|
+
{ statusCode: 200, body: "OK" }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
exports.handler = HyperProbe.wrap_lambda(
|
|
153
|
+
service_id: '<service-uuid>',
|
|
154
|
+
environment: 'production',
|
|
155
|
+
broker_url: 'https://logger.app.hyperprobe.co'
|
|
156
|
+
) { |event:, context:| handler.call(event: event, context: context) }
|
|
157
|
+
```
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'thread'
|
|
6
|
+
require_relative 'version'
|
|
7
|
+
require_relative 'core/quota'
|
|
8
|
+
require_relative 'core/safety'
|
|
9
|
+
require_relative 'core/serializer'
|
|
10
|
+
require_relative 'core/evaluator'
|
|
11
|
+
require_relative 'core/trace_extractor'
|
|
12
|
+
require_relative 'core/monitoring_engine'
|
|
13
|
+
require_relative 'core/broker'
|
|
14
|
+
|
|
15
|
+
module HyperProbe
|
|
16
|
+
UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.freeze
|
|
17
|
+
|
|
18
|
+
class Agent
|
|
19
|
+
attr_reader :options, :agent_id, :owner_pid, :is_shutdown, :active_probes, :global_config, :quota_manager, :safety_monitor, :broker_client, :engine
|
|
20
|
+
|
|
21
|
+
def initialize(options = {})
|
|
22
|
+
@options = (options || {}).dup
|
|
23
|
+
@owner_pid = Process.pid
|
|
24
|
+
@agent_id = SecureRandom.uuid
|
|
25
|
+
@is_shutdown = false
|
|
26
|
+
@is_agent_disabled = false
|
|
27
|
+
@stop_event = ConditionVariable.new
|
|
28
|
+
@mutex = Monitor.new # Reentrant mutex in Ruby
|
|
29
|
+
|
|
30
|
+
@log = Core::Logger.get_logger('hyperprobe:agent')
|
|
31
|
+
@log_broker = Core::Logger.get_logger('hyperprobe:broker')
|
|
32
|
+
@log_safety = Core::Logger.get_logger('hyperprobe:safety')
|
|
33
|
+
@log_stats = Core::Logger.get_logger('hyperprobe:stats')
|
|
34
|
+
|
|
35
|
+
parse_configuration
|
|
36
|
+
|
|
37
|
+
@quota_manager = Core::QuotaManager.new(@hits_per_sec, @bandwidth_kb_per_sec * 1024)
|
|
38
|
+
@safety_monitor = Core::SafetyMonitor.new(
|
|
39
|
+
method(:handle_health_change),
|
|
40
|
+
max_lag_ms: @max_lag_ms,
|
|
41
|
+
pause_budget_ms: @pause_budget_ms,
|
|
42
|
+
is_ephemeral_lambda: @is_ephemeral_lambda
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@broker_client = Core::BrokerClient.new(
|
|
46
|
+
broker_url: @broker_url,
|
|
47
|
+
service_id: @service_id,
|
|
48
|
+
environment: @environment,
|
|
49
|
+
commit_sha: @commit_sha,
|
|
50
|
+
agent_id: @agent_id,
|
|
51
|
+
agent_version: VERSION,
|
|
52
|
+
rpc_timeout_sec: @rpc_timeout_sec,
|
|
53
|
+
enable_keep_alive: @enable_keep_alive
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@telemetry_queue = Queue.new
|
|
57
|
+
@active_probes = {}
|
|
58
|
+
@local_hits = {}
|
|
59
|
+
@cooldown_timer = nil
|
|
60
|
+
|
|
61
|
+
@engine = Core::MonitoringEngine.new(
|
|
62
|
+
@quota_manager,
|
|
63
|
+
@safety_monitor,
|
|
64
|
+
method(:handle_capture),
|
|
65
|
+
@options[:set_trace_id]
|
|
66
|
+
)
|
|
67
|
+
@engine.set_global_config(@global_config)
|
|
68
|
+
|
|
69
|
+
@sync_thread = nil
|
|
70
|
+
@flush_thread = nil
|
|
71
|
+
@stats_thread = nil
|
|
72
|
+
|
|
73
|
+
if @is_ephemeral_lambda
|
|
74
|
+
@log.debug 'Running in Ephemeral AWS Lambda Mode.'
|
|
75
|
+
else
|
|
76
|
+
@safety_monitor.start
|
|
77
|
+
start_background_loops
|
|
78
|
+
sync_with_broker
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@log.info "Agent started for #{@service_id} in #{@environment} (v#{VERSION})"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def agent_disabled?
|
|
85
|
+
@is_agent_disabled
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def telemetry_queue_length
|
|
89
|
+
@telemetry_queue.size
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def force_sync(timeout_sec = nil)
|
|
93
|
+
return if @is_shutdown || @is_agent_disabled
|
|
94
|
+
|
|
95
|
+
sync_with_broker(timeout_sec)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def force_flush(timeout_sec = nil)
|
|
99
|
+
return if @is_shutdown || @is_agent_disabled
|
|
100
|
+
|
|
101
|
+
flush_telemetry(timeout_sec)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def shutdown
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
return if @is_shutdown
|
|
107
|
+
|
|
108
|
+
@is_shutdown = true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@safety_monitor&.stop
|
|
112
|
+
@engine&.close
|
|
113
|
+
@cooldown_timer&.exit if @cooldown_timer&.alive?
|
|
114
|
+
@broker_client&.shutdown
|
|
115
|
+
|
|
116
|
+
[@sync_thread, @flush_thread, @stats_thread].compact.each do |t|
|
|
117
|
+
t.kill if t.alive? && t != Thread.current
|
|
118
|
+
t.join(1.0) rescue nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def after_fork
|
|
123
|
+
return if defined?(JRUBY_VERSION)
|
|
124
|
+
|
|
125
|
+
@mutex.synchronize do
|
|
126
|
+
return if @is_shutdown
|
|
127
|
+
|
|
128
|
+
@owner_pid = Process.pid
|
|
129
|
+
@agent_id = SecureRandom.uuid
|
|
130
|
+
|
|
131
|
+
# Reset queues and client for new process
|
|
132
|
+
@telemetry_queue = Queue.new
|
|
133
|
+
@broker_client = Core::BrokerClient.new(
|
|
134
|
+
broker_url: @broker_url,
|
|
135
|
+
service_id: @service_id,
|
|
136
|
+
environment: @environment,
|
|
137
|
+
commit_sha: @commit_sha,
|
|
138
|
+
agent_id: @agent_id,
|
|
139
|
+
agent_version: VERSION,
|
|
140
|
+
rpc_timeout_sec: @rpc_timeout_sec,
|
|
141
|
+
enable_keep_alive: @enable_keep_alive
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@safety_monitor.start
|
|
145
|
+
start_background_loops
|
|
146
|
+
sync_with_broker
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def parse_configuration
|
|
153
|
+
@service_id = get_opt(:service_id, :serviceId) || ENV['HYPERPROBE_SERVICE_ID']
|
|
154
|
+
@environment = get_opt(:environment) || ENV['HYPERPROBE_ENVIRONMENT']
|
|
155
|
+
@broker_url = get_opt(:broker_url, :brokerUrl) || ENV['HYPERPROBE_BROKER_URL']
|
|
156
|
+
@commit_sha = get_opt(:commit_sha, :commitSha) || ENV['GIT_COMMIT'] || ENV['HYPERPROBE_COMMIT_SHA']
|
|
157
|
+
|
|
158
|
+
@sync_interval_sec = ((get_opt(:sync_interval_ms, :syncIntervalMs) || parse_env_int('HYPERPROBE_SYNC_INTERVAL_MS', 60_000)).to_f / 1000.0)
|
|
159
|
+
@flush_interval_sec = ((get_opt(:flush_interval_ms, :flushIntervalMs) || parse_env_int('HYPERPROBE_FLUSH_INTERVAL_MS', 1000)).to_f / 1000.0)
|
|
160
|
+
@max_queue_size = get_opt(:max_queue_size, :maxQueueSize) || parse_env_int('HYPERPROBE_MAX_QUEUE_SIZE', 100)
|
|
161
|
+
@cooldown_sec = get_opt(:cooldown_sec, :cooldownSec) || parse_env_int('HYPERPROBE_COOLDOWN_SEC', 10)
|
|
162
|
+
|
|
163
|
+
@hits_per_sec = get_opt(:hits_per_sec, :hitsPerSec) || parse_env_int('HYPERPROBE_HITS_PER_SEC', 10)
|
|
164
|
+
@bandwidth_kb_per_sec = get_opt(:bandwidth_kb_per_sec, :bandwidthKbPerSec) || parse_env_int('HYPERPROBE_BANDWIDTH_KB_PER_SEC', 1024)
|
|
165
|
+
@max_lag_ms = (get_opt(:max_lag_ms, :maxLagMs) || parse_env_int('HYPERPROBE_MAX_LAG_MS', 50)).to_f
|
|
166
|
+
@pause_budget_ms = (get_opt(:pause_budget_ms, :pauseBudgetMs) || parse_env_int('HYPERPROBE_PAUSE_BUDGET_MS', 15)).to_f
|
|
167
|
+
@rpc_timeout_sec = (get_opt(:rpc_timeout_sec, :rpcTimeoutSec) || parse_env_int('HYPERPROBE_RPC_TIMEOUT_SEC', 10)).to_f
|
|
168
|
+
|
|
169
|
+
is_aws_lambda = !ENV['AWS_LAMBDA_FUNCTION_NAME'].nil?
|
|
170
|
+
is_local_emulator = ENV['IS_OFFLINE'] == 'true' || ENV['AWS_SAM_LOCAL'] == 'true'
|
|
171
|
+
@is_ephemeral_lambda = (get_opt(:is_lambda, :isLambda) || is_aws_lambda) && !is_local_emulator
|
|
172
|
+
enable_keep_alive_opt = get_opt(:enable_keep_alive, :enableKeepAlive)
|
|
173
|
+
@enable_keep_alive = enable_keep_alive_opt.nil? ? !@is_ephemeral_lambda : enable_keep_alive_opt
|
|
174
|
+
|
|
175
|
+
redact_keys_raw = get_opt(:redact_keys, :redactKeys) || (ENV['HYPERPROBE_REDACT_KEYS'] ? ENV['HYPERPROBE_REDACT_KEYS'].split(',') : %w[password secret token authorization cookie key signature])
|
|
176
|
+
redact_values_raw = get_opt(:redact_values, :redactValues) || (ENV['HYPERPROBE_REDACT_VALUES'] ? ENV['HYPERPROBE_REDACT_VALUES'].split(',') : [])
|
|
177
|
+
|
|
178
|
+
@global_config = {
|
|
179
|
+
redact_keys: Array(redact_keys_raw).map(&:to_s).map(&:strip).reject(&:empty?),
|
|
180
|
+
redact_values: Array(redact_values_raw).map(&:to_s).map(&:strip).reject(&:empty?),
|
|
181
|
+
max_object_depth: get_opt(:max_object_depth, :maxObjectDepth) || parse_env_int('HYPERPROBE_MAX_OBJECT_DEPTH', 3),
|
|
182
|
+
max_array_length: get_opt(:max_array_length, :maxArrayLength) || parse_env_int('HYPERPROBE_MAX_ARRAY_LENGTH', 3),
|
|
183
|
+
stack_frame_depth: get_opt(:stack_frame_depth, :stackFrameDepth) || parse_env_int('HYPERPROBE_STACK_FRAME_DEPTH', 3),
|
|
184
|
+
max_object_properties: get_opt(:max_object_properties, :maxObjectProperties) || parse_env_int('HYPERPROBE_MAX_OBJECT_PROPERTIES', 50),
|
|
185
|
+
max_string_length: get_opt(:max_string_length, :maxStringLength) || parse_env_int('HYPERPROBE_MAX_STRING_LENGTH', 1024)
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def get_opt(*keys)
|
|
190
|
+
keys.each do |k|
|
|
191
|
+
return @options[k] if @options.key?(k)
|
|
192
|
+
return @options[k.to_s] if @options.key?(k.to_s)
|
|
193
|
+
end
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def parse_env_int(key, default_val)
|
|
198
|
+
val = ENV[key]
|
|
199
|
+
return default_val if val.nil? || val.strip.empty?
|
|
200
|
+
|
|
201
|
+
val.to_i rescue default_val
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def start_background_loops
|
|
205
|
+
@sync_thread = Thread.new { sync_loop }
|
|
206
|
+
@sync_thread.name = 'hyperprobe-sync' if @sync_thread.respond_to?(:name=)
|
|
207
|
+
|
|
208
|
+
@flush_thread = Thread.new { flush_loop }
|
|
209
|
+
@flush_thread.name = 'hyperprobe-flush' if @flush_thread.respond_to?(:name=)
|
|
210
|
+
|
|
211
|
+
@stats_thread = Thread.new { stats_loop }
|
|
212
|
+
@stats_thread.name = 'hyperprobe-stats' if @stats_thread.respond_to?(:name=)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def sync_loop
|
|
216
|
+
until @is_shutdown
|
|
217
|
+
sleep @sync_interval_sec
|
|
218
|
+
break if @is_shutdown
|
|
219
|
+
|
|
220
|
+
begin
|
|
221
|
+
sync_with_broker
|
|
222
|
+
rescue StandardError => e
|
|
223
|
+
# Silently handle transient sync error
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def sync_with_broker(timeout_sec = nil)
|
|
229
|
+
return if @is_shutdown
|
|
230
|
+
|
|
231
|
+
@log_broker.debug "sync_with_broker started #{timeout_sec ? "with timeout #{timeout_sec}" : ''}"
|
|
232
|
+
response = @broker_client.get_probes(timeout_sec)
|
|
233
|
+
return unless response
|
|
234
|
+
|
|
235
|
+
@log_broker.debug "sync_with_broker: got #{response.probes.length} probes from broker"
|
|
236
|
+
|
|
237
|
+
# Dynamic global config updates from broker
|
|
238
|
+
if response.global_config
|
|
239
|
+
gc = response.global_config
|
|
240
|
+
@mutex.synchronize do
|
|
241
|
+
@global_config[:redact_keys] = gc.redact_keys.to_a unless gc.redact_keys.empty?
|
|
242
|
+
@global_config[:redact_values] = gc.redact_values.to_a unless gc.redact_values.empty?
|
|
243
|
+
@global_config[:max_object_depth] = gc.max_object_depth if gc.max_object_depth.positive?
|
|
244
|
+
@global_config[:max_array_length] = gc.max_array_length if gc.max_array_length.positive?
|
|
245
|
+
@global_config[:stack_frame_depth] = gc.stack_frame_depth if gc.stack_frame_depth.positive?
|
|
246
|
+
@global_config[:max_object_properties] = gc.max_object_properties if gc.max_object_properties.positive?
|
|
247
|
+
@global_config[:max_string_length] = gc.max_string_length if gc.max_string_length.positive?
|
|
248
|
+
end
|
|
249
|
+
@engine.set_global_config(@global_config)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
server_probes = response.probes.to_a
|
|
253
|
+
server_probe_ids = server_probes.map(&:id)
|
|
254
|
+
|
|
255
|
+
to_apply = []
|
|
256
|
+
now_ms = (Time.now.to_f * 1000).to_i
|
|
257
|
+
|
|
258
|
+
@mutex.synchronize do
|
|
259
|
+
# Clean stale local probes
|
|
260
|
+
@active_probes.delete_if { |pid, _| !server_probe_ids.include?(pid) }
|
|
261
|
+
@local_hits.delete_if { |pid, _| !server_probe_ids.include?(pid) }
|
|
262
|
+
|
|
263
|
+
server_probes.each do |probe|
|
|
264
|
+
hits = @local_hits[probe.id] || 0
|
|
265
|
+
is_expired = probe.expiry_time.positive? && probe.expiry_time <= now_ms
|
|
266
|
+
|
|
267
|
+
if hits < probe.hit_limit && !is_expired
|
|
268
|
+
to_apply << probe
|
|
269
|
+
@active_probes[probe.id] = probe
|
|
270
|
+
else
|
|
271
|
+
@active_probes.delete(probe.id)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
@log_broker.debug "Found #{to_apply.length} active probes to apply locally"
|
|
277
|
+
@engine.set_probes(to_apply)
|
|
278
|
+
true
|
|
279
|
+
rescue StandardError => e
|
|
280
|
+
@log_broker.error("Failed to sync with broker: #{e.message}", e)
|
|
281
|
+
false
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def flush_loop
|
|
285
|
+
until @is_shutdown
|
|
286
|
+
sleep @flush_interval_sec
|
|
287
|
+
break if @is_shutdown
|
|
288
|
+
|
|
289
|
+
begin
|
|
290
|
+
flush_telemetry
|
|
291
|
+
rescue StandardError => e
|
|
292
|
+
# Silently handle flush loop error
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def flush_telemetry(timeout_sec = nil)
|
|
298
|
+
return if @is_shutdown || @telemetry_queue.empty?
|
|
299
|
+
|
|
300
|
+
batch = []
|
|
301
|
+
until @telemetry_queue.empty?
|
|
302
|
+
begin
|
|
303
|
+
batch << @telemetry_queue.pop(true)
|
|
304
|
+
rescue ThreadError
|
|
305
|
+
break
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
return if batch.empty?
|
|
310
|
+
|
|
311
|
+
@log_broker.info "Flushing #{batch.length} telemetry events to broker..."
|
|
312
|
+
|
|
313
|
+
begin
|
|
314
|
+
events = batch.map { |item| item[:event] }
|
|
315
|
+
finished_probe_ids = @broker_client.report_telemetry(events, timeout_sec)
|
|
316
|
+
|
|
317
|
+
@log_broker.info "Successfully flushed #{batch.length} telemetry events to broker"
|
|
318
|
+
|
|
319
|
+
# Commit bandwidth reservations
|
|
320
|
+
batch.each do |item|
|
|
321
|
+
item[:reservation]&.commit
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Handle globally finished probes
|
|
325
|
+
if finished_probe_ids && !finished_probe_ids.empty?
|
|
326
|
+
changed = false
|
|
327
|
+
@mutex.synchronize do
|
|
328
|
+
finished_probe_ids.each do |pid|
|
|
329
|
+
if @active_probes.key?(pid)
|
|
330
|
+
@active_probes.delete(pid)
|
|
331
|
+
changed = true
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
@engine.set_probes(@active_probes.values) if changed
|
|
336
|
+
end
|
|
337
|
+
rescue StandardError => e
|
|
338
|
+
@log_broker.error("Failed to flush telemetry: #{e.message}", e)
|
|
339
|
+
# On flush failure, re-queue events up to max capacity
|
|
340
|
+
batch.each do |item|
|
|
341
|
+
item[:reservation]&.release
|
|
342
|
+
if @telemetry_queue.size < @max_queue_size
|
|
343
|
+
@telemetry_queue.push(item)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def stats_loop
|
|
350
|
+
until @is_shutdown
|
|
351
|
+
sleep 5.0
|
|
352
|
+
break if @is_shutdown
|
|
353
|
+
|
|
354
|
+
begin
|
|
355
|
+
stats = @engine.get_stats
|
|
356
|
+
if stats[:hits].positive? || stats[:skips].positive?
|
|
357
|
+
@log_stats.info "Probes Hit: #{stats[:hits]}, Probes Skipped: #{stats[:skips]}"
|
|
358
|
+
end
|
|
359
|
+
rescue StandardError
|
|
360
|
+
# Ignore stats error
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def handle_capture(event)
|
|
366
|
+
return if @is_shutdown || @is_agent_disabled
|
|
367
|
+
|
|
368
|
+
probe_id = event[:probe_id]
|
|
369
|
+
@log.debug "Captured event for probe #{probe_id}"
|
|
370
|
+
probe = nil
|
|
371
|
+
hits = 0
|
|
372
|
+
|
|
373
|
+
@mutex.synchronize do
|
|
374
|
+
probe = @active_probes[probe_id]
|
|
375
|
+
return unless probe
|
|
376
|
+
|
|
377
|
+
hits = (@local_hits[probe_id] || 0) + 1
|
|
378
|
+
@local_hits[probe_id] = hits
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Reserve bandwidth
|
|
382
|
+
event_size = @broker_client.estimate_event_size(event)
|
|
383
|
+
reservation = @quota_manager.reserve_bandwidth(event_size)
|
|
384
|
+
unless reservation
|
|
385
|
+
@log.debug "Bandwidth quota exceeded; dropping event for probe #{probe_id}"
|
|
386
|
+
return
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Admission to telemetry queue
|
|
390
|
+
item = { event: event, reservation: reservation }
|
|
391
|
+
if @telemetry_queue.size < @max_queue_size
|
|
392
|
+
@telemetry_queue.push(item)
|
|
393
|
+
else
|
|
394
|
+
@log.debug "Telemetry queue full. Dropping event for probe #{probe_id}"
|
|
395
|
+
reservation.release
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Enforce hitLimit fuse
|
|
399
|
+
hit_limit = probe.respond_to?(:hit_limit) ? probe.hit_limit : probe[:hit_limit]
|
|
400
|
+
if hits >= hit_limit
|
|
401
|
+
remaining_probes = nil
|
|
402
|
+
@mutex.synchronize do
|
|
403
|
+
@active_probes.delete(probe_id)
|
|
404
|
+
remaining_probes = @active_probes.values
|
|
405
|
+
end
|
|
406
|
+
@engine.set_probes(remaining_probes)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def handle_health_change(health, reason = nil)
|
|
411
|
+
return if @is_shutdown
|
|
412
|
+
|
|
413
|
+
case health
|
|
414
|
+
when Core::AgentHealth::RED
|
|
415
|
+
@log_safety.error "Safety Shield Triggered: RED Status. #{reason || ''}"
|
|
416
|
+
@engine.suspend
|
|
417
|
+
|
|
418
|
+
@cooldown_timer&.exit if @cooldown_timer&.alive?
|
|
419
|
+
cooldown_duration = @cooldown_sec
|
|
420
|
+
@cooldown_timer = Thread.new do
|
|
421
|
+
@log_safety.info "Cooldown period started (#{cooldown_duration}s)..."
|
|
422
|
+
sleep cooldown_duration
|
|
423
|
+
if @safety_monitor.get_health == Core::AgentHealth::GREEN
|
|
424
|
+
@log_safety.info "Cooldown period ended. Resuming instrumentation..."
|
|
425
|
+
@engine.resume
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
when Core::AgentHealth::YELLOW
|
|
429
|
+
@log_safety.warn "Safety Warning: YELLOW Status (Moderate Overhead). #{reason || ''}"
|
|
430
|
+
when Core::AgentHealth::GREEN
|
|
431
|
+
@log_safety.info "Status back to GREEN. #{reason || ''}"
|
|
432
|
+
if @engine.is_suspended && (!@cooldown_timer || !@cooldown_timer.alive?)
|
|
433
|
+
@engine.resume
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|