miniapm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +43 -0
- data/LICENSE +21 -0
- data/README.md +174 -0
- data/lib/generators/miniapm/install_generator.rb +27 -0
- data/lib/generators/miniapm/templates/README +19 -0
- data/lib/generators/miniapm/templates/initializer.rb +60 -0
- data/lib/miniapm/configuration.rb +176 -0
- data/lib/miniapm/context.rb +138 -0
- data/lib/miniapm/error_event.rb +130 -0
- data/lib/miniapm/exporters/errors.rb +67 -0
- data/lib/miniapm/exporters/otlp.rb +90 -0
- data/lib/miniapm/instrumentations/activejob.rb +271 -0
- data/lib/miniapm/instrumentations/activerecord.rb +123 -0
- data/lib/miniapm/instrumentations/base.rb +61 -0
- data/lib/miniapm/instrumentations/cache.rb +85 -0
- data/lib/miniapm/instrumentations/http/faraday.rb +112 -0
- data/lib/miniapm/instrumentations/http/httparty.rb +84 -0
- data/lib/miniapm/instrumentations/http/net_http.rb +99 -0
- data/lib/miniapm/instrumentations/rails/controller.rb +129 -0
- data/lib/miniapm/instrumentations/rails/railtie.rb +42 -0
- data/lib/miniapm/instrumentations/redis/redis.rb +135 -0
- data/lib/miniapm/instrumentations/redis/redis_client.rb +116 -0
- data/lib/miniapm/instrumentations/registry.rb +90 -0
- data/lib/miniapm/instrumentations/search/elasticsearch.rb +121 -0
- data/lib/miniapm/instrumentations/search/opensearch.rb +120 -0
- data/lib/miniapm/instrumentations/search/searchkick.rb +119 -0
- data/lib/miniapm/instrumentations/sidekiq.rb +185 -0
- data/lib/miniapm/middleware/error_handler.rb +120 -0
- data/lib/miniapm/middleware/rack.rb +103 -0
- data/lib/miniapm/span.rb +289 -0
- data/lib/miniapm/testing.rb +209 -0
- data/lib/miniapm/trace.rb +26 -0
- data/lib/miniapm/transport/batch_sender.rb +345 -0
- data/lib/miniapm/transport/http.rb +45 -0
- data/lib/miniapm/version.rb +5 -0
- data/lib/miniapm.rb +184 -0
- metadata +183 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 525eb11f5af8c0b35aedcb05f67523d4f390c56af7fd3fd1dc3169ad1f934967
|
|
4
|
+
data.tar.gz: '09aa0e5d42736b39c895174cdddc9aec4944de4168beae335c6e335b212fb3ba'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bfd98917e4502be3f040ade6c9b5e1666d3a0e1f05d99cda07be033eee5f82d99e8e5584906d6e09514b01c2e8dd22d4ed9e6e76beab24cd16240d4a1d9e321d
|
|
7
|
+
data.tar.gz: 9b8f7707deb4cd5c3a941c87c5416a64188527fe19c605c3fdc37a27f1ac3df44cf0a6de3c5acc72f4ef0fe67bb870e711418ac28ded78c726fff9538a8077ac
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-01-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- OTLP trace export to MiniAPM server
|
|
15
|
+
- Error tracking with fingerprinting and parameter filtering
|
|
16
|
+
- W3C Trace Context support for distributed tracing
|
|
17
|
+
- Automatic instrumentation for:
|
|
18
|
+
- Rails (ActionController, ActionView)
|
|
19
|
+
- ActiveRecord
|
|
20
|
+
- ActiveJob (SolidQueue, Sidekiq adapter)
|
|
21
|
+
- Sidekiq
|
|
22
|
+
- Rails Cache
|
|
23
|
+
- Net::HTTP
|
|
24
|
+
- HTTParty
|
|
25
|
+
- Faraday
|
|
26
|
+
- Elasticsearch
|
|
27
|
+
- OpenSearch
|
|
28
|
+
- Searchkick
|
|
29
|
+
- Redis (redis-client and legacy redis gem)
|
|
30
|
+
- Async batched sending with configurable batch size and flush interval
|
|
31
|
+
- Sampling support with configurable sample rate
|
|
32
|
+
- Rails generator for easy setup (`rails g miniapm:install`)
|
|
33
|
+
- Testing helpers for capturing spans and errors in tests
|
|
34
|
+
- Health check endpoint verification
|
|
35
|
+
- Retry logic with exponential backoff for failed exports
|
|
36
|
+
|
|
37
|
+
### Security
|
|
38
|
+
- Automatic parameter filtering for sensitive data
|
|
39
|
+
- SQL query sanitization option
|
|
40
|
+
- No sensitive data logged by default
|
|
41
|
+
|
|
42
|
+
[Unreleased]: https://github.com/miniapm/miniapm-ruby/compare/v1.0.0...HEAD
|
|
43
|
+
[1.0.0]: https://github.com/miniapm/miniapm-ruby/releases/tag/v1.0.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Chris Hasinski
|
|
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,174 @@
|
|
|
1
|
+
# MiniAPM Ruby Client
|
|
2
|
+
|
|
3
|
+
A lightweight, zero-dependency APM client for Rails applications. Exports traces in OTLP format, captures errors, and provides comprehensive instrumentation.
|
|
4
|
+
|
|
5
|
+
**Website:** [miniapm.com](https://miniapm.com)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **OTLP Compatible**: Exports traces in OpenTelemetry Protocol format
|
|
10
|
+
- **Error Tracking**: Automatic exception capture with fingerprinting
|
|
11
|
+
- **W3C Trace Context**: Distributed tracing across microservices
|
|
12
|
+
- **Zero Runtime Dependencies**: Uses only Ruby stdlib
|
|
13
|
+
- **Auto-instrumentation**: Detects and instruments installed gems
|
|
14
|
+
- **Non-blocking**: Async batched sending never blocks requests
|
|
15
|
+
|
|
16
|
+
## Supported Instrumentations
|
|
17
|
+
|
|
18
|
+
| Category | Library | Method |
|
|
19
|
+
|----------|---------|--------|
|
|
20
|
+
| **Rails** | ActionController, ActionView | ActiveSupport::Notifications |
|
|
21
|
+
| **Database** | ActiveRecord | ActiveSupport::Notifications |
|
|
22
|
+
| **Background Jobs** | ActiveJob, SolidQueue | ActiveSupport::Notifications |
|
|
23
|
+
| **Background Jobs** | Sidekiq | Server Middleware |
|
|
24
|
+
| **Cache** | Rails Cache | ActiveSupport::Notifications |
|
|
25
|
+
| **HTTP Clients** | Net::HTTP | Monkey-patch |
|
|
26
|
+
| **HTTP Clients** | HTTParty | Monkey-patch |
|
|
27
|
+
| **HTTP Clients** | Faraday | Auto-injected Middleware |
|
|
28
|
+
| **Search** | Elasticsearch | Monkey-patch |
|
|
29
|
+
| **Search** | OpenSearch | Monkey-patch |
|
|
30
|
+
| **Search** | Searchkick | ActiveSupport::Notifications |
|
|
31
|
+
| **Redis** | redis-client | Middleware |
|
|
32
|
+
| **Redis** | redis (legacy) | Monkey-patch |
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Add to your Gemfile:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
gem 'miniapm'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then run:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bundle install
|
|
46
|
+
rails generate miniapm:install
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
Set environment variables:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export MINI_APM_URL="http://your-miniapm-server:3000"
|
|
55
|
+
export MINI_APM_API_KEY="your_project_api_key"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or configure in `config/initializers/miniapm.rb`:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
MiniAPM.configure do |config|
|
|
62
|
+
# Required
|
|
63
|
+
config.endpoint = ENV["MINI_APM_URL"]
|
|
64
|
+
config.api_key = ENV["MINI_APM_API_KEY"]
|
|
65
|
+
|
|
66
|
+
# Service identification
|
|
67
|
+
config.service_name = "my-rails-app"
|
|
68
|
+
config.environment = Rails.env
|
|
69
|
+
|
|
70
|
+
# Sampling (0.0 to 1.0)
|
|
71
|
+
config.sample_rate = 1.0
|
|
72
|
+
|
|
73
|
+
# Batching
|
|
74
|
+
config.batch_size = 100
|
|
75
|
+
config.flush_interval = 5.0
|
|
76
|
+
|
|
77
|
+
# Configure instrumentations
|
|
78
|
+
config.instrument :activerecord, log_sql: true
|
|
79
|
+
config.instrument :redis, enabled: false
|
|
80
|
+
|
|
81
|
+
# Error filtering
|
|
82
|
+
config.ignored_exceptions = ["ActionController::RoutingError"]
|
|
83
|
+
config.filter_parameters = [:password, :token]
|
|
84
|
+
|
|
85
|
+
# Disable in test
|
|
86
|
+
config.enabled = !Rails.env.test?
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Manual Instrumentation
|
|
91
|
+
|
|
92
|
+
Create custom spans:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
MiniAPM.span("process_order", category: :internal) do |span|
|
|
96
|
+
span.add_attribute("order.id", order.id)
|
|
97
|
+
span.add_attribute("order.total", order.total)
|
|
98
|
+
|
|
99
|
+
process_order(order)
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Report errors manually:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
begin
|
|
107
|
+
risky_operation
|
|
108
|
+
rescue => e
|
|
109
|
+
MiniAPM.record_error(e, context: {
|
|
110
|
+
user_id: current_user.id,
|
|
111
|
+
params: { order_id: params[:id] }
|
|
112
|
+
})
|
|
113
|
+
raise
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Distributed Tracing
|
|
118
|
+
|
|
119
|
+
MiniAPM automatically propagates trace context using W3C Trace Context headers. When making HTTP requests with instrumented clients (Net::HTTP, HTTParty, Faraday), the `traceparent` header is automatically injected.
|
|
120
|
+
|
|
121
|
+
For incoming requests, MiniAPM extracts the trace context from the `traceparent` header to continue the trace.
|
|
122
|
+
|
|
123
|
+
## Testing
|
|
124
|
+
|
|
125
|
+
Disable MiniAPM in tests:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# config/initializers/miniapm.rb
|
|
129
|
+
config.enabled = !Rails.env.test?
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or use the test helpers:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
require 'miniapm/testing'
|
|
136
|
+
|
|
137
|
+
RSpec.describe "MyFeature", :miniapm do
|
|
138
|
+
it "tracks spans" do
|
|
139
|
+
perform_action
|
|
140
|
+
|
|
141
|
+
expect(MiniAPM::Testing.recorded_spans).to include(
|
|
142
|
+
having_attributes(name: /process_action/)
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Configuration Options
|
|
149
|
+
|
|
150
|
+
| Option | Default | Description |
|
|
151
|
+
|--------|---------|-------------|
|
|
152
|
+
| `endpoint` | `http://localhost:3000` | MiniAPM server URL |
|
|
153
|
+
| `api_key` | `nil` | API key for authentication |
|
|
154
|
+
| `service_name` | `rails-app` | Service identifier |
|
|
155
|
+
| `environment` | `Rails.env` | Deployment environment |
|
|
156
|
+
| `sample_rate` | `1.0` | Sampling rate (0.0 to 1.0) |
|
|
157
|
+
| `batch_size` | `100` | Max spans per batch |
|
|
158
|
+
| `flush_interval` | `5.0` | Seconds between flushes |
|
|
159
|
+
| `max_queue_size` | `10000` | Max queued items |
|
|
160
|
+
| `enabled` | `true` | Enable/disable tracing |
|
|
161
|
+
| `auto_start` | `true` | Start on Rails boot |
|
|
162
|
+
|
|
163
|
+
## Requirements
|
|
164
|
+
|
|
165
|
+
- Ruby 3.0+
|
|
166
|
+
- Rails 7.0+ (optional, for auto-setup)
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
[Documentation](https://miniapm.com/docs) | [Source Code](https://github.com/miniapm/miniapm-ruby)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Miniapm
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Creates a MiniAPM initializer file at config/initializers/miniapm.rb"
|
|
11
|
+
|
|
12
|
+
def create_initializer_file
|
|
13
|
+
template "initializer.rb", "config/initializers/miniapm.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def show_readme
|
|
17
|
+
readme "README" if behavior == :invoke
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def readme(path)
|
|
23
|
+
say File.read(File.expand_path(path, self.class.source_root))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
===============================================================================
|
|
3
|
+
|
|
4
|
+
MiniAPM has been installed!
|
|
5
|
+
|
|
6
|
+
Next steps:
|
|
7
|
+
|
|
8
|
+
1. Set your API key in your environment:
|
|
9
|
+
|
|
10
|
+
export MINI_APM_API_KEY="your_project_api_key"
|
|
11
|
+
export MINI_APM_URL="http://your-miniapm-server:3000"
|
|
12
|
+
|
|
13
|
+
2. Review the generated config/initializers/miniapm.rb file
|
|
14
|
+
|
|
15
|
+
3. Start your Rails server and MiniAPM will begin collecting traces
|
|
16
|
+
|
|
17
|
+
For more information, see: https://github.com/hasik/miniapm
|
|
18
|
+
|
|
19
|
+
===============================================================================
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# MiniAPM Configuration
|
|
4
|
+
# Documentation: https://miniapm.com/docs
|
|
5
|
+
MiniAPM.configure do |config|
|
|
6
|
+
# Required: MiniAPM server endpoint
|
|
7
|
+
config.endpoint = ENV.fetch("MINI_APM_URL", "http://localhost:3000")
|
|
8
|
+
|
|
9
|
+
# Required: API key for authentication
|
|
10
|
+
config.api_key = ENV["MINI_APM_API_KEY"]
|
|
11
|
+
|
|
12
|
+
# Service identification
|
|
13
|
+
config.service_name = ENV.fetch("MINI_APM_SERVICE_NAME", "<%= Rails.application.class.module_parent_name.underscore.dasherize %>")
|
|
14
|
+
config.environment = Rails.env
|
|
15
|
+
|
|
16
|
+
# Optional: Service version for tracking deployments
|
|
17
|
+
# config.service_version = ENV["APP_VERSION"]
|
|
18
|
+
|
|
19
|
+
# Optional: Git SHA for deployment tracking
|
|
20
|
+
# config.git_sha = ENV["GIT_SHA"] || ENV["HEROKU_SLUG_COMMIT"]
|
|
21
|
+
|
|
22
|
+
# Batching configuration (defaults are usually fine)
|
|
23
|
+
# config.batch_size = 100 # Max spans per batch
|
|
24
|
+
# config.flush_interval = 5.0 # Seconds between flushes
|
|
25
|
+
# config.max_queue_size = 10_000 # Max queued items before dropping
|
|
26
|
+
|
|
27
|
+
# Sampling (1.0 = 100%, 0.1 = 10%)
|
|
28
|
+
# Useful for high-traffic applications
|
|
29
|
+
config.sample_rate = Rails.env.production? ? 1.0 : 1.0
|
|
30
|
+
|
|
31
|
+
# Enable/disable specific instrumentations
|
|
32
|
+
# config.instrument :activerecord, log_sql: true # Include SQL in spans
|
|
33
|
+
# config.instrument :redis, enabled: false # Disable Redis tracing
|
|
34
|
+
|
|
35
|
+
# Error tracking configuration
|
|
36
|
+
# Exceptions to ignore (won't be reported)
|
|
37
|
+
config.ignored_exceptions = [
|
|
38
|
+
"ActionController::RoutingError",
|
|
39
|
+
"ActionController::InvalidAuthenticityToken",
|
|
40
|
+
"ActionController::UnknownFormat",
|
|
41
|
+
"ActiveRecord::RecordNotFound"
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Parameters to filter from error reports (merged with Rails defaults)
|
|
45
|
+
config.filter_parameters = Rails.application.config.filter_parameters
|
|
46
|
+
|
|
47
|
+
# Custom span modification (return false to drop span)
|
|
48
|
+
# config.before_send = ->(span) {
|
|
49
|
+
# # Add custom attributes
|
|
50
|
+
# span.add_attribute("custom.attribute", "value")
|
|
51
|
+
#
|
|
52
|
+
# # Return false to drop this span
|
|
53
|
+
# # return false if span.name.include?("health_check")
|
|
54
|
+
#
|
|
55
|
+
# true
|
|
56
|
+
# }
|
|
57
|
+
|
|
58
|
+
# Disable in test environment
|
|
59
|
+
config.enabled = !Rails.env.test?
|
|
60
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
module MiniAPM
|
|
6
|
+
class Configuration
|
|
7
|
+
# Core settings
|
|
8
|
+
attr_accessor :endpoint # MiniAPM server URL
|
|
9
|
+
attr_accessor :api_key # Bearer token for authentication
|
|
10
|
+
attr_accessor :enabled # Enable/disable the gem
|
|
11
|
+
attr_accessor :auto_start # Auto-start on Rails boot
|
|
12
|
+
|
|
13
|
+
# Service identification
|
|
14
|
+
attr_accessor :service_name # e.g., "my-rails-app"
|
|
15
|
+
attr_accessor :service_version # e.g., "1.2.3"
|
|
16
|
+
attr_accessor :environment # e.g., "production"
|
|
17
|
+
|
|
18
|
+
# Metadata
|
|
19
|
+
attr_accessor :host # Hostname
|
|
20
|
+
attr_accessor :rails_version # Auto-detected
|
|
21
|
+
attr_accessor :ruby_version # Auto-detected
|
|
22
|
+
attr_accessor :git_sha # Git SHA for deploys
|
|
23
|
+
|
|
24
|
+
# Batching settings
|
|
25
|
+
attr_accessor :batch_size # Max spans per batch
|
|
26
|
+
attr_accessor :flush_interval # Seconds between flushes
|
|
27
|
+
attr_accessor :max_queue_size # Max queued items before dropping
|
|
28
|
+
|
|
29
|
+
# Instrumentation toggles
|
|
30
|
+
attr_reader :instrumentations
|
|
31
|
+
|
|
32
|
+
# Sampling
|
|
33
|
+
attr_accessor :sample_rate # 0.0 to 1.0
|
|
34
|
+
|
|
35
|
+
# Error filtering
|
|
36
|
+
attr_accessor :ignored_exceptions
|
|
37
|
+
|
|
38
|
+
# Parameter filtering
|
|
39
|
+
attr_accessor :filter_parameters
|
|
40
|
+
|
|
41
|
+
# Callbacks
|
|
42
|
+
attr_accessor :before_send # Proc to modify/filter spans
|
|
43
|
+
|
|
44
|
+
def initialize
|
|
45
|
+
@endpoint = ENV.fetch("MINI_APM_URL", "http://localhost:3000")
|
|
46
|
+
@api_key = ENV["MINI_APM_API_KEY"]
|
|
47
|
+
@enabled = true
|
|
48
|
+
@auto_start = true
|
|
49
|
+
|
|
50
|
+
@service_name = ENV.fetch("MINI_APM_SERVICE_NAME", "rails-app")
|
|
51
|
+
@service_version = ENV["MINI_APM_SERVICE_VERSION"]
|
|
52
|
+
@environment = ENV.fetch("RAILS_ENV", ENV.fetch("RACK_ENV", "development"))
|
|
53
|
+
|
|
54
|
+
@host = Socket.gethostname rescue "unknown"
|
|
55
|
+
@rails_version = defined?(Rails::VERSION::STRING) ? Rails::VERSION::STRING : nil
|
|
56
|
+
@ruby_version = RUBY_VERSION
|
|
57
|
+
@git_sha = ENV["GIT_SHA"] || ENV["HEROKU_SLUG_COMMIT"] || detect_git_sha
|
|
58
|
+
|
|
59
|
+
@batch_size = 100
|
|
60
|
+
@flush_interval = 5.0
|
|
61
|
+
@max_queue_size = 10_000
|
|
62
|
+
|
|
63
|
+
@instrumentations = InstrumentationConfig.new
|
|
64
|
+
|
|
65
|
+
@sample_rate = 1.0
|
|
66
|
+
|
|
67
|
+
@ignored_exceptions = [
|
|
68
|
+
"ActionController::RoutingError",
|
|
69
|
+
"ActionController::InvalidAuthenticityToken",
|
|
70
|
+
"ActionController::UnknownFormat",
|
|
71
|
+
"ActiveRecord::RecordNotFound"
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
@filter_parameters = [:password, :password_confirmation, :token, :secret, :api_key, :access_token]
|
|
75
|
+
|
|
76
|
+
@before_send = nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def instrument(name, enabled: true, **options)
|
|
80
|
+
@instrumentations.configure(name, enabled: enabled, **options)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Validate configuration and raise on errors
|
|
84
|
+
def validate!
|
|
85
|
+
errors = []
|
|
86
|
+
|
|
87
|
+
# Validate endpoint
|
|
88
|
+
if @endpoint.nil? || @endpoint.empty?
|
|
89
|
+
errors << "endpoint is required"
|
|
90
|
+
else
|
|
91
|
+
begin
|
|
92
|
+
uri = URI.parse(@endpoint)
|
|
93
|
+
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
94
|
+
errors << "endpoint must be an HTTP(S) URL"
|
|
95
|
+
end
|
|
96
|
+
rescue URI::InvalidURIError
|
|
97
|
+
errors << "endpoint is not a valid URL"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Validate sample_rate
|
|
102
|
+
unless @sample_rate.is_a?(Numeric) && @sample_rate >= 0.0 && @sample_rate <= 1.0
|
|
103
|
+
errors << "sample_rate must be a number between 0.0 and 1.0"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Validate batch settings
|
|
107
|
+
errors << "batch_size must be a positive integer" unless @batch_size.is_a?(Integer) && @batch_size > 0
|
|
108
|
+
errors << "flush_interval must be a positive number" unless @flush_interval.is_a?(Numeric) && @flush_interval > 0
|
|
109
|
+
errors << "max_queue_size must be a positive integer" unless @max_queue_size.is_a?(Integer) && @max_queue_size > 0
|
|
110
|
+
|
|
111
|
+
# Warn about missing api_key (not an error, as it might be set later)
|
|
112
|
+
if @api_key.nil? || @api_key.empty?
|
|
113
|
+
MiniAPM.logger.warn { "MiniAPM: api_key is not configured - requests will fail" }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
raise ConfigurationError, "Invalid configuration: #{errors.join(', ')}" if errors.any?
|
|
117
|
+
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def valid?
|
|
122
|
+
validate!
|
|
123
|
+
true
|
|
124
|
+
rescue ConfigurationError
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def detect_git_sha
|
|
131
|
+
sha = `git rev-parse HEAD 2>/dev/null`.strip
|
|
132
|
+
sha.empty? ? nil : sha
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class InstrumentationConfig
|
|
139
|
+
DEFAULTS = {
|
|
140
|
+
rails: { enabled: true },
|
|
141
|
+
activerecord: { enabled: true, log_sql: false },
|
|
142
|
+
activejob: { enabled: true },
|
|
143
|
+
sidekiq: { enabled: true },
|
|
144
|
+
cache: { enabled: true },
|
|
145
|
+
net_http: { enabled: true },
|
|
146
|
+
httparty: { enabled: true },
|
|
147
|
+
faraday: { enabled: true },
|
|
148
|
+
opensearch: { enabled: true },
|
|
149
|
+
elasticsearch: { enabled: true },
|
|
150
|
+
searchkick: { enabled: true },
|
|
151
|
+
redis: { enabled: true },
|
|
152
|
+
redis_client: { enabled: true }
|
|
153
|
+
}.freeze
|
|
154
|
+
|
|
155
|
+
def initialize
|
|
156
|
+
@config = DEFAULTS.transform_values(&:dup)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def configure(name, **options)
|
|
160
|
+
@config[name.to_sym] ||= {}
|
|
161
|
+
@config[name.to_sym].merge!(options)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def [](name)
|
|
165
|
+
@config[name.to_sym] || { enabled: false }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def enabled?(name)
|
|
169
|
+
self[name][:enabled]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def options(name)
|
|
173
|
+
self[name]
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MiniAPM
|
|
4
|
+
# Fiber-safe context for trace propagation
|
|
5
|
+
# Uses Fiber.[] storage (Ruby 3.2+) with Thread.current fallback
|
|
6
|
+
module Context
|
|
7
|
+
TRACE_KEY = :miniapm_trace
|
|
8
|
+
SPAN_STACK_KEY = :miniapm_span_stack
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Ruby 3.2+ has Fiber.[] for fiber-local storage
|
|
12
|
+
# Fall back to Thread.current for older Ruby (not fiber-safe)
|
|
13
|
+
if Fiber.respond_to?(:[])
|
|
14
|
+
def current_trace
|
|
15
|
+
Fiber[TRACE_KEY]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def current_trace=(trace)
|
|
19
|
+
Fiber[TRACE_KEY] = trace
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def span_stack
|
|
23
|
+
Fiber[SPAN_STACK_KEY] ||= []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def span_stack=(stack)
|
|
27
|
+
Fiber[SPAN_STACK_KEY] = stack
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
# Fallback for Ruby < 3.2 (not fiber-safe, but thread-safe)
|
|
31
|
+
def current_trace
|
|
32
|
+
Thread.current[TRACE_KEY]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def current_trace=(trace)
|
|
36
|
+
Thread.current[TRACE_KEY] = trace
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def span_stack
|
|
40
|
+
Thread.current[SPAN_STACK_KEY] ||= []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def span_stack=(stack)
|
|
44
|
+
Thread.current[SPAN_STACK_KEY] = stack
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def current_trace_id
|
|
49
|
+
current_trace&.trace_id
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def current_span
|
|
53
|
+
span_stack.last
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def push_span(span)
|
|
57
|
+
span_stack.push(span)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def pop_span
|
|
61
|
+
span_stack.pop
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def with_span(span)
|
|
65
|
+
push_span(span)
|
|
66
|
+
yield span
|
|
67
|
+
ensure
|
|
68
|
+
pop_span
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def with_trace(trace)
|
|
72
|
+
old_trace = current_trace
|
|
73
|
+
old_stack = span_stack
|
|
74
|
+
|
|
75
|
+
self.current_trace = trace
|
|
76
|
+
self.span_stack = []
|
|
77
|
+
|
|
78
|
+
yield trace
|
|
79
|
+
ensure
|
|
80
|
+
self.current_trace = old_trace
|
|
81
|
+
self.span_stack = old_stack
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def clear!
|
|
85
|
+
self.current_trace = nil
|
|
86
|
+
self.span_stack = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fiber_safe?
|
|
90
|
+
Fiber.respond_to?(:[])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Extract trace context from incoming HTTP headers (W3C Trace Context)
|
|
94
|
+
# Format: 00-{trace_id}-{parent_span_id}-{flags}
|
|
95
|
+
# Example: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
|
|
96
|
+
def extract_from_headers(headers)
|
|
97
|
+
traceparent = headers["traceparent"] ||
|
|
98
|
+
headers["HTTP_TRACEPARENT"] ||
|
|
99
|
+
headers["Traceparent"]
|
|
100
|
+
return nil unless traceparent
|
|
101
|
+
|
|
102
|
+
parts = traceparent.to_s.split("-")
|
|
103
|
+
return nil unless parts.length == 4
|
|
104
|
+
return nil unless parts[0] == "00" # version check
|
|
105
|
+
|
|
106
|
+
trace_id = parts[1]
|
|
107
|
+
parent_span_id = parts[2]
|
|
108
|
+
flags = parts[3].to_i(16)
|
|
109
|
+
|
|
110
|
+
# Validate format
|
|
111
|
+
return nil unless trace_id.match?(/\A[0-9a-f]{32}\z/)
|
|
112
|
+
return nil unless parent_span_id.match?(/\A[0-9a-f]{16}\z/)
|
|
113
|
+
|
|
114
|
+
{
|
|
115
|
+
trace_id: trace_id,
|
|
116
|
+
parent_span_id: parent_span_id,
|
|
117
|
+
sampled: (flags & 0x01) == 1
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Inject trace context into outgoing HTTP headers (W3C Trace Context)
|
|
122
|
+
def inject_into_headers(headers)
|
|
123
|
+
return headers unless current_span
|
|
124
|
+
|
|
125
|
+
flags = current_trace&.sampled? ? "01" : "00"
|
|
126
|
+
traceparent = format(
|
|
127
|
+
"00-%s-%s-%s",
|
|
128
|
+
current_trace_id,
|
|
129
|
+
current_span.span_id,
|
|
130
|
+
flags
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
headers["traceparent"] = traceparent
|
|
134
|
+
headers
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|