Class: Attio::Observability::Instrumentation

Inherits:
Object
  • Object
show all
Defined in:
lib/attio/observability.rb

Overview

Base instrumentation class

Since:

  • 1.0.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(logger: nil, metrics_backend: nil, trace_backend: nil) ⇒ Instrumentation

Returns a new instance of Instrumentation.

Since:

  • 1.0.0



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

#loggerObject (readonly)

Since:

  • 1.0.0



17
18
19
# File 'lib/attio/observability.rb', line 17

def logger
  @logger
end

#metricsObject (readonly)

Since:

  • 1.0.0



17
18
19
# File 'lib/attio/observability.rb', line 17

def metrics
  @metrics
end

#tracesObject (readonly)

Since:

  • 1.0.0



17
18
19
# File 'lib/attio/observability.rb', line 17

def traces
  @traces
end

Instance Method Details

#disable!Object

Disable instrumentation

Since:

  • 1.0.0



113
114
115
# File 'lib/attio/observability.rb', line 113

def disable!
  @enabled = false
end

#enable!Object

Enable instrumentation

Since:

  • 1.0.0



118
119
120
# File 'lib/attio/observability.rb', line 118

def enable!
  @enabled = true
end

#format_log_entry(**fields) ⇒ Object (private)

Since:

  • 1.0.0



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)

Since:

  • 1.0.0



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

Parameters:

  • method (Symbol)

    HTTP method

  • path (String)

    API path

  • duration (Float)

    Call duration in seconds

  • status (Integer) (defaults to: nil)

    HTTP status code

  • error (Exception) (defaults to: nil)

    Error if any

Since:

  • 1.0.0



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)

Since:

  • 1.0.0



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)

Since:

  • 1.0.0



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

Parameters:

  • key (String)

    Cache key

  • hit (Boolean)

    Whether it was a hit

Since:

  • 1.0.0



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

Parameters:

  • endpoint (String)

    Endpoint name

  • old_state (Symbol)

    Previous state

  • new_state (Symbol)

    New state

Since:

  • 1.0.0



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

Parameters:

  • stats (Hash)

    Pool statistics

Since:

  • 1.0.0



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

Parameters:

  • remaining (Integer)

    Requests remaining

  • limit (Integer)

    Rate limit

  • reset_at (Time)

    When limit resets

Since:

  • 1.0.0



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)

Since:

  • 1.0.0



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