bug_bunny 4.13.0 → 4.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1bcda002a00d8ebc93786a496f8649d43e82d58645aa2b77e3d9b72044e7e07
4
- data.tar.gz: b49f4c724ae39592d6c1a1774873fc8b272cf853688d998cbfb2f22d721d3589
3
+ metadata.gz: 6081a471e20278bc190a7d7517b69cf6fe6d852293bca353974c5ba1cbe4e746
4
+ data.tar.gz: 2e1b952c9627b7f3f1f9ab1e2af25af49ca0422ff59f062ac7499c14265d6a57
5
5
  SHA512:
6
- metadata.gz: 3b16b769d198b82f5455294bfca2c9c157f6dc7cb34a4cceedf9441d11677035a51c97b1d29e7cb59994d7d954072062919c24958ac340d9da018dded6c9e199
7
- data.tar.gz: fdea4a6b6d49dd032787c520e5516045812d94afa5ba4dc8a6ff818e7e89bc6263f92877266e82a40a3789907a2aafd9727ecd2770949511b3e8584915cc87ab
6
+ metadata.gz: 6cf7de01b98c925c8f02de1e09aafc0113f24f93145498ace1616445deecf6319bde3464d9afcd89b5b461904fbe878d4bfb24ea3a2a310149a32f6f2b06f4d0
7
+ data.tar.gz: 2560004c6036b238456fe9c9af9bf478dde67b1ff07240fdac942ecab303467d1db04f3029c2579463e5017b7c9f25ad2e5f1c5ef2c999f86bcde0bedb42eb67
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.14.0] - 2026-05-12
4
+
5
+ ### Nuevas funcionalidades
6
+ - **Duraciones medidas internamente en el Producer:** BugBunny ahora emite `duration_s` automáticamente en los eventos del publisher siguiendo las [OpenTelemetry metric semantic conventions](https://opentelemetry.io/docs/specs/semconv/general/metrics/) (`Float` en segundos). El código de aplicación ya no necesita envolver `client.publish` con `Process.clock_gettime`. — @Gabriel
7
+ - `producer.published` (INFO): `duration_s` del `basic_publish` (TCP enqueue al broker, sin esperar ACK).
8
+ - `producer.confirmed` (INFO): tres duraciones desglosadas — `publish_duration_s`, `confirm_duration_s` (espera de `wait_for_confirms`) y `duration_s` total. Útil para distinguir latencia de red vs latencia del confirm policy del broker.
9
+ - `producer.rpc_response_received`: ahora incluye `duration_s` con el round-trip RPC completo (publish + procesamiento remoto + reply).
10
+
11
+ ### Cambios de comportamiento
12
+ - **`producer.rpc_response_received` promovido de DEBUG a INFO.** No es breaking de API pero aumenta el volumen de logs en clientes RPC. Si el cambio impacta tu pipeline de observabilidad, filtralo por nivel.
13
+
14
+ ### Documentación
15
+ - README + `skill/SKILL.md` + `skill/references/client-middleware.md` actualizados con el catálogo completo de eventos de log emitidos por la gema y la tabla de qué mide cada `duration_s`. Mensaje explícito en ambas audiencias (humana + agente) advirtiendo no duplicar la medición en código de aplicación.
16
+
3
17
  ## [4.13.0] - 2026-05-11
4
18
 
5
19
  ### Nuevas funcionalidades
data/README.md CHANGED
@@ -252,11 +252,27 @@ BugBunny implementa de forma nativa las [OpenTelemetry semantic conventions for
252
252
  Todos los eventos internos se emiten como logs `key=value` compatibles con Datadog, CloudWatch, ELK y ExisRay.
253
253
 
254
254
  ```
255
+ component=bug_bunny event=producer.publish method=POST path=acct/publish messaging_destination_name=acct_x messaging_routing_key=acct.start.42
256
+ component=bug_bunny event=producer.published method=POST path=acct/publish routing_key=acct.start.42 messaging_message_id=corr-1 duration_s=0.000812
257
+ component=bug_bunny event=producer.confirmed method=POST path=acct/publish routing_key=acct.start.42 publish_duration_s=0.000812 confirm_duration_s=0.012 duration_s=0.013
258
+ component=bug_bunny event=producer.rpc_response_received method=GET path=users/42 duration_s=0.034 messaging_operation=receive
255
259
  component=bug_bunny event=consumer.message_processed status=200 duration_s=0.012 messaging_operation=process controller=NodesController action=show
256
260
  component=bug_bunny event=consumer.execution_error error_class=RuntimeError error_message="..." duration_s=0.003
257
261
  component=bug_bunny event=consumer.connection_error attempt_count=2 retry_in_s=10 error_message="..."
258
262
  ```
259
263
 
264
+ ### Duraciones medidas internamente
265
+
266
+ BugBunny mide y emite duraciones automáticamente — **no es necesario envolver llamadas a `client.publish` con `Process.clock_gettime` en el código de aplicación**. Las unidades siguen las [OpenTelemetry metric semantic conventions](https://opentelemetry.io/docs/specs/semconv/general/metrics/) (`s`, segundos como `Float`).
267
+
268
+ | Evento | Duración | Mide |
269
+ |---|---|---|
270
+ | `producer.published` | `duration_s` | Solo el `basic_publish` (TCP enqueue al broker). |
271
+ | `producer.confirmed` | `publish_duration_s` + `confirm_duration_s` + `duration_s` (total) | Publish + espera de ACK del broker. |
272
+ | `producer.rpc_response_received` | `duration_s` | Round-trip RPC completo (publish + procesamiento remoto + reply). |
273
+ | `consumer.message_processed` | `duration_s` | Procesamiento del mensaje (router + controller + reply). |
274
+ | `consumer.execution_error` | `duration_s` | Tiempo transcurrido hasta el error. |
275
+
260
276
  Las claves sensibles (`password`, `token`, `secret`, `api_key`, `authorization`, etc.) se filtran automáticamente a `[FILTERED]` en toda la salida de logs.
261
277
 
262
278
  ---
@@ -60,9 +60,16 @@ module BugBunny
60
60
  # (default `true` — ver {BugBunny::Configuration#nack_raise}).
61
61
  # @raise [BugBunny::CommunicationError] Si el canal AMQP falla durante la publicación o confirm.
62
62
  def confirmed(request)
63
- publish_message(request)
63
+ started_at = monotonic_now
64
+ publish_duration = publish_message(request)
65
+
66
+ confirm_started_at = monotonic_now
64
67
  acked = wait_for_confirms!(request)
68
+ confirm_duration = duration_s(confirm_started_at)
69
+
65
70
  handle_confirm_result(request, acked)
71
+ log_confirmed(request, publish_duration, confirm_duration, started_at)
72
+
66
73
  { 'status' => 202, 'body' => nil }
67
74
  rescue BugBunny::Error
68
75
  raise
@@ -90,6 +97,7 @@ module BugBunny
90
97
  future = Concurrent::IVar.new
91
98
  @pending_requests[cid] = future
92
99
 
100
+ started_at = monotonic_now
93
101
  begin
94
102
  fire(request)
95
103
 
@@ -101,11 +109,7 @@ module BugBunny
101
109
  raise BugBunny::RequestTimeout, "Timeout waiting for RPC: #{request.path} [#{request.method}]" if result.nil?
102
110
 
103
111
  BugBunny.configuration.on_rpc_reply&.call(result[:headers])
104
-
105
- safe_log(:debug, 'producer.rpc_response_received',
106
- messaging_system: 'rabbitmq', messaging_operation: 'receive', messaging_message_id: cid,
107
- response_body: result[:body]&.truncate(500),
108
- response_headers: result[:headers]&.to_json&.truncate(300))
112
+ log_rpc_response_received(request, cid, result, started_at)
109
113
 
110
114
  parse_response(result[:body])
111
115
  ensure
@@ -119,8 +123,11 @@ module BugBunny
119
123
  # Resuelve exchange, serializa payload, logea y publica el mensaje.
120
124
  # Compartido por {#fire} y {#confirmed}.
121
125
  #
126
+ # Emite `producer.publish` antes y `producer.published` después con `duration_s`
127
+ # midiendo solo el `basic_publish` (TCP enqueue al broker, sin esperar ACK).
128
+ #
122
129
  # @param request [BugBunny::Request]
123
- # @return [void]
130
+ # @return [Float] duración del publish en segundos.
124
131
  def publish_message(request)
125
132
  x = @session.exchange(
126
133
  name: request.exchange,
@@ -129,7 +136,43 @@ module BugBunny
129
136
  )
130
137
  payload = serialize_message(request.body)
131
138
  log_request(request, payload)
139
+
140
+ started_at = monotonic_now
132
141
  x.publish(payload, request.amqp_options.merge(routing_key: request.final_routing_key))
142
+ publish_duration = duration_s(started_at)
143
+
144
+ log_published(request, publish_duration)
145
+ publish_duration
146
+ end
147
+
148
+ # @api private
149
+ def log_published(request, publish_duration_s)
150
+ safe_log(:info, 'producer.published',
151
+ method: request.method.to_s.upcase, path: request.path,
152
+ routing_key: request.final_routing_key,
153
+ messaging_message_id: request.correlation_id,
154
+ duration_s: publish_duration_s)
155
+ end
156
+
157
+ # @api private
158
+ def log_rpc_response_received(request, cid, result, started_at)
159
+ safe_log(:info, 'producer.rpc_response_received',
160
+ method: request.method.to_s.upcase, path: request.path,
161
+ messaging_system: 'rabbitmq', messaging_operation: 'receive', messaging_message_id: cid,
162
+ duration_s: duration_s(started_at),
163
+ response_body: result[:body]&.truncate(500),
164
+ response_headers: result[:headers]&.to_json&.truncate(300))
165
+ end
166
+
167
+ # @api private
168
+ def log_confirmed(request, publish_duration_s, confirm_duration_s, started_at)
169
+ safe_log(:info, 'producer.confirmed',
170
+ method: request.method.to_s.upcase, path: request.path,
171
+ routing_key: request.final_routing_key,
172
+ messaging_message_id: request.correlation_id,
173
+ publish_duration_s: publish_duration_s,
174
+ confirm_duration_s: confirm_duration_s,
175
+ duration_s: duration_s(started_at))
133
176
  end
134
177
 
135
178
  # Espera la confirmación del broker con timeout opcional.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = '4.13.0'
4
+ VERSION = '4.14.0'
5
5
  end
data/skill/SKILL.md CHANGED
@@ -97,6 +97,37 @@ BugBunny implementa las [OpenTelemetry semantic conventions for messaging](https
97
97
  - **Consumer:** Extrae los campos de los logs estructurados sin mutar los headers originales. Los eventos `consumer.message_received` y `consumer.message_processed` incluyen estos campos automáticamente.
98
98
  - **RPC Reply:** El consumer inyecta los mismos campos en el reply para cerrar el ciclo de traza del lado del cliente.
99
99
 
100
+ ### Eventos de log y duraciones internas
101
+
102
+ BugBunny mide y emite duraciones automáticamente. **No envolver llamadas a `client.publish` con `Process.clock_gettime` en código de aplicación** — duplica el trabajo. Las duraciones siguen las [OpenTelemetry metric semantic conventions](https://opentelemetry.io/docs/specs/semconv/general/metrics/) (`duration_s` como `Float` en segundos).
103
+
104
+ | Evento | Nivel | Emitido por | Campos clave |
105
+ |---|---|---|---|
106
+ | `producer.publish` | INFO | `Producer#publish_message` (pre) | `method`, `path`, `messaging_*` |
107
+ | `producer.publish_payload` | INFO | `Producer#publish_message` | `payload` (truncado), `body_size` |
108
+ | `producer.publish_detail` | DEBUG | `Producer#publish_message` | `exchange_opts` final |
109
+ | `producer.published` | INFO | `Producer#publish_message` (post) | `method`, `path`, `routing_key`, `messaging_message_id`, **`duration_s`** (publish solo) |
110
+ | `producer.confirmed` | INFO | `Producer#confirmed` (post-ACK) | `method`, `path`, `routing_key`, **`publish_duration_s`**, **`confirm_duration_s`**, **`duration_s`** (total) |
111
+ | `producer.confirms_nacked` | WARN | `Producer#confirmed` (NACK) | `count`, `path` |
112
+ | `producer.rpc_waiting` | DEBUG | `Producer#rpc` | `messaging_message_id`, `timeout_s` |
113
+ | `producer.rpc_response_received` | INFO | `Producer#rpc` (reply recibido) | `method`, `path`, **`duration_s`** (round-trip total), `response_body` |
114
+ | `producer.rpc_response_orphaned` | WARN | reply listener | `correlation_id` |
115
+ | `consumer.message_received` | INFO | `Consumer#process_message` | `method`, `path`, `messaging_*` |
116
+ | `consumer.message_processed` | INFO | `Consumer#process_message` (post) | `response_status`, **`duration_s`**, `controller`, `action`, `messaging_*` |
117
+ | `consumer.execution_error` | ERROR | `Consumer#process_message` (rescue) | **`duration_s`**, `error_class`, `error_message` |
118
+ | `consumer.route_not_found` | WARN | `Consumer#process_message` | `method`, `path` |
119
+ | `consumer.connection_error` | ERROR | `Consumer#subscribe` (retry loop) | `attempt_count`, `retry_in_s`, `error_*` |
120
+ | `session.broker_return` | WARN | `Session` (mandatory unrouted) | `reply_code`, `reply_text`, `exchange`, `routing_key` |
121
+
122
+ **Resumen de qué mide cada `duration_s`:**
123
+
124
+ - `producer.published.duration_s` — solo el `basic_publish` (TCP enqueue al broker).
125
+ - `producer.confirmed.publish_duration_s` — el publish.
126
+ - `producer.confirmed.confirm_duration_s` — la espera del ACK del broker (`wait_for_confirms`).
127
+ - `producer.confirmed.duration_s` — total (publish + ACK).
128
+ - `producer.rpc_response_received.duration_s` — round-trip RPC completo (publish + procesamiento remoto + reply).
129
+ - `consumer.message_processed.duration_s` — procesamiento server-side (router + controller + reply).
130
+
100
131
  ---
101
132
 
102
133
  ## API: Configuración Global
@@ -59,11 +59,13 @@ El `Producer` es usado internamente por el `Client`. Implementa tres patrones de
59
59
  - Reply listener (`basic_consume`) auto-iniciado en el primer RPC.
60
60
  - Double-checked locking mutex para seguridad del listener.
61
61
  - Timeout lanza `BugBunny::RequestTimeout`.
62
+ - **Emite `producer.rpc_response_received` (INFO) con `duration_s` = round-trip total** (publish + procesamiento remoto + reply). No medir en código de aplicación.
62
63
 
63
64
  ### Fire-and-Forget (`Producer#fire`)
64
65
 
65
66
  - Publica en el exchange y retorna `{ 'status' => 202 }` inmediatamente.
66
67
  - Sin confirmación de procesamiento.
68
+ - **Emite `producer.published` (INFO) con `duration_s`** = solo el `basic_publish` (TCP enqueue al broker).
67
69
 
68
70
  ### Confirmed (`Producer#confirmed`)
69
71
 
@@ -72,6 +74,7 @@ El `Producer` es usado internamente por el `Client`. Implementa tres patrones de
72
74
  - Si `wait_for_confirms` devuelve `false` (broker NACKea), se logea `producer.confirms_nacked` con `count` y `path`. Por default (`config.nack_raise = true`) levanta `BugBunny::PublishNacked` con `path` y `nacked_count`. Para opt-out: `config.nack_raise = false` o pasar `nack_raise: false` per request — en ese caso solo logea y retorna 202.
73
75
  - Si `mandatory: true` y el mensaje no es ruteable, el broker dispara `basic.return`. El handler se atacha vía `Bunny::Exchange#on_return` en `Session#exchange` la primera vez que se resuelve cada exchange (cacheado por nombre, una sola vez por canal) y delega a `Configuration#on_return` o al logger por default.
74
76
  - Errores del canal se envuelven en `BugBunny::CommunicationError`; errores `BugBunny::Error` pre-existentes se propagan sin envolver.
77
+ - **Emite `producer.confirmed` (INFO) con tres duraciones desglosadas**: `publish_duration_s` (TCP enqueue), `confirm_duration_s` (`wait_for_confirms`), `duration_s` (total). Útil para distinguir latencia de red vs latencia de confirm policy del broker.
75
78
 
76
79
  ## Middleware Stack (Client-side, Onion Architecture)
77
80
 
@@ -95,6 +95,31 @@ RSpec.describe BugBunny::Producer do
95
95
  messaging_destination_name: 'events_x'
96
96
  )
97
97
  end
98
+
99
+ it 'producer.published incluye duration_s y campos de routing' do
100
+ request = BugBunny::Request.new('users')
101
+ request.exchange = 'users_x'
102
+ request.method = :post
103
+ request.correlation_id = 'corr-1'
104
+
105
+ fake_exchange = double('exchange')
106
+ allow(session).to receive(:exchange).and_return(fake_exchange)
107
+ allow(fake_exchange).to receive(:publish)
108
+
109
+ producer.fire(request)
110
+
111
+ published_event = logged_events.find { |e| e[:event] == 'producer.published' }
112
+ expect(published_event).not_to be_nil
113
+ expect(published_event[:level]).to eq(:info)
114
+ expect(published_event[:kwargs]).to include(
115
+ method: 'POST',
116
+ path: 'users',
117
+ routing_key: 'users',
118
+ messaging_message_id: 'corr-1'
119
+ )
120
+ expect(published_event[:kwargs][:duration_s]).to be_a(Numeric)
121
+ expect(published_event[:kwargs][:duration_s]).to be >= 0
122
+ end
98
123
  end
99
124
 
100
125
  describe '#rpc — log events incluyen campos OTel' do
@@ -182,6 +207,28 @@ RSpec.describe BugBunny::Producer do
182
207
  messaging_message_id: 'corr-reply-test'
183
208
  )
184
209
  end
210
+
211
+ it 'producer.rpc_response_received incluye duration_s total' do
212
+ request = BugBunny::Request.new('users')
213
+ request.exchange = 'users_x'
214
+ request.method = :get
215
+
216
+ allow(rpc_producer).to receive(:ensure_reply_listener!)
217
+
218
+ ivar = Concurrent::IVar.new
219
+ allow(Concurrent::IVar).to receive(:new).and_return(ivar)
220
+
221
+ request.correlation_id = 'corr-dur'
222
+ rpc_producer.instance_variable_get(:@pending_requests)['corr-dur'] = ivar
223
+
224
+ Thread.new { ivar.set({ body: '{"ok":true}', headers: {} }) }.join(0.1)
225
+
226
+ rpc_producer.rpc(request)
227
+
228
+ response_event = logged_events.find { |e| e[:event] == 'producer.rpc_response_received' }
229
+ expect(response_event[:kwargs][:duration_s]).to be_a(Numeric)
230
+ expect(response_event[:kwargs][:duration_s]).to be >= 0
231
+ end
185
232
  end
186
233
  end
187
234
 
@@ -252,6 +299,18 @@ RSpec.describe BugBunny::Producer do
252
299
  expect(nack_event).to be_nil
253
300
  end
254
301
 
302
+ it 'logea producer.confirmed con publish_duration_s, confirm_duration_s y duration_s total' do
303
+ confirmed_producer.confirmed(build_request)
304
+
305
+ ev = logged_events.find { |e| e[:event] == 'producer.confirmed' }
306
+ expect(ev).not_to be_nil
307
+ expect(ev[:level]).to eq(:info)
308
+ expect(ev[:kwargs]).to include(method: 'POST', path: 'acct.start', routing_key: 'acct.start')
309
+ expect(ev[:kwargs][:publish_duration_s]).to be_a(Numeric)
310
+ expect(ev[:kwargs][:confirm_duration_s]).to be_a(Numeric)
311
+ expect(ev[:kwargs][:duration_s]).to be_a(Numeric)
312
+ end
313
+
255
314
  context 'cuando el broker NACKea (wait_for_confirms devuelve false)' do
256
315
  before do
257
316
  allow(mock_channel).to receive(:wait_for_confirms).and_return(false)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.13.0
4
+ version: 4.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-11 00:00:00.000000000 Z
11
+ date: 2026-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -303,7 +303,7 @@ metadata:
303
303
  homepage_uri: https://github.com/gedera/bug_bunny
304
304
  source_code_uri: https://github.com/gedera/bug_bunny
305
305
  changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
306
- documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.13.0/skill
306
+ documentation_uri: https://github.com/gedera/bug_bunny/blob/v4.14.0/skill
307
307
  post_install_message:
308
308
  rdoc_options: []
309
309
  require_paths: