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.
- checksums.yaml +7 -0
- data/.devcontainer/Dockerfile +54 -0
- data/.devcontainer/README.md +178 -0
- data/.devcontainer/devcontainer.json +46 -0
- data/.devcontainer/docker-compose.yml +28 -0
- data/.devcontainer/post-create.sh +51 -0
- data/.devcontainer/post-start.sh +44 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +0 -0
- data/LICENSE.txt +21 -0
- data/MIGRATION.md +274 -0
- data/README.md +370 -0
- data/Rakefile +10 -0
- data/config/secapi.yml.example +57 -0
- data/docs/development-guide.md +291 -0
- data/docs/enumerator_pattern_design.md +483 -0
- data/docs/examples/README.md +58 -0
- data/docs/examples/backfill_filings.rb +419 -0
- data/docs/examples/instrumentation.rb +583 -0
- data/docs/examples/query_builder.rb +308 -0
- data/docs/examples/streaming_notifications.rb +491 -0
- data/docs/index.md +244 -0
- data/docs/migration-guide-v1.md +1091 -0
- data/docs/pre-review-checklist.md +145 -0
- data/docs/project-overview.md +90 -0
- data/docs/project-scan-report.json +60 -0
- data/docs/source-tree-analysis.md +190 -0
- data/lib/sec_api/callback_helper.rb +49 -0
- data/lib/sec_api/client.rb +606 -0
- data/lib/sec_api/collections/filings.rb +267 -0
- data/lib/sec_api/collections/fulltext_results.rb +86 -0
- data/lib/sec_api/config.rb +590 -0
- data/lib/sec_api/deep_freezable.rb +42 -0
- data/lib/sec_api/errors/authentication_error.rb +24 -0
- data/lib/sec_api/errors/configuration_error.rb +5 -0
- data/lib/sec_api/errors/error.rb +75 -0
- data/lib/sec_api/errors/network_error.rb +26 -0
- data/lib/sec_api/errors/not_found_error.rb +23 -0
- data/lib/sec_api/errors/pagination_error.rb +28 -0
- data/lib/sec_api/errors/permanent_error.rb +29 -0
- data/lib/sec_api/errors/rate_limit_error.rb +57 -0
- data/lib/sec_api/errors/reconnection_error.rb +34 -0
- data/lib/sec_api/errors/server_error.rb +25 -0
- data/lib/sec_api/errors/transient_error.rb +28 -0
- data/lib/sec_api/errors/validation_error.rb +23 -0
- data/lib/sec_api/extractor.rb +122 -0
- data/lib/sec_api/filing_journey.rb +477 -0
- data/lib/sec_api/mapping.rb +125 -0
- data/lib/sec_api/metrics_collector.rb +411 -0
- data/lib/sec_api/middleware/error_handler.rb +250 -0
- data/lib/sec_api/middleware/instrumentation.rb +186 -0
- data/lib/sec_api/middleware/rate_limiter.rb +541 -0
- data/lib/sec_api/objects/data_file.rb +34 -0
- data/lib/sec_api/objects/document_format_file.rb +45 -0
- data/lib/sec_api/objects/entity.rb +92 -0
- data/lib/sec_api/objects/extracted_data.rb +118 -0
- data/lib/sec_api/objects/fact.rb +147 -0
- data/lib/sec_api/objects/filing.rb +197 -0
- data/lib/sec_api/objects/fulltext_result.rb +66 -0
- data/lib/sec_api/objects/period.rb +96 -0
- data/lib/sec_api/objects/stream_filing.rb +194 -0
- data/lib/sec_api/objects/xbrl_data.rb +356 -0
- data/lib/sec_api/query.rb +423 -0
- data/lib/sec_api/rate_limit_state.rb +130 -0
- data/lib/sec_api/rate_limit_tracker.rb +154 -0
- data/lib/sec_api/stream.rb +841 -0
- data/lib/sec_api/structured_logger.rb +199 -0
- data/lib/sec_api/types.rb +32 -0
- data/lib/sec_api/version.rb +42 -0
- data/lib/sec_api/xbrl.rb +220 -0
- data/lib/sec_api.rb +137 -0
- data/sig/sec_api.rbs +4 -0
- 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
|