lapsoss 0.2.0 → 0.3.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -733
  3. data/lib/lapsoss/adapters/appsignal_adapter.rb +22 -22
  4. data/lib/lapsoss/adapters/base.rb +0 -3
  5. data/lib/lapsoss/adapters/insight_hub_adapter.rb +108 -104
  6. data/lib/lapsoss/adapters/logger_adapter.rb +1 -1
  7. data/lib/lapsoss/adapters/rollbar_adapter.rb +108 -68
  8. data/lib/lapsoss/adapters/sentry_adapter.rb +24 -24
  9. data/lib/lapsoss/backtrace_frame.rb +37 -206
  10. data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
  11. data/lib/lapsoss/backtrace_processor.rb +27 -23
  12. data/lib/lapsoss/client.rb +2 -4
  13. data/lib/lapsoss/configuration.rb +28 -32
  14. data/lib/lapsoss/current.rb +10 -2
  15. data/lib/lapsoss/event.rb +28 -5
  16. data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
  17. data/lib/lapsoss/exclusion_configuration.rb +30 -0
  18. data/lib/lapsoss/exclusion_filter.rb +0 -273
  19. data/lib/lapsoss/exclusion_presets.rb +249 -0
  20. data/lib/lapsoss/fingerprinter.rb +28 -28
  21. data/lib/lapsoss/http_client.rb +8 -8
  22. data/lib/lapsoss/merged_scope.rb +63 -0
  23. data/lib/lapsoss/middleware/base.rb +15 -0
  24. data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
  25. data/lib/lapsoss/middleware/event_enricher.rb +19 -0
  26. data/lib/lapsoss/middleware/event_transformer.rb +19 -0
  27. data/lib/lapsoss/middleware/exception_filter.rb +43 -0
  28. data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
  29. data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
  30. data/lib/lapsoss/middleware/release_tracker.rb +117 -0
  31. data/lib/lapsoss/middleware/sample_filter.rb +23 -0
  32. data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
  33. data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
  34. data/lib/lapsoss/pipeline.rb +0 -68
  35. data/lib/lapsoss/pipeline_builder.rb +69 -0
  36. data/lib/lapsoss/rails_error_subscriber.rb +42 -0
  37. data/lib/lapsoss/rails_middleware.rb +78 -0
  38. data/lib/lapsoss/railtie.rb +22 -50
  39. data/lib/lapsoss/registry.rb +18 -5
  40. data/lib/lapsoss/release_providers.rb +110 -0
  41. data/lib/lapsoss/release_tracker.rb +159 -232
  42. data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
  43. data/lib/lapsoss/sampling/base.rb +11 -0
  44. data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
  45. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
  46. data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
  47. data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
  48. data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
  49. data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
  50. data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
  51. data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
  52. data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
  53. data/lib/lapsoss/scope.rb +12 -48
  54. data/lib/lapsoss/scrubber.rb +7 -7
  55. data/lib/lapsoss/user_context.rb +30 -203
  56. data/lib/lapsoss/user_context_integrations.rb +39 -0
  57. data/lib/lapsoss/user_context_middleware.rb +50 -0
  58. data/lib/lapsoss/user_context_provider.rb +93 -0
  59. data/lib/lapsoss/utils.rb +13 -0
  60. data/lib/lapsoss/validators.rb +15 -15
  61. data/lib/lapsoss/version.rb +1 -1
  62. data/lib/lapsoss.rb +3 -3
  63. metadata +60 -7
  64. data/lib/lapsoss/middleware.rb +0 -345
  65. data/lib/lapsoss/sampling.rb +0 -328
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'digest'
3
+ require "digest"
4
4
 
5
5
  module Lapsoss
6
6
  # Release and version tracking system
@@ -13,6 +13,9 @@ module Lapsoss
13
13
  @cache_duration = configuration[:cache_duration] || 300 # 5 minutes
14
14
  @cached_release_info = nil
15
15
  @cache_timestamp = nil
16
+
17
+ # Auto-register rails_app_version provider if available
18
+ add_rails_app_version_provider if defined?(Rails) && Rails.application.respond_to?(:version)
16
19
  end
17
20
 
18
21
  def get_release_info
@@ -69,44 +72,57 @@ module Lapsoss
69
72
  @cache_timestamp = nil
70
73
  end
71
74
 
75
+ def add_rails_app_version_provider
76
+ add_version_provider do
77
+ if defined?(Rails) && Rails.application.respond_to?(:version)
78
+ version = Rails.application.version
79
+ {
80
+ rails_app_version: version.to_s,
81
+ rails_app_version_cache_key: version.to_cache_key,
82
+ rails_app_environment: Rails.application.env
83
+ }
84
+ end
85
+ end
86
+ end
87
+
72
88
  private
73
89
 
74
90
  def detect_git_info
75
- return nil unless File.exist?('.git')
91
+ return nil unless File.exist?(".git")
76
92
 
77
93
  git_info = {}
78
94
 
79
95
  begin
80
96
  # Get current commit SHA
81
- commit_sha = execute_git_command('rev-parse HEAD')
97
+ commit_sha = execute_git_command("rev-parse HEAD")
82
98
  git_info[:commit_sha] = commit_sha if commit_sha
83
99
 
84
100
  # Get short commit SHA
85
- short_sha = execute_git_command('rev-parse --short HEAD')
101
+ short_sha = execute_git_command("rev-parse --short HEAD")
86
102
  git_info[:short_sha] = short_sha if short_sha
87
103
 
88
104
  # Get branch name
89
- branch = execute_git_command('rev-parse --abbrev-ref HEAD')
90
- git_info[:branch] = branch if branch && branch != 'HEAD'
105
+ branch = execute_git_command("rev-parse --abbrev-ref HEAD")
106
+ git_info[:branch] = branch if branch && branch != "HEAD"
91
107
 
92
108
  # Get commit timestamp
93
- commit_timestamp = execute_git_command('log -1 --format=%ct')
109
+ commit_timestamp = execute_git_command("log -1 --format=%ct")
94
110
  git_info[:commit_timestamp] = Time.zone.at(commit_timestamp.to_i) if commit_timestamp.present?
95
111
 
96
112
  # Get commit message
97
- commit_message = execute_git_command('log -1 --format=%s')
113
+ commit_message = execute_git_command("log -1 --format=%s")
98
114
  git_info[:commit_message] = commit_message if commit_message
99
115
 
100
116
  # Get committer info
101
- committer = execute_git_command('log -1 --format=%cn')
117
+ committer = execute_git_command("log -1 --format=%cn")
102
118
  git_info[:committer] = committer if committer
103
119
 
104
120
  # Get tag if on a tag
105
- tag = execute_git_command('describe --exact-match --tags HEAD 2>/dev/null')
121
+ tag = execute_git_command("describe --exact-match --tags HEAD 2>/dev/null")
106
122
  git_info[:tag] = tag if tag.present?
107
123
 
108
124
  # Get latest tag
109
- latest_tag = execute_git_command('describe --tags --abbrev=0 2>/dev/null')
125
+ latest_tag = execute_git_command("describe --tags --abbrev=0 2>/dev/null")
110
126
  git_info[:latest_tag] = latest_tag if latest_tag.present?
111
127
 
112
128
  # Get commits since latest tag
@@ -116,11 +132,11 @@ module Lapsoss
116
132
  end
117
133
 
118
134
  # Check if working directory is dirty
119
- git_status = execute_git_command('status --porcelain')
135
+ git_status = execute_git_command("status --porcelain")
120
136
  git_info[:dirty] = !git_status.empty? if git_status
121
137
 
122
138
  # Get remote URL
123
- remote_url = execute_git_command('config --get remote.origin.url')
139
+ remote_url = execute_git_command("config --get remote.origin.url")
124
140
  git_info[:remote_url] = sanitize_remote_url(remote_url) if remote_url
125
141
 
126
142
  git_info
@@ -133,20 +149,33 @@ module Lapsoss
133
149
  def detect_environment_info
134
150
  env_info = {}
135
151
 
136
- # Application version from common environment variables
137
- env_info[:app_version] = ENV['APP_VERSION'] if ENV['APP_VERSION']
138
- env_info[:version] = ENV['VERSION'] if ENV['VERSION']
152
+ # Try rails_app_version first if available
153
+ if defined?(Rails) && Rails.application.respond_to?(:version)
154
+ env_info[:version] = Rails.application.version.to_s
155
+ env_info[:app_version] = Rails.application.version.to_s
156
+
157
+ # Add detailed version info
158
+ version_obj = Rails.application.version
159
+ env_info[:version_major] = version_obj.major
160
+ env_info[:version_minor] = version_obj.minor
161
+ env_info[:version_patch] = version_obj.patch
162
+ env_info[:version_prerelease] = version_obj.prerelease? if version_obj.respond_to?(:prerelease?)
163
+ else
164
+ # Fallback to environment variables
165
+ env_info[:app_version] = ENV["APP_VERSION"] if ENV["APP_VERSION"]
166
+ env_info[:version] = ENV["VERSION"] if ENV["VERSION"]
167
+ end
139
168
 
140
169
  # Environment detection
141
170
  env_info[:environment] = detect_environment
142
171
 
143
172
  # Application name
144
- env_info[:app_name] = ENV['APP_NAME'] if ENV['APP_NAME']
173
+ env_info[:app_name] = ENV["APP_NAME"] if ENV["APP_NAME"]
145
174
 
146
175
  # Build information
147
- env_info[:build_number] = ENV['BUILD_NUMBER'] if ENV['BUILD_NUMBER']
148
- env_info[:build_id] = ENV['BUILD_ID'] if ENV['BUILD_ID']
149
- env_info[:build_url] = ENV['BUILD_URL'] if ENV['BUILD_URL']
176
+ env_info[:build_number] = ENV["BUILD_NUMBER"] if ENV["BUILD_NUMBER"]
177
+ env_info[:build_id] = ENV["BUILD_ID"] if ENV["BUILD_ID"]
178
+ env_info[:build_url] = ENV["BUILD_URL"] if ENV["BUILD_URL"]
150
179
 
151
180
  # CI/CD information
152
181
  env_info[:ci] = detect_ci_info
@@ -158,14 +187,14 @@ module Lapsoss
158
187
  deployment_info = {}
159
188
 
160
189
  # Deployment timestamp
161
- if ENV['DEPLOYMENT_TIME']
162
- deployment_info[:deployment_time] = parse_time(ENV['DEPLOYMENT_TIME'])
163
- elsif ENV['DEPLOYED_AT']
164
- deployment_info[:deployment_time] = parse_time(ENV['DEPLOYED_AT'])
190
+ if ENV["DEPLOYMENT_TIME"]
191
+ deployment_info[:deployment_time] = parse_time(ENV["DEPLOYMENT_TIME"])
192
+ elsif ENV["DEPLOYED_AT"]
193
+ deployment_info[:deployment_time] = parse_time(ENV["DEPLOYED_AT"])
165
194
  end
166
195
 
167
196
  # Deployment ID
168
- deployment_info[:deployment_id] = ENV['DEPLOYMENT_ID'] if ENV['DEPLOYMENT_ID']
197
+ deployment_info[:deployment_id] = ENV["DEPLOYMENT_ID"] if ENV["DEPLOYMENT_ID"]
169
198
 
170
199
  # Platform-specific detection
171
200
  deployment_info.merge!(detect_heroku_info)
@@ -179,111 +208,114 @@ module Lapsoss
179
208
  end
180
209
 
181
210
  def detect_environment
182
- return ENV['RAILS_ENV'] if ENV['RAILS_ENV']
183
- return ENV['RACK_ENV'] if ENV['RACK_ENV']
184
- return ENV['NODE_ENV'] if ENV['NODE_ENV']
185
- return ENV['ENVIRONMENT'] if ENV['ENVIRONMENT']
186
- return ENV['ENV'] if ENV['ENV']
211
+ # Try rails_app_version first if available
212
+ return Rails.application.env if defined?(Rails) && Rails.application.respond_to?(:env)
213
+
214
+ return ENV["RAILS_ENV"] if ENV["RAILS_ENV"]
215
+ return ENV["RACK_ENV"] if ENV["RACK_ENV"]
216
+ return ENV["NODE_ENV"] if ENV["NODE_ENV"]
217
+ return ENV["ENVIRONMENT"] if ENV["ENVIRONMENT"]
218
+ return ENV["ENV"] if ENV["ENV"]
187
219
 
188
220
  # Try to detect from Rails if available
189
221
  return Rails.env.to_s if defined?(Rails) && Rails.respond_to?(:env)
190
222
 
191
223
  # Default fallback
192
- 'unknown'
224
+ "unknown"
193
225
  end
194
226
 
195
227
  def detect_ci_info
196
228
  ci_info = {}
197
229
 
198
230
  # GitHub Actions
199
- if ENV['GITHUB_ACTIONS']
200
- ci_info[:provider] = 'github_actions'
201
- ci_info[:run_id] = ENV.fetch('GITHUB_RUN_ID', nil)
202
- ci_info[:run_number] = ENV.fetch('GITHUB_RUN_NUMBER', nil)
203
- ci_info[:workflow] = ENV.fetch('GITHUB_WORKFLOW', nil)
204
- ci_info[:actor] = ENV.fetch('GITHUB_ACTOR', nil)
205
- ci_info[:repository] = ENV.fetch('GITHUB_REPOSITORY', nil)
206
- ci_info[:ref] = ENV.fetch('GITHUB_REF', nil)
207
- ci_info[:sha] = ENV.fetch('GITHUB_SHA', nil)
231
+ if ENV["GITHUB_ACTIONS"]
232
+ ci_info[:provider] = "github_actions"
233
+ ci_info[:run_id] = ENV.fetch("GITHUB_RUN_ID", nil)
234
+ ci_info[:run_number] = ENV.fetch("GITHUB_RUN_NUMBER", nil)
235
+ ci_info[:workflow] = ENV.fetch("GITHUB_WORKFLOW", nil)
236
+ ci_info[:actor] = ENV.fetch("GITHUB_ACTOR", nil)
237
+ ci_info[:repository] = ENV.fetch("GITHUB_REPOSITORY", nil)
238
+ ci_info[:ref] = ENV.fetch("GITHUB_REF", nil)
239
+ ci_info[:sha] = ENV.fetch("GITHUB_SHA", nil)
208
240
  end
209
241
 
210
242
  # GitLab CI
211
- if ENV['GITLAB_CI']
212
- ci_info[:provider] = 'gitlab_ci'
213
- ci_info[:pipeline_id] = ENV.fetch('CI_PIPELINE_ID', nil)
214
- ci_info[:job_id] = ENV.fetch('CI_JOB_ID', nil)
215
- ci_info[:job_name] = ENV.fetch('CI_JOB_NAME', nil)
216
- ci_info[:commit_sha] = ENV.fetch('CI_COMMIT_SHA', nil)
217
- ci_info[:commit_ref] = ENV.fetch('CI_COMMIT_REF_NAME', nil)
218
- ci_info[:project_url] = ENV.fetch('CI_PROJECT_URL', nil)
243
+ if ENV["GITLAB_CI"]
244
+ ci_info[:provider] = "gitlab_ci"
245
+ ci_info[:pipeline_id] = ENV.fetch("CI_PIPELINE_ID", nil)
246
+ ci_info[:job_id] = ENV.fetch("CI_JOB_ID", nil)
247
+ ci_info[:job_name] = ENV.fetch("CI_JOB_NAME", nil)
248
+ ci_info[:commit_sha] = ENV.fetch("CI_COMMIT_SHA", nil)
249
+ ci_info[:commit_ref] = ENV.fetch("CI_COMMIT_REF_NAME", nil)
250
+ ci_info[:project_url] = ENV.fetch("CI_PROJECT_URL", nil)
219
251
  end
220
252
 
221
253
  # Jenkins
222
- if ENV['JENKINS_URL']
223
- ci_info[:provider] = 'jenkins'
224
- ci_info[:build_number] = ENV.fetch('BUILD_NUMBER', nil)
225
- ci_info[:build_id] = ENV.fetch('BUILD_ID', nil)
226
- ci_info[:job_name] = ENV.fetch('JOB_NAME', nil)
227
- ci_info[:build_url] = ENV.fetch('BUILD_URL', nil)
228
- ci_info[:git_commit] = ENV.fetch('GIT_COMMIT', nil)
229
- ci_info[:git_branch] = ENV.fetch('GIT_BRANCH', nil)
254
+ if ENV["JENKINS_URL"]
255
+ ci_info[:provider] = "jenkins"
256
+ ci_info[:build_number] = ENV.fetch("BUILD_NUMBER", nil)
257
+ ci_info[:build_id] = ENV.fetch("BUILD_ID", nil)
258
+ ci_info[:job_name] = ENV.fetch("JOB_NAME", nil)
259
+ ci_info[:build_url] = ENV.fetch("BUILD_URL", nil)
260
+ ci_info[:git_commit] = ENV.fetch("GIT_COMMIT", nil)
261
+ ci_info[:git_branch] = ENV.fetch("GIT_BRANCH", nil)
230
262
  end
231
263
 
232
264
  # CircleCI
233
- if ENV['CIRCLECI']
234
- ci_info[:provider] = 'circleci'
235
- ci_info[:build_num] = ENV.fetch('CIRCLE_BUILD_NUM', nil)
236
- ci_info[:workflow_id] = ENV.fetch('CIRCLE_WORKFLOW_ID', nil)
237
- ci_info[:job] = ENV.fetch('CIRCLE_JOB', nil)
238
- ci_info[:project_reponame] = ENV.fetch('CIRCLE_PROJECT_REPONAME', nil)
239
- ci_info[:sha1] = ENV.fetch('CIRCLE_SHA1', nil)
240
- ci_info[:branch] = ENV.fetch('CIRCLE_BRANCH', nil)
265
+ if ENV["CIRCLECI"]
266
+ ci_info[:provider] = "circleci"
267
+ ci_info[:build_num] = ENV.fetch("CIRCLE_BUILD_NUM", nil)
268
+ ci_info[:workflow_id] = ENV.fetch("CIRCLE_WORKFLOW_ID", nil)
269
+ ci_info[:job] = ENV.fetch("CIRCLE_JOB", nil)
270
+ ci_info[:project_reponame] = ENV.fetch("CIRCLE_PROJECT_REPONAME", nil)
271
+ ci_info[:sha1] = ENV.fetch("CIRCLE_SHA1", nil)
272
+ ci_info[:branch] = ENV.fetch("CIRCLE_BRANCH", nil)
241
273
  end
242
274
 
243
275
  # Travis CI
244
- if ENV['TRAVIS']
245
- ci_info[:provider] = 'travis'
246
- ci_info[:build_id] = ENV.fetch('TRAVIS_BUILD_ID', nil)
247
- ci_info[:build_number] = ENV.fetch('TRAVIS_BUILD_NUMBER', nil)
248
- ci_info[:job_id] = ENV.fetch('TRAVIS_JOB_ID', nil)
249
- ci_info[:commit] = ENV.fetch('TRAVIS_COMMIT', nil)
250
- ci_info[:branch] = ENV.fetch('TRAVIS_BRANCH', nil)
251
- ci_info[:tag] = ENV.fetch('TRAVIS_TAG', nil)
276
+ if ENV["TRAVIS"]
277
+ ci_info[:provider] = "travis"
278
+ ci_info[:build_id] = ENV.fetch("TRAVIS_BUILD_ID", nil)
279
+ ci_info[:build_number] = ENV.fetch("TRAVIS_BUILD_NUMBER", nil)
280
+ ci_info[:job_id] = ENV.fetch("TRAVIS_JOB_ID", nil)
281
+ ci_info[:commit] = ENV.fetch("TRAVIS_COMMIT", nil)
282
+ ci_info[:branch] = ENV.fetch("TRAVIS_BRANCH", nil)
283
+ ci_info[:tag] = ENV.fetch("TRAVIS_TAG", nil)
252
284
  end
253
285
 
254
286
  ci_info
255
287
  end
256
288
 
257
289
  def detect_heroku_info
258
- return {} unless ENV['HEROKU_APP_NAME']
290
+ return {} unless ENV["HEROKU_APP_NAME"]
259
291
 
260
292
  {
261
- platform: 'heroku',
262
- app_name: ENV.fetch('HEROKU_APP_NAME', nil),
263
- dyno: ENV.fetch('DYNO', nil),
264
- slug_commit: ENV.fetch('HEROKU_SLUG_COMMIT', nil),
265
- release_version: ENV.fetch('HEROKU_RELEASE_VERSION', nil),
266
- slug_description: ENV.fetch('HEROKU_SLUG_DESCRIPTION', nil)
293
+ platform: "heroku",
294
+ app_name: ENV.fetch("HEROKU_APP_NAME", nil),
295
+ dyno: ENV.fetch("DYNO", nil),
296
+ slug_commit: ENV.fetch("HEROKU_SLUG_COMMIT", nil),
297
+ release_version: ENV.fetch("HEROKU_RELEASE_VERSION", nil),
298
+ slug_description: ENV.fetch("HEROKU_SLUG_DESCRIPTION", nil)
267
299
  }
268
300
  end
269
301
 
270
302
  def detect_aws_info
271
303
  info = {}
272
304
 
273
- if ENV['AWS_EXECUTION_ENV']
274
- info[:platform] = 'aws'
275
- info[:execution_env] = ENV['AWS_EXECUTION_ENV']
276
- info[:region] = ENV['AWS_REGION'] || ENV.fetch('AWS_DEFAULT_REGION', nil)
277
- info[:function_name] = ENV.fetch('AWS_LAMBDA_FUNCTION_NAME', nil)
278
- info[:function_version] = ENV.fetch('AWS_LAMBDA_FUNCTION_VERSION', nil)
305
+ if ENV["AWS_EXECUTION_ENV"]
306
+ info[:platform] = "aws"
307
+ info[:execution_env] = ENV["AWS_EXECUTION_ENV"]
308
+ info[:region] = ENV["AWS_REGION"] || ENV.fetch("AWS_DEFAULT_REGION", nil)
309
+ info[:function_name] = ENV.fetch("AWS_LAMBDA_FUNCTION_NAME", nil)
310
+ info[:function_version] = ENV.fetch("AWS_LAMBDA_FUNCTION_VERSION", nil)
279
311
  end
280
312
 
281
313
  # EC2 metadata (if available)
282
- if ENV['EC2_INSTANCE_ID']
283
- info[:platform] = 'aws_ec2'
284
- info[:instance_id] = ENV['EC2_INSTANCE_ID']
285
- info[:instance_type] = ENV.fetch('EC2_INSTANCE_TYPE', nil)
286
- info[:availability_zone] = ENV.fetch('EC2_AVAILABILITY_ZONE', nil)
314
+ if ENV["EC2_INSTANCE_ID"]
315
+ info[:platform] = "aws_ec2"
316
+ info[:instance_id] = ENV["EC2_INSTANCE_ID"]
317
+ info[:instance_type] = ENV.fetch("EC2_INSTANCE_TYPE", nil)
318
+ info[:availability_zone] = ENV.fetch("EC2_AVAILABILITY_ZONE", nil)
287
319
  end
288
320
 
289
321
  info
@@ -292,21 +324,21 @@ module Lapsoss
292
324
  def detect_gcp_info
293
325
  info = {}
294
326
 
295
- if ENV['GOOGLE_CLOUD_PROJECT']
296
- info[:platform] = 'gcp'
297
- info[:project] = ENV['GOOGLE_CLOUD_PROJECT']
298
- info[:region] = ENV.fetch('GOOGLE_CLOUD_REGION', nil)
299
- info[:function_name] = ENV.fetch('FUNCTION_NAME', nil)
300
- info[:function_signature_type] = ENV.fetch('FUNCTION_SIGNATURE_TYPE', nil)
327
+ if ENV["GOOGLE_CLOUD_PROJECT"]
328
+ info[:platform] = "gcp"
329
+ info[:project] = ENV["GOOGLE_CLOUD_PROJECT"]
330
+ info[:region] = ENV.fetch("GOOGLE_CLOUD_REGION", nil)
331
+ info[:function_name] = ENV.fetch("FUNCTION_NAME", nil)
332
+ info[:function_signature_type] = ENV.fetch("FUNCTION_SIGNATURE_TYPE", nil)
301
333
  end
302
334
 
303
335
  # App Engine
304
- if ENV['GAE_APPLICATION']
305
- info[:platform] = 'gcp_app_engine'
306
- info[:application] = ENV['GAE_APPLICATION']
307
- info[:service] = ENV.fetch('GAE_SERVICE', nil)
308
- info[:version] = ENV.fetch('GAE_VERSION', nil)
309
- info[:runtime] = ENV.fetch('GAE_RUNTIME', nil)
336
+ if ENV["GAE_APPLICATION"]
337
+ info[:platform] = "gcp_app_engine"
338
+ info[:application] = ENV["GAE_APPLICATION"]
339
+ info[:service] = ENV.fetch("GAE_SERVICE", nil)
340
+ info[:version] = ENV.fetch("GAE_VERSION", nil)
341
+ info[:runtime] = ENV.fetch("GAE_RUNTIME", nil)
310
342
  end
311
343
 
312
344
  info
@@ -315,12 +347,12 @@ module Lapsoss
315
347
  def detect_azure_info
316
348
  info = {}
317
349
 
318
- if ENV['WEBSITE_SITE_NAME']
319
- info[:platform] = 'azure'
320
- info[:site_name] = ENV['WEBSITE_SITE_NAME']
321
- info[:resource_group] = ENV.fetch('WEBSITE_RESOURCE_GROUP', nil)
322
- info[:subscription_id] = ENV.fetch('WEBSITE_OWNER_NAME', nil)
323
- info[:sku] = ENV.fetch('WEBSITE_SKU', nil)
350
+ if ENV["WEBSITE_SITE_NAME"]
351
+ info[:platform] = "azure"
352
+ info[:site_name] = ENV["WEBSITE_SITE_NAME"]
353
+ info[:resource_group] = ENV.fetch("WEBSITE_RESOURCE_GROUP", nil)
354
+ info[:subscription_id] = ENV.fetch("WEBSITE_OWNER_NAME", nil)
355
+ info[:sku] = ENV.fetch("WEBSITE_SKU", nil)
324
356
  end
325
357
 
326
358
  info
@@ -329,11 +361,11 @@ module Lapsoss
329
361
  def detect_docker_info
330
362
  info = {}
331
363
 
332
- if ENV['DOCKER_CONTAINER_ID'] || File.exist?('/.dockerenv')
333
- info[:platform] = 'docker'
334
- info[:container_id] = ENV['DOCKER_CONTAINER_ID']
335
- info[:image] = ENV.fetch('DOCKER_IMAGE', nil)
336
- info[:tag] = ENV.fetch('DOCKER_TAG', nil)
364
+ if ENV["DOCKER_CONTAINER_ID"] || File.exist?("/.dockerenv")
365
+ info[:platform] = "docker"
366
+ info[:container_id] = ENV["DOCKER_CONTAINER_ID"]
367
+ info[:image] = ENV.fetch("DOCKER_IMAGE", nil)
368
+ info[:tag] = ENV.fetch("DOCKER_TAG", nil)
337
369
  end
338
370
 
339
371
  info
@@ -342,13 +374,13 @@ module Lapsoss
342
374
  def detect_kubernetes_info
343
375
  info = {}
344
376
 
345
- if ENV['KUBERNETES_SERVICE_HOST']
346
- info[:platform] = 'kubernetes'
347
- info[:namespace] = ENV.fetch('KUBERNETES_NAMESPACE', nil)
348
- info[:pod_name] = ENV.fetch('HOSTNAME', nil)
349
- info[:service_account] = ENV.fetch('KUBERNETES_SERVICE_ACCOUNT', nil)
350
- info[:cluster_name] = ENV.fetch('CLUSTER_NAME', nil)
351
- info[:node_name] = ENV.fetch('NODE_NAME', nil)
377
+ if ENV["KUBERNETES_SERVICE_HOST"]
378
+ info[:platform] = "kubernetes"
379
+ info[:namespace] = ENV.fetch("KUBERNETES_NAMESPACE", nil)
380
+ info[:pod_name] = ENV.fetch("HOSTNAME", nil)
381
+ info[:service_account] = ENV.fetch("KUBERNETES_SERVICE_ACCOUNT", nil)
382
+ info[:cluster_name] = ENV.fetch("CLUSTER_NAME", nil)
383
+ info[:node_name] = ENV.fetch("NODE_NAME", nil)
352
384
  end
353
385
 
354
386
  info
@@ -363,7 +395,7 @@ module Lapsoss
363
395
 
364
396
  def sanitize_remote_url(url)
365
397
  # Remove credentials from Git URLs
366
- url.gsub(%r{://[^@/]+@}, '://')
398
+ url.gsub(%r{://[^@/]+@}, "://")
367
399
  end
368
400
 
369
401
  def parse_time(time_str)
@@ -371,11 +403,11 @@ module Lapsoss
371
403
 
372
404
  # Try different time formats
373
405
  formats = [
374
- '%Y-%m-%dT%H:%M:%S%z', # ISO 8601 with timezone
375
- '%Y-%m-%dT%H:%M:%SZ', # ISO 8601 UTC
376
- '%Y-%m-%d %H:%M:%S %z', # Standard format with timezone
377
- '%Y-%m-%d %H:%M:%S', # Standard format without timezone
378
- '%s' # Unix timestamp
406
+ "%Y-%m-%dT%H:%M:%S%z", # ISO 8601 with timezone
407
+ "%Y-%m-%dT%H:%M:%SZ", # ISO 8601 UTC
408
+ "%Y-%m-%d %H:%M:%S %z", # Standard format with timezone
409
+ "%Y-%m-%d %H:%M:%S", # Standard format without timezone
410
+ "%s" # Unix timestamp
379
411
  ]
380
412
 
381
413
  formats.each do |format|
@@ -413,7 +445,7 @@ module Lapsoss
413
445
 
414
446
  # If we have components, join them
415
447
  if components.any?
416
- release_id = components.join('-')
448
+ release_id = components.join("-")
417
449
  # Truncate if too long
418
450
  release_id.length > 64 ? release_id[0, 64] : release_id
419
451
  else
@@ -423,109 +455,4 @@ module Lapsoss
423
455
  end
424
456
  end
425
457
  end
426
-
427
- # Built-in release providers for common scenarios
428
- class ReleaseProviders
429
- def self.from_file(file_path)
430
- lambda do
431
- return nil unless File.exist?(file_path)
432
-
433
- content = File.read(file_path).strip
434
- return nil if content.empty?
435
-
436
- # Try to parse as JSON first
437
- begin
438
- JSON.parse(content)
439
- rescue JSON::ParserError
440
- # Treat as plain text version
441
- { version: content }
442
- end
443
- end
444
- end
445
-
446
- def self.from_ruby_constant(constant_name)
447
- lambda do
448
- constant = Object.const_get(constant_name)
449
- { version: constant.to_s }
450
- rescue NameError
451
- nil
452
- end
453
- end
454
-
455
- def self.from_gemfile_lock
456
- lambda do
457
- return nil unless File.exist?('Gemfile.lock')
458
-
459
- content = File.read('Gemfile.lock')
460
-
461
- # Extract gems with versions
462
- gems = {}
463
- content.scan(/^\s{4}(\w+)\s+\(([^)]+)\)/).each do |name, version|
464
- gems[name] = version
465
- end
466
-
467
- { gems: gems }
468
- end
469
- end
470
-
471
- def self.from_package_json
472
- lambda do
473
- return nil unless File.exist?('package.json')
474
-
475
- begin
476
- package_info = JSON.parse(File.read('package.json'))
477
- {
478
- version: package_info['version'],
479
- name: package_info['name'],
480
- dependencies: package_info['dependencies']&.keys
481
- }.compact
482
- rescue JSON::ParserError
483
- nil
484
- end
485
- end
486
- end
487
-
488
- def self.from_rails_application
489
- lambda do
490
- return nil unless defined?(Rails) && Rails.respond_to?(:application)
491
-
492
- app = Rails.application
493
- return nil unless app
494
-
495
- info = {
496
- rails_version: Rails.version,
497
- environment: Rails.env,
498
- root: Rails.root.to_s
499
- }
500
-
501
- # Get application version if defined
502
- info[:app_version] = app.class.version if app.class.respond_to?(:version)
503
-
504
- # Get application name
505
- info[:app_name] = app.class.name if app.class.respond_to?(:name)
506
-
507
- info
508
- end
509
- end
510
-
511
- def self.from_capistrano
512
- lambda do
513
- # Check for Capistrano deployment files
514
- %w[REVISION current/REVISION].each do |file|
515
- next unless File.exist?(file)
516
-
517
- revision = File.read(file).strip
518
- next if revision.empty?
519
-
520
- return {
521
- revision: revision,
522
- deployed_at: File.mtime(file),
523
- deployment_method: 'capistrano'
524
- }
525
- end
526
-
527
- nil
528
- end
529
- end
530
- end
531
458
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class AdaptiveSampler < Base
6
+ def initialize(target_rate: 1.0, adjustment_period: 60)
7
+ @target_rate = target_rate
8
+ @adjustment_period = adjustment_period
9
+ @current_rate = target_rate
10
+ @events_count = 0
11
+ @last_adjustment = Time.zone.now
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ def sample?(_event, _hint = {})
16
+ @mutex.synchronize do
17
+ @events_count += 1
18
+
19
+ # Adjust rate periodically
20
+ now = Time.zone.now
21
+ if now - @last_adjustment > @adjustment_period
22
+ adjust_rate
23
+ @last_adjustment = now
24
+ @events_count = 0
25
+ end
26
+ end
27
+
28
+ @current_rate > rand
29
+ end
30
+
31
+ attr_reader :current_rate
32
+
33
+ private
34
+
35
+ def adjust_rate
36
+ # Simple adaptive logic - can be enhanced based on system metrics
37
+ # For now, just ensure we don't drift too far from target
38
+ if @events_count > 100 # High volume
39
+ @current_rate = [ @current_rate * 0.9, @target_rate * 0.1 ].max
40
+ elsif @events_count < 10 # Low volume
41
+ @current_rate = [ @current_rate * 1.1, @target_rate ].min
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class Base
6
+ def sample?(_event, _hint = {})
7
+ true
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class CompositeSampler < Base
6
+ def initialize(app = nil, samplers: [], strategy: :all)
7
+ @app = app
8
+ @samplers = samplers
9
+ @strategy = strategy
10
+ end
11
+
12
+ def sample?(event, hint = {})
13
+ case @strategy
14
+ when :all
15
+ @samplers.all? { |sampler| sampler.sample?(event, hint) }
16
+ when :any
17
+ @samplers.any? { |sampler| sampler.sample?(event, hint) }
18
+ when :first
19
+ @samplers.first&.sample?(event, hint) || true
20
+ else
21
+ raise ArgumentError, "Unknown strategy: #{@strategy}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end