Class: Attio::Observability::Instrumentation
- Inherits:
-
Object
- Object
- Attio::Observability::Instrumentation
- Defined in:
- lib/attio/observability.rb
Overview
Base instrumentation class
Instance Attribute Summary collapse
- #logger ⇒ Object readonly
- #metrics ⇒ Object readonly
- #traces ⇒ Object readonly
Instance Method Summary collapse
-
#disable! ⇒ Object
Disable instrumentation.
-
#enable! ⇒ Object
Enable instrumentation.
- #format_log_entry(**fields) ⇒ Object private
-
#initialize(logger: nil, metrics_backend: nil, trace_backend: nil) ⇒ Instrumentation
constructor
A new instance of Instrumentation.
- #log_api_call(method, path, duration, status, error) ⇒ Object private
-
#record_api_call(method:, path:, duration:, status: nil, error: nil) ⇒ Object
Record an API call.
- #record_api_metrics(method, path, duration, error) ⇒ Object private
- #record_api_trace(method, path, status, error) ⇒ Object private
-
#record_cache(key:, hit:) ⇒ Object
Record cache hit/miss.
-
#record_circuit_breaker(endpoint:, old_state:, new_state:) ⇒ Object
Record circuit breaker state change.
-
#record_pool_stats(stats) ⇒ Object
Record connection pool stats.
-
#record_rate_limit(remaining:, limit:, reset_at:) ⇒ Object
Record rate limit information.
- #sanitize_path(path) ⇒ Object private
Constructor Details
#initialize(logger: nil, metrics_backend: nil, trace_backend: nil) ⇒ Instrumentation
Returns a new instance of Instrumentation.
19 20 21 22 23 24 |
# File 'lib/attio/observability.rb', line 19 def initialize(logger: nil, metrics_backend: nil, trace_backend: nil) @logger = logger || Logger.new($stdout) @metrics = Metrics.for(metrics_backend) if metrics_backend @traces = Traces.for(trace_backend) if trace_backend @enabled = true end |
Instance Attribute Details
#logger ⇒ Object (readonly)
17 18 19 |
# File 'lib/attio/observability.rb', line 17 def logger @logger end |
#metrics ⇒ Object (readonly)
17 18 19 |
# File 'lib/attio/observability.rb', line 17 def metrics @metrics end |
#traces ⇒ Object (readonly)
17 18 19 |
# File 'lib/attio/observability.rb', line 17 def traces @traces end |
Instance Method Details
#disable! ⇒ Object
Disable instrumentation
113 114 115 |
# File 'lib/attio/observability.rb', line 113 def disable! @enabled = false end |
#enable! ⇒ Object
Enable instrumentation
118 119 120 |
# File 'lib/attio/observability.rb', line 118 def enable! @enabled = true end |
#format_log_entry(**fields) ⇒ Object (private)
122 123 124 125 126 |
# File 'lib/attio/observability.rb', line 122 private def format_log_entry(**fields) fields[:timestamp] = Time.now.iso8601 fields[:service] = "attio-ruby" JSON.generate(fields) end |
#log_api_call(method, path, duration, status, error) ⇒ Object (private)
135 136 137 138 139 140 141 142 143 144 |
# File 'lib/attio/observability.rb', line 135 private def log_api_call(method, path, duration, status, error) @logger.info(format_log_entry( event: "api_call", method: method, path: path, duration_ms: (duration * 1000).round(2), status: status, error: error&.class&.name )) end |
#record_api_call(method:, path:, duration:, status: nil, error: nil) ⇒ Object
Record an API call
33 34 35 36 37 38 39 |
# File 'lib/attio/observability.rb', line 33 def record_api_call(method:, path:, duration:, status: nil, error: nil) return unless @enabled log_api_call(method, path, duration, status, error) record_api_metrics(method, path, duration, error) if @metrics record_api_trace(method, path, status, error) if @traces end |
#record_api_metrics(method, path, duration, error) ⇒ Object (private)
146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/attio/observability.rb', line 146 private def record_api_metrics(method, path, duration, error) @metrics.increment("attio.api.calls", tags: { method: method, path: sanitize_path(path) }) @metrics.histogram("attio.api.duration", duration * 1000, tags: { method: method }) return unless error @metrics.increment("attio.api.errors", tags: { method: method, error_class: error.class.name, }) end |
#record_api_trace(method, path, status, error) ⇒ Object (private)
158 159 160 161 162 163 164 165 |
# File 'lib/attio/observability.rb', line 158 private def record_api_trace(method, path, status, error) @traces.span("attio.api.call") do |span| span.set_attribute("http.method", method.to_s) span.set_attribute("http.path", path) span.set_attribute("http.status_code", status) if status span.set_attribute("error", true) if error end end |
#record_cache(key:, hit:) ⇒ Object
Record cache hit/miss
67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/attio/observability.rb', line 67 def record_cache(key:, hit:) return unless @enabled @logger.debug(format_log_entry( event: "cache", key: key, hit: hit )) @metrics&.increment("attio.cache.#{hit ? 'hits' : 'misses'}") end |
#record_circuit_breaker(endpoint:, old_state:, new_state:) ⇒ Object
Record circuit breaker state change
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/attio/observability.rb', line 84 def record_circuit_breaker(endpoint:, old_state:, new_state:) return unless @enabled @logger.warn(format_log_entry( event: "circuit_breaker", endpoint: endpoint, old_state: old_state, new_state: new_state )) @metrics&.increment("attio.circuit_breaker.transitions", tags: { from: old_state, to: new_state, }) end |
#record_pool_stats(stats) ⇒ Object
Record connection pool stats
103 104 105 106 107 108 109 110 |
# File 'lib/attio/observability.rb', line 103 def record_pool_stats(stats) return unless @enabled @metrics&.gauge("attio.pool.size", stats[:size]) @metrics&.gauge("attio.pool.available", stats[:available]) @metrics&.gauge("attio.pool.allocated", stats[:allocated]) @metrics&.gauge("attio.pool.utilization", stats[:allocated].to_f / stats[:size]) end |
#record_rate_limit(remaining:, limit:, reset_at:) ⇒ Object
Record rate limit information
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/attio/observability.rb', line 46 def record_rate_limit(remaining:, limit:, reset_at:) return unless @enabled utilization = 1.0 - (remaining.to_f / limit) @logger.debug(format_log_entry( event: "rate_limit", remaining: remaining, limit: limit, utilization: utilization.round(3), reset_in: (reset_at - Time.now).to_i )) @metrics&.gauge("attio.rate_limit.remaining", remaining) @metrics&.gauge("attio.rate_limit.utilization", utilization) end |
#sanitize_path(path) ⇒ Object (private)
128 129 130 131 132 133 |
# File 'lib/attio/observability.rb', line 128 private def sanitize_path(path) # Remove IDs from paths for metric aggregation path.gsub(%r{/[a-f0-9-]{36}}, "/:id") # UUIDs .gsub(%r{/[a-zA-Z]+-\d+-[a-zA-Z]+}, "/:id") # IDs like abc-123-def .gsub(%r{/\d+}, "/:id") # Numeric IDs end |