sec_api 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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/Dockerfile +54 -0
  3. data/.devcontainer/README.md +178 -0
  4. data/.devcontainer/devcontainer.json +46 -0
  5. data/.devcontainer/docker-compose.yml +28 -0
  6. data/.devcontainer/post-create.sh +51 -0
  7. data/.devcontainer/post-start.sh +44 -0
  8. data/.rspec +3 -0
  9. data/.standard.yml +3 -0
  10. data/CHANGELOG.md +5 -0
  11. data/CLAUDE.md +0 -0
  12. data/LICENSE.txt +21 -0
  13. data/MIGRATION.md +274 -0
  14. data/README.md +370 -0
  15. data/Rakefile +10 -0
  16. data/config/secapi.yml.example +57 -0
  17. data/docs/development-guide.md +291 -0
  18. data/docs/enumerator_pattern_design.md +483 -0
  19. data/docs/examples/README.md +58 -0
  20. data/docs/examples/backfill_filings.rb +419 -0
  21. data/docs/examples/instrumentation.rb +583 -0
  22. data/docs/examples/query_builder.rb +308 -0
  23. data/docs/examples/streaming_notifications.rb +491 -0
  24. data/docs/index.md +244 -0
  25. data/docs/migration-guide-v1.md +1091 -0
  26. data/docs/pre-review-checklist.md +145 -0
  27. data/docs/project-overview.md +90 -0
  28. data/docs/project-scan-report.json +60 -0
  29. data/docs/source-tree-analysis.md +190 -0
  30. data/lib/sec_api/callback_helper.rb +49 -0
  31. data/lib/sec_api/client.rb +606 -0
  32. data/lib/sec_api/collections/filings.rb +267 -0
  33. data/lib/sec_api/collections/fulltext_results.rb +86 -0
  34. data/lib/sec_api/config.rb +590 -0
  35. data/lib/sec_api/deep_freezable.rb +42 -0
  36. data/lib/sec_api/errors/authentication_error.rb +24 -0
  37. data/lib/sec_api/errors/configuration_error.rb +5 -0
  38. data/lib/sec_api/errors/error.rb +75 -0
  39. data/lib/sec_api/errors/network_error.rb +26 -0
  40. data/lib/sec_api/errors/not_found_error.rb +23 -0
  41. data/lib/sec_api/errors/pagination_error.rb +28 -0
  42. data/lib/sec_api/errors/permanent_error.rb +29 -0
  43. data/lib/sec_api/errors/rate_limit_error.rb +57 -0
  44. data/lib/sec_api/errors/reconnection_error.rb +34 -0
  45. data/lib/sec_api/errors/server_error.rb +25 -0
  46. data/lib/sec_api/errors/transient_error.rb +28 -0
  47. data/lib/sec_api/errors/validation_error.rb +23 -0
  48. data/lib/sec_api/extractor.rb +122 -0
  49. data/lib/sec_api/filing_journey.rb +477 -0
  50. data/lib/sec_api/mapping.rb +125 -0
  51. data/lib/sec_api/metrics_collector.rb +411 -0
  52. data/lib/sec_api/middleware/error_handler.rb +250 -0
  53. data/lib/sec_api/middleware/instrumentation.rb +186 -0
  54. data/lib/sec_api/middleware/rate_limiter.rb +541 -0
  55. data/lib/sec_api/objects/data_file.rb +34 -0
  56. data/lib/sec_api/objects/document_format_file.rb +45 -0
  57. data/lib/sec_api/objects/entity.rb +92 -0
  58. data/lib/sec_api/objects/extracted_data.rb +118 -0
  59. data/lib/sec_api/objects/fact.rb +147 -0
  60. data/lib/sec_api/objects/filing.rb +197 -0
  61. data/lib/sec_api/objects/fulltext_result.rb +66 -0
  62. data/lib/sec_api/objects/period.rb +96 -0
  63. data/lib/sec_api/objects/stream_filing.rb +194 -0
  64. data/lib/sec_api/objects/xbrl_data.rb +356 -0
  65. data/lib/sec_api/query.rb +423 -0
  66. data/lib/sec_api/rate_limit_state.rb +130 -0
  67. data/lib/sec_api/rate_limit_tracker.rb +154 -0
  68. data/lib/sec_api/stream.rb +841 -0
  69. data/lib/sec_api/structured_logger.rb +199 -0
  70. data/lib/sec_api/types.rb +32 -0
  71. data/lib/sec_api/version.rb +42 -0
  72. data/lib/sec_api/xbrl.rb +220 -0
  73. data/lib/sec_api.rb +137 -0
  74. data/sig/sec_api.rbs +4 -0
  75. metadata +217 -0
@@ -0,0 +1,583 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example: Instrumentation and Observability
5
+ #
6
+ # This example demonstrates:
7
+ # - All callback hooks (on_request, on_response, on_retry, on_error, on_rate_limit)
8
+ # - Integration with Rails.logger for structured logging
9
+ # - Integration with StatsD/Datadog for metrics
10
+ # - Bugsnag/Sentry error tracking integration
11
+ # - Correlation ID usage for request tracing
12
+ # - Filing journey tracking
13
+ # - metrics_backend configuration pattern
14
+ #
15
+ # Prerequisites:
16
+ # - gem install sec_api
17
+ # - Set SECAPI_API_KEY environment variable
18
+ #
19
+ # Usage:
20
+ # ruby docs/examples/instrumentation.rb
21
+
22
+ require "sec_api"
23
+ require "logger"
24
+
25
+ # =============================================================================
26
+ # SECTION 1: All Callback Hooks Overview
27
+ # =============================================================================
28
+
29
+ puts "=" * 60
30
+ puts "SECTION 1: Callback Hooks Overview"
31
+ puts "=" * 60
32
+
33
+ # The SecApi client provides these instrumentation callbacks:
34
+ # - on_request: Called BEFORE each REST API request
35
+ # - on_response: Called AFTER each REST API response
36
+ # - on_retry: Called BEFORE each retry attempt (transient errors)
37
+ # - on_error: Called on FINAL failure (after all retries exhausted)
38
+ # - on_rate_limit: Called when 429 rate limit is hit
39
+ # - on_throttle: Called when proactive throttling occurs
40
+ # - on_queue: Called when a request is queued (rate limit exhausted)
41
+ # - on_dequeue: Called when a request exits the queue
42
+ # - on_filing: Called for each filing received via stream
43
+ # - on_reconnect: Called when WebSocket reconnects after disconnect
44
+ # - on_callback_error: Called when a stream callback raises an error
45
+
46
+ puts "\nCallback hooks available:"
47
+ puts " REST API: on_request, on_response, on_retry, on_error"
48
+ puts " Rate Limiting: on_rate_limit, on_throttle, on_queue, on_dequeue"
49
+ puts " Streaming: on_filing, on_reconnect, on_callback_error"
50
+
51
+ # =============================================================================
52
+ # SECTION 2: Basic Callback Configuration
53
+ # =============================================================================
54
+
55
+ puts "\n" + "=" * 60
56
+ puts "SECTION 2: Basic Callback Configuration"
57
+ puts "=" * 60
58
+
59
+ # Create a simple logger for demonstration
60
+ demo_logger = Logger.new($stdout)
61
+ demo_logger.formatter = proc { |sev, time, prog, msg| "#{sev}: #{msg}\n" }
62
+
63
+ # Configure all callbacks
64
+ config = SecApi::Config.new(
65
+ api_key: ENV.fetch("SECAPI_API_KEY"),
66
+
67
+ # Request lifecycle callbacks
68
+ on_request: ->(request_id:, method:, url:, headers:) {
69
+ demo_logger.info("[REQUEST] #{request_id} #{method.upcase} #{url}")
70
+ },
71
+
72
+ on_response: ->(request_id:, status:, duration_ms:, url:, method:) {
73
+ demo_logger.info("[RESPONSE] #{request_id} #{status} in #{duration_ms}ms")
74
+ },
75
+
76
+ on_retry: ->(request_id:, attempt:, max_attempts:, error_class:, error_message:, will_retry_in:) {
77
+ demo_logger.warn("[RETRY] #{request_id} attempt #{attempt}/#{max_attempts} " \
78
+ "due to #{error_class}, retry in #{will_retry_in}s")
79
+ },
80
+
81
+ on_error: ->(request_id:, error:, url:, method:) {
82
+ demo_logger.error("[ERROR] #{request_id} #{error.class}: #{error.message}")
83
+ }
84
+ )
85
+
86
+ client = SecApi::Client.new(config)
87
+
88
+ # Make a request to see the callbacks in action
89
+ puts "\nMaking a test request with callbacks:"
90
+ filings = client.query.ticker("AAPL").limit(1).search
91
+ puts "Received #{filings.count} filings"
92
+
93
+ # =============================================================================
94
+ # SECTION 3: Structured Logging with Rails.logger
95
+ # =============================================================================
96
+
97
+ puts "\n" + "=" * 60
98
+ puts "SECTION 3: Structured Logging"
99
+ puts "=" * 60
100
+
101
+ # Pattern 1: Using default_logging for automatic structured logging
102
+ puts "\nPattern 1: Automatic structured logging"
103
+ puts <<~CODE
104
+ config = SecApi::Config.new(
105
+ api_key: ENV.fetch("SECAPI_API_KEY"),
106
+ logger: Rails.logger,
107
+ log_level: :info,
108
+ default_logging: true # Enable automatic structured logging
109
+ )
110
+
111
+ client = SecApi::Client.new(config)
112
+
113
+ # Logs are automatically generated as JSON:
114
+ # {"event":"secapi.request.start","request_id":"abc-123","method":"GET",...}
115
+ # {"event":"secapi.request.complete","request_id":"abc-123","status":200,...}
116
+ CODE
117
+
118
+ # Pattern 2: Manual structured logging with StructuredLogger
119
+ puts "\nPattern 2: Manual StructuredLogger usage"
120
+ puts <<~CODE
121
+ # Use StructuredLogger directly for custom logging
122
+ SecApi::StructuredLogger.log_request(Rails.logger, :info,
123
+ request_id: "abc-123",
124
+ method: :get,
125
+ url: "https://api.sec-api.io/query"
126
+ )
127
+
128
+ SecApi::StructuredLogger.log_response(Rails.logger, :info,
129
+ request_id: "abc-123",
130
+ status: 200,
131
+ duration_ms: 150,
132
+ url: "https://api.sec-api.io/query",
133
+ method: :get
134
+ )
135
+
136
+ SecApi::StructuredLogger.log_retry(Rails.logger, :warn,
137
+ request_id: "abc-123",
138
+ attempt: 2,
139
+ max_attempts: 5,
140
+ error_class: "SecApi::ServerError",
141
+ error_message: "Internal Server Error",
142
+ will_retry_in: 4.0
143
+ )
144
+
145
+ SecApi::StructuredLogger.log_error(Rails.logger, :error,
146
+ request_id: "abc-123",
147
+ error: SecApi::NetworkError.new("Connection refused"),
148
+ url: "https://api.sec-api.io/query",
149
+ method: :get
150
+ )
151
+ CODE
152
+
153
+ # Demonstrate actual structured logging
154
+ puts "\nActual structured log output:"
155
+ json_logger = Logger.new($stdout)
156
+ json_logger.formatter = proc { |sev, time, prog, msg| "#{msg}\n" }
157
+
158
+ SecApi::StructuredLogger.log_request(json_logger, :info,
159
+ request_id: "demo-request-123",
160
+ method: :post,
161
+ url: "https://api.sec-api.io/query")
162
+
163
+ SecApi::StructuredLogger.log_response(json_logger, :info,
164
+ request_id: "demo-request-123",
165
+ status: 200,
166
+ duration_ms: 142,
167
+ url: "https://api.sec-api.io/query",
168
+ method: :post)
169
+
170
+ # =============================================================================
171
+ # SECTION 4: StatsD/Datadog Metrics Integration
172
+ # =============================================================================
173
+
174
+ puts "\n" + "=" * 60
175
+ puts "SECTION 4: Metrics Integration (StatsD/Datadog)"
176
+ puts "=" * 60
177
+
178
+ # Pattern 1: Using metrics_backend for automatic metrics collection
179
+ puts "\nPattern 1: Automatic metrics with metrics_backend"
180
+ puts <<~CODE
181
+ require 'statsd-ruby' # or 'datadog/statsd'
182
+
183
+ statsd = StatsD.new('localhost', 8125)
184
+
185
+ config = SecApi::Config.new(
186
+ api_key: ENV.fetch("SECAPI_API_KEY"),
187
+ metrics_backend: statsd # Enables automatic metrics collection
188
+ )
189
+
190
+ client = SecApi::Client.new(config)
191
+
192
+ # Metrics are automatically collected:
193
+ # sec_api.requests.total (counter)
194
+ # sec_api.requests.duration_ms (histogram)
195
+ # sec_api.requests.success / sec_api.requests.error (counters)
196
+ # sec_api.retries.total (counter)
197
+ # sec_api.rate_limit.throttle (counter)
198
+ # sec_api.rate_limit.exceeded (counter)
199
+ # sec_api.stream.filings_received (counter)
200
+ # sec_api.stream.latency_ms (histogram)
201
+ CODE
202
+
203
+ # Pattern 2: Manual StatsD integration via callbacks
204
+ puts "\nPattern 2: Manual StatsD via callbacks"
205
+ puts <<~CODE
206
+ require 'statsd-ruby'
207
+ statsd = StatsD.new('localhost', 8125)
208
+
209
+ config = SecApi::Config.new(
210
+ api_key: ENV.fetch("SECAPI_API_KEY"),
211
+
212
+ on_response: ->(request_id:, status:, duration_ms:, url:, method:) {
213
+ # Histogram for request duration
214
+ statsd.histogram("sec_api.request.duration_ms", duration_ms)
215
+
216
+ # Counter for success/error by status
217
+ if status >= 400
218
+ statsd.increment("sec_api.request.error", tags: ["status:\#{status}"])
219
+ else
220
+ statsd.increment("sec_api.request.success")
221
+ end
222
+ },
223
+
224
+ on_retry: ->(request_id:, attempt:, max_attempts:, error_class:, **) {
225
+ statsd.increment("sec_api.retry", tags: [
226
+ "attempt:\#{attempt}",
227
+ "error:\#{error_class}"
228
+ ])
229
+ },
230
+
231
+ on_rate_limit: ->(info) {
232
+ statsd.increment("sec_api.rate_limit.exceeded")
233
+ statsd.gauge("sec_api.rate_limit.retry_after", info[:retry_after] || 0)
234
+ },
235
+
236
+ on_throttle: ->(info) {
237
+ statsd.increment("sec_api.rate_limit.throttle")
238
+ statsd.histogram("sec_api.rate_limit.delay", info[:delay])
239
+ statsd.gauge("sec_api.rate_limit.remaining", info[:remaining])
240
+ }
241
+ )
242
+ CODE
243
+
244
+ # Pattern 3: Datadog with tags
245
+ puts "\nPattern 3: Datadog StatsD with tags"
246
+ puts <<~CODE
247
+ require 'datadog/statsd'
248
+ statsd = Datadog::Statsd.new('localhost', 8125)
249
+
250
+ config = SecApi::Config.new(
251
+ api_key: ENV.fetch("SECAPI_API_KEY"),
252
+
253
+ on_response: ->(request_id:, status:, duration_ms:, url:, method:) {
254
+ tags = [
255
+ "method:\#{method}",
256
+ "status:\#{status}",
257
+ "status_class:\#{status / 100}xx"
258
+ ]
259
+
260
+ statsd.histogram("sec_api.request.duration", duration_ms, tags: tags)
261
+ statsd.increment("sec_api.request.total", tags: tags)
262
+ }
263
+ )
264
+ CODE
265
+
266
+ # =============================================================================
267
+ # SECTION 5: Error Tracking (Bugsnag/Sentry)
268
+ # =============================================================================
269
+
270
+ puts "\n" + "=" * 60
271
+ puts "SECTION 5: Error Tracking Integration"
272
+ puts "=" * 60
273
+
274
+ # Pattern 1: Bugsnag integration
275
+ puts "\nPattern 1: Bugsnag integration"
276
+ puts <<~CODE
277
+ config = SecApi::Config.new(
278
+ api_key: ENV.fetch("SECAPI_API_KEY"),
279
+
280
+ on_error: ->(request_id:, error:, url:, method:) {
281
+ Bugsnag.notify(error) do |report|
282
+ report.add_metadata(:sec_api, {
283
+ request_id: request_id,
284
+ url: url,
285
+ method: method
286
+ })
287
+
288
+ # Set severity based on error type
289
+ if error.is_a?(SecApi::PermanentError)
290
+ report.severity = "error"
291
+ else
292
+ report.severity = "warning" # Transient errors that exhausted retries
293
+ end
294
+ end
295
+ },
296
+
297
+ # Also track stream callback errors
298
+ on_callback_error: ->(info) {
299
+ Bugsnag.notify(info[:error]) do |report|
300
+ report.add_metadata(:filing, {
301
+ accession_no: info[:accession_no],
302
+ ticker: info[:ticker]
303
+ })
304
+ end
305
+ }
306
+ )
307
+ CODE
308
+
309
+ # Pattern 2: Sentry integration
310
+ puts "\nPattern 2: Sentry integration"
311
+ puts <<~CODE
312
+ config = SecApi::Config.new(
313
+ api_key: ENV.fetch("SECAPI_API_KEY"),
314
+
315
+ on_error: ->(request_id:, error:, url:, method:) {
316
+ Sentry.capture_exception(error) do |scope|
317
+ scope.set_tags(
318
+ sec_api_request_id: request_id,
319
+ sec_api_method: method
320
+ )
321
+ scope.set_context("sec_api", {
322
+ url: url,
323
+ error_class: error.class.name
324
+ })
325
+ end
326
+ }
327
+ )
328
+ CODE
329
+
330
+ # =============================================================================
331
+ # SECTION 6: Correlation ID Usage for Request Tracing
332
+ # =============================================================================
333
+
334
+ puts "\n" + "=" * 60
335
+ puts "SECTION 6: Correlation ID Tracing"
336
+ puts "=" * 60
337
+
338
+ puts "\nThe request_id parameter is a UUID generated per request."
339
+ puts "Use it to correlate logs, metrics, and errors for a single request."
340
+
341
+ # Pattern: Full request tracing
342
+ puts "\nPattern: Full request lifecycle tracing"
343
+ puts <<~CODE
344
+ config = SecApi::Config.new(
345
+ api_key: ENV.fetch("SECAPI_API_KEY"),
346
+ logger: Rails.logger,
347
+
348
+ on_request: ->(request_id:, method:, url:, headers:) {
349
+ # Store request_id in thread-local for access elsewhere
350
+ Thread.current[:sec_api_request_id] = request_id
351
+
352
+ Rails.logger.info("[SEC_API] Request started", {
353
+ request_id: request_id,
354
+ method: method,
355
+ url: url
356
+ })
357
+ },
358
+
359
+ on_response: ->(request_id:, status:, duration_ms:, url:, method:) {
360
+ Rails.logger.info("[SEC_API] Request completed", {
361
+ request_id: request_id,
362
+ status: status,
363
+ duration_ms: duration_ms
364
+ })
365
+
366
+ Thread.current[:sec_api_request_id] = nil
367
+ },
368
+
369
+ on_retry: ->(request_id:, attempt:, max_attempts:, error_class:, **) {
370
+ Rails.logger.warn("[SEC_API] Retrying", {
371
+ request_id: request_id,
372
+ attempt: attempt,
373
+ max_attempts: max_attempts,
374
+ error_class: error_class
375
+ })
376
+ },
377
+
378
+ on_error: ->(request_id:, error:, url:, method:) {
379
+ Rails.logger.error("[SEC_API] Request failed", {
380
+ request_id: request_id,
381
+ error_class: error.class.name,
382
+ error_message: error.message
383
+ })
384
+
385
+ Thread.current[:sec_api_request_id] = nil
386
+ }
387
+ )
388
+ CODE
389
+
390
+ # Pattern: OpenTelemetry integration
391
+ puts "\nPattern: OpenTelemetry tracing"
392
+ puts <<~CODE
393
+ config = SecApi::Config.new(
394
+ api_key: ENV.fetch("SECAPI_API_KEY"),
395
+
396
+ on_request: ->(request_id:, method:, url:, headers:) {
397
+ span = OpenTelemetry::Trace.current_span
398
+ span.set_attribute("sec_api.request_id", request_id)
399
+ span.set_attribute("http.method", method.to_s.upcase)
400
+ span.set_attribute("http.url", url)
401
+ },
402
+
403
+ on_response: ->(request_id:, status:, duration_ms:, url:, method:) {
404
+ span = OpenTelemetry::Trace.current_span
405
+ span.set_attribute("http.status_code", status)
406
+ span.set_attribute("sec_api.duration_ms", duration_ms)
407
+ }
408
+ )
409
+ CODE
410
+
411
+ # =============================================================================
412
+ # SECTION 7: Filing Journey Tracking
413
+ # =============================================================================
414
+
415
+ puts "\n" + "=" * 60
416
+ puts "SECTION 7: Filing Journey Tracking"
417
+ puts "=" * 60
418
+
419
+ puts "\nFilingJourney tracks a filing through your processing pipeline:"
420
+ puts " detected -> queried -> extracted -> processed"
421
+ puts "\nUse accession_no as the correlation key across all stages."
422
+
423
+ # Demonstrate FilingJourney logging
424
+ puts "\nFilingJourney log output:"
425
+ journey_logger = Logger.new($stdout)
426
+ journey_logger.formatter = proc { |sev, time, prog, msg| "#{msg}\n" }
427
+
428
+ SecApi::FilingJourney.log_detected(journey_logger, :info,
429
+ accession_no: "0000320193-24-000001",
430
+ ticker: "AAPL",
431
+ form_type: "10-K",
432
+ latency_ms: 1500)
433
+
434
+ SecApi::FilingJourney.log_queried(journey_logger, :info,
435
+ accession_no: "0000320193-24-000001",
436
+ found: true,
437
+ duration_ms: 150)
438
+
439
+ SecApi::FilingJourney.log_extracted(journey_logger, :info,
440
+ accession_no: "0000320193-24-000001",
441
+ facts_count: 42,
442
+ duration_ms: 200)
443
+
444
+ SecApi::FilingJourney.log_processed(journey_logger, :info,
445
+ accession_no: "0000320193-24-000001",
446
+ success: true,
447
+ total_duration_ms: 1850)
448
+
449
+ # Pattern: Complete pipeline with FilingJourney
450
+ puts "\nPattern: Complete filing pipeline"
451
+ puts <<~CODE
452
+ class FilingPipeline
453
+ def initialize(client, logger)
454
+ @client = client
455
+ @logger = logger
456
+ end
457
+
458
+ def process_filing(stream_filing)
459
+ accession_no = stream_filing.accession_no
460
+ start_time = Time.now
461
+
462
+ # Stage 1: Log detection
463
+ SecApi::FilingJourney.log_detected(@logger, :info,
464
+ accession_no: accession_no,
465
+ ticker: stream_filing.ticker,
466
+ form_type: stream_filing.form_type,
467
+ latency_ms: stream_filing.latency_ms
468
+ )
469
+
470
+ # Stage 2: Query for full details
471
+ query_start = Time.now
472
+ full_filing = @client.query
473
+ .ticker(stream_filing.ticker)
474
+ .form_type(stream_filing.form_type)
475
+ .limit(1)
476
+ .search
477
+ .first
478
+
479
+ SecApi::FilingJourney.log_queried(@logger, :info,
480
+ accession_no: accession_no,
481
+ found: !full_filing.nil?,
482
+ duration_ms: SecApi::FilingJourney.calculate_duration_ms(query_start)
483
+ )
484
+
485
+ # Stage 3: Extract XBRL
486
+ extract_start = Time.now
487
+ xbrl_data = @client.xbrl.to_json(full_filing)
488
+
489
+ SecApi::FilingJourney.log_extracted(@logger, :info,
490
+ accession_no: accession_no,
491
+ facts_count: xbrl_data&.facts&.size || 0,
492
+ duration_ms: SecApi::FilingJourney.calculate_duration_ms(extract_start)
493
+ )
494
+
495
+ # Stage 4: Process
496
+ process_xbrl_data(xbrl_data)
497
+
498
+ SecApi::FilingJourney.log_processed(@logger, :info,
499
+ accession_no: accession_no,
500
+ success: true,
501
+ total_duration_ms: SecApi::FilingJourney.calculate_duration_ms(start_time)
502
+ )
503
+
504
+ rescue => e
505
+ SecApi::FilingJourney.log_processed(@logger, :error,
506
+ accession_no: accession_no,
507
+ success: false,
508
+ total_duration_ms: SecApi::FilingJourney.calculate_duration_ms(start_time),
509
+ error_class: e.class.name
510
+ )
511
+ raise
512
+ end
513
+ end
514
+ CODE
515
+
516
+ # =============================================================================
517
+ # SECTION 8: Complete Production Configuration
518
+ # =============================================================================
519
+
520
+ puts "\n" + "=" * 60
521
+ puts "SECTION 8: Complete Production Configuration"
522
+ puts "=" * 60
523
+
524
+ puts "\nComplete production setup with all observability:"
525
+ puts <<~CODE
526
+ require 'sec_api'
527
+ require 'datadog/statsd'
528
+
529
+ statsd = Datadog::Statsd.new('localhost', 8125)
530
+
531
+ config = SecApi::Config.new(
532
+ api_key: ENV.fetch("SECAPI_API_KEY"),
533
+
534
+ # Automatic logging
535
+ logger: Rails.logger,
536
+ log_level: :info,
537
+ default_logging: true,
538
+
539
+ # Automatic metrics
540
+ metrics_backend: statsd,
541
+
542
+ # Rate limiting observability
543
+ rate_limit_threshold: 0.2,
544
+ on_throttle: ->(info) {
545
+ Rails.logger.info("Rate limit throttle", info)
546
+ },
547
+ on_queue: ->(info) {
548
+ Rails.logger.warn("Request queued", info)
549
+ },
550
+
551
+ # Error tracking (custom, overrides default_logging)
552
+ on_error: ->(request_id:, error:, url:, method:) {
553
+ # Log the error
554
+ SecApi::StructuredLogger.log_error(Rails.logger, :error,
555
+ request_id: request_id, error: error, url: url, method: method)
556
+
557
+ # Also send to Bugsnag
558
+ Bugsnag.notify(error) do |report|
559
+ report.add_metadata(:sec_api, {request_id: request_id, url: url})
560
+ end
561
+ },
562
+
563
+ # Stream monitoring
564
+ stream_latency_warning_threshold: 120.0,
565
+ on_filing: ->(filing:, latency_ms:, received_at:) {
566
+ statsd.histogram("sec_api.stream.latency_ms", latency_ms,
567
+ tags: ["form_type:\#{filing.form_type}"])
568
+ },
569
+ on_reconnect: ->(info) {
570
+ Rails.logger.warn("Stream reconnected", info)
571
+ statsd.increment("sec_api.stream.reconnected")
572
+ },
573
+ on_callback_error: ->(info) {
574
+ Bugsnag.notify(info[:error])
575
+ }
576
+ )
577
+
578
+ client = SecApi::Client.new(config)
579
+ CODE
580
+
581
+ puts "\n" + "=" * 60
582
+ puts "Examples completed successfully!"
583
+ puts "=" * 60