exis_ray 0.5.11 → 0.7.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.
@@ -0,0 +1,155 @@
1
+ # Estándar de Logging Wispro — Complemento
2
+
3
+ > Este documento recoge las reglas del estándar de logging del ecosistema Wispro que **no** están ya cubiertas en `SKILL.md`. Para formato general (`component=x event=y`), niveles, `source`, DEBUG block form, reloj monotónico, filtrado de claves sensibles y campos auto-inyectados, ver `SKILL.md`.
4
+
5
+ ---
6
+
7
+ ## Data First — Unidad en la key, número en el valor
8
+
9
+ Regla de oro para métricas operables: **separar la unidad del dato numérico**. Nunca incluir unidades dentro de los valores.
10
+
11
+ ```
12
+ # Incorrecto
13
+ duration="0.5s" memory="128MB"
14
+
15
+ # Correcto
16
+ duration_s=0.5 memory_mb=128
17
+ ```
18
+
19
+ Sufijos de unidad estándar:
20
+
21
+ | Sufijo | Tipo | Uso |
22
+ |:---------|:--------|:----|
23
+ | `_s` | Float | Segundos. Estándar para duraciones y latencias. |
24
+ | `_ms` | Integer | Milisegundos. Precisión técnica interna de alta frecuencia. |
25
+ | `_count` | Integer | Cantidades o volúmenes. Ej: `record_count`, `retry_count`. |
26
+ | `_bytes` / `_kb` / `_mb` | Integer | Almacenamiento o memoria. |
27
+ | `_human` | String | (Opcional) Texto legible. Ej: `duration_human="2 minutes 5 seconds"`. |
28
+
29
+ Los valores numéricos se emiten como números reales (sin comillas) para que el motor de logs haga casting automático.
30
+
31
+ ---
32
+
33
+ ## Alineación con OpenTelemetry
34
+
35
+ `ExisRay` sigue el **OpenTelemetry Log Data Model**. Los campos del estándar Wispro se mapean a las convenciones semánticas oficiales de OTel:
36
+
37
+ | Campo Wispro | OTel Semantic Convention | Descripción |
38
+ |:--------------|:--------------------------------|:------------|
39
+ | `body` | `body` | Contenido principal del log (texto libre). |
40
+ | `level` | `severity_text` | Nivel de importancia. |
41
+ | `http_status` | `http.response.status_code` | Código de respuesta HTTP (Integer). |
42
+ | `outcome` | `otel.status_code` (enum) | Resultado semántico de tasks/jobs (`success`/`failed`). |
43
+ | `method` | `http.request.method` | Método HTTP. |
44
+ | `path` | `url.path` | Ruta del request. |
45
+ | `user_id` | `user.id` | Identificador del usuario. |
46
+
47
+ > **Nota sobre `duration_s`**: OTel no define un atributo genérico `duration`. Las duraciones se expresan implícitamente en spans (diferencia entre `start_time` y `end_time`), o en métricas específicas como `http.server.request.duration` (histograma en segundos, según la convención de métricas). En logs Wispro, `duration_s` es una **extensión propia** que permite queries directas sin contexto de span.
48
+
49
+ Para campos nuevos, seguir las OpenTelemetry Semantic Conventions siempre que exista una equivalencia oficial.
50
+
51
+ ### Naming de campos OTel en formato flat
52
+
53
+ ExisRay emite JSON flat (ver divergencia D2 más abajo). Al mapear campos OTel al formato flat, aplicar estas reglas:
54
+
55
+ 1. **Dots → underscores**: `messaging.system` → `messaging_system`, `exception.type` → `exception.type` (excepción: los campos `exception.*` mantienen el dot por estar en transición OTel).
56
+ 2. **Preservar el prefijo semántico**: nunca abreviar el namespace OTel. `messaging.system` se convierte en `messaging_system`, **no** en `system`. El prefijo evita colisiones con otros campos del log (ej: `source: "system"` ya existe en ExisRay).
57
+ 3. **Sufijo de unidad donde aplique**: si el campo OTel tiene unidad implícita, agregar el sufijo Wispro (`_s`, `_ms`, `_bytes`). Ej: `http.server.request.duration` → `duration_s`.
58
+
59
+ Ejemplos de mapeo existente:
60
+
61
+ | OTel Semantic Convention | Campo flat ExisRay |
62
+ |:-------------------------|:-------------------|
63
+ | `http.response.status_code` | `http_status` |
64
+ | `user_agent.original` | `user_agent_original` |
65
+ | `server.address` | `server_address` |
66
+ | `service.version` | `service_version` |
67
+ | `deployment.environment` | `deployment_environment` |
68
+ | `exception.type` | `exception.type` (en transición) |
69
+ | `messaging.system` | `messaging_system` |
70
+ | `messaging.operation` | `messaging_operation` |
71
+ | `messaging.destination.name` | `messaging_destination_name` |
72
+ | `messaging.message.id` | `messaging_message_id` |
73
+ | `messaging.rabbitmq.destination.routing_key` | `messaging_routing_key` |
74
+
75
+ **Regla de oro**: si al leer el campo aislado de su contexto no queda claro a qué dominio pertenece (ej: `system`, `operation`, `id`, `name`), le falta el prefijo.
76
+
77
+ ---
78
+
79
+ ## Divergencias deliberadas con OpenTelemetry
80
+
81
+ ExisRay no pretende ser una implementación OTel. Las siguientes divergencias son decisiones conscientes con razones de peso:
82
+
83
+ ### Wire format AWS X-Ray, no W3C Trace Context
84
+
85
+ OTel usa `traceparent: 00-<trace-id>-<parent-id>-<flags>`. ExisRay usa `X-Amzn-Trace-Id: Root=1-...;Self=...`.
86
+
87
+ **Razón**: toda la infraestructura Wispro vive en AWS con X-Ray como backend de trazas. Si en el futuro se migra a un backend OTel-native, la traducción ocurrirá solo en el edge (HTTP middleware), sin tocar el core.
88
+
89
+ ### Flat JSON, no resource/attributes anidado
90
+
91
+ OTel Log Data Model anida: `{"Resource": {...}, "Attributes": {...}, "Body": "..."}`. ExisRay emite flat: `{"service": "...", "component": "...", ...}`.
92
+
93
+ **Razón**: los agregadores comunes (CloudWatch, Loki, Datadog) indexan mejor JSON flat. Queries como `component:auth AND event:login_failed` son triviales en flat. Un exporter OTel futuro haría el unflatten al emitir.
94
+
95
+ ### Sufijos de unidad en keys (`_s`, `_ms`, `_bytes`)
96
+
97
+ OTel usa unit metadata UCUM o nombres sin sufijo. ExisRay usa sufijos explícitos.
98
+
99
+ **Razón**: en JSON flat, un campo llamado `duration` sin unidad es ambiguo. El sufijo hace que el consumidor humano y el parser automático sepan la unidad sin metadata externa (regla "Data First" Wispro).
100
+
101
+ ### Campo `source` (http/sidekiq/task/system)
102
+
103
+ No tiene equivalente OTel directo. Lo más parecido es `span.kind`, pero mide otra cosa.
104
+
105
+ **Razón**: es el campo más útil para filtrado operacional — saber si un error viene de una request HTTP, un job o un consumer es la primera pregunta al mirar logs. Es una **extensión Wispro**.
106
+
107
+ ### Campo `component`
108
+
109
+ OTel lo más parecido es `instrumentation.scope.name`. ExisRay lo usa para identificar el módulo de negocio (`component=billing`, `component=auth`).
110
+
111
+ **Razón**: `component` es dominio-específico y útil para filtrado operacional. Convive sin conflicto con `instrumentation.scope.name` si en el futuro se agrega el emisor técnico.
112
+
113
+ ---
114
+
115
+ ## Ciclo de Vida del Evento
116
+
117
+ ### Procesos, Jobs y Tasks
118
+
119
+ Todo proceso aislado debe reportar inicio y finalización con estructura consistente:
120
+
121
+ - `event`: identificador de estado (`task_started`, `task_finished`, `job_started`, `job_finished`).
122
+ - `outcome`: resultado final como **string semántico**, nunca un código numérico: `success`, `failed`, `aborted`.
123
+ - `duration_s`: tiempo total de ejecución (reloj monotónico).
124
+ - `error_class` y `error_message`: **obligatorios solo en caso de fallo**.
125
+
126
+ ```ruby
127
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
128
+ begin
129
+ InvoiceService.process_all
130
+ duration_s = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
131
+ Rails.logger.info("component=billing event=task_finished outcome=success duration_s=#{duration_s} record_count=500")
132
+ rescue => e
133
+ duration_s = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
134
+ Rails.logger.error("component=billing event=task_finished outcome=failed duration_s=#{duration_s} error_class=#{e.class} error_message=\"#{e.message}\"")
135
+ raise
136
+ end
137
+ ```
138
+
139
+ `ExisRay::TaskMonitor` implementa exactamente este contrato para Rake/Cron.
140
+
141
+ ### Peticiones HTTP
142
+
143
+ Los logs de cierre de request estandarizan el reporte de rendimiento:
144
+
145
+ - `http_status`: **código HTTP como Integer** (ej: `200`, `404`, `500`).
146
+ - `duration_s`: tiempo total de respuesta del servidor.
147
+ - `[subsystem]_runtime_s`: desglose opcional por capa (`db_runtime_s`, `view_runtime_s`).
148
+
149
+ `ExisRay::LogSubscriber` emite este shape automáticamente — no hace falta loguearlo manualmente.
150
+
151
+ ---
152
+
153
+ ## Regla de Oro
154
+
155
+ > Tu log manual solo debe contener datos de **tu lógica de negocio**. La infraestructura (ExisRay) ya sabe quién sos, de dónde venís y cuál es tu ID de traza — no dupliques esos campos en los logs manuales.
data/skills.lock CHANGED
@@ -1,6 +1,9 @@
1
1
  ---
2
- synced_at: '2026-04-04 19:02:47'
2
+ synced_at: '2026-04-05 18:20:39'
3
3
  skills:
4
+ - name: action-plan
5
+ scope: local
6
+ path: "/Users/gabriel/src/gems/exis_ray/.agents/skills/action-plan"
4
7
  - name: agent-review
5
8
  scope: local
6
9
  path: "/Users/gabriel/src/gems/exis_ray/.agents/skills/agent-review"
@@ -10,9 +13,15 @@ skills:
10
13
  - name: documentation-writer
11
14
  scope: local
12
15
  path: "/Users/gabriel/src/gems/exis_ray/.agents/skills/documentation-writer"
16
+ - name: find-skills
17
+ scope: local
18
+ path: "/Users/gabriel/src/gems/exis_ray/.agents/skills/find-skills"
13
19
  - name: gem-release
14
20
  scope: local
15
21
  path: "/Users/gabriel/src/gems/exis_ray/.agents/skills/gem-release"
22
+ - name: opentelemetry
23
+ scope: local
24
+ path: "/Users/gabriel/src/gems/exis_ray/.agents/skills/opentelemetry"
16
25
  - name: quality-code
17
26
  scope: local
18
27
  path: "/Users/gabriel/src/gems/exis_ray/.agents/skills/quality-code"
data/skills.yml CHANGED
@@ -2,23 +2,48 @@ mcps:
2
2
  - github
3
3
  - clickup
4
4
  skills:
5
- - name: skill-manager
5
+ skill-manager:
6
6
  repo: sequre/ai_knowledge
7
- - name: yard
7
+ scope: local
8
+ yard:
8
9
  repo: sequre/ai_knowledge
9
- - name: quality-code
10
+ scope: local
11
+ quality-code:
10
12
  repo: sequre/ai_knowledge
11
- - name: gem-release
13
+ scope: local
14
+ gem-release:
12
15
  repo: sequre/ai_knowledge
13
- - name: skill-builder
16
+ scope: local
17
+ skill-builder:
14
18
  repo: sequre/ai_knowledge
15
- - name: ai-reports
19
+ scope: local
20
+ ai-reports:
16
21
  repo: sequre/ai_knowledge
17
- - name: agent-review
22
+ scope: local
23
+ environment:
24
+ space_id: "${AI_REPORTS_SPACE_ID}"
25
+ bug_reports_list_id: "${AI_REPORTS_BUG_REPORTS_LIST_ID}"
26
+ improvements_list_id: "${AI_REPORTS_IMPROVEMENTS_LIST_ID}"
27
+ agent-review:
18
28
  repo: sequre/ai_knowledge
19
- - name: documentation-writer
29
+ scope: local
30
+ environment:
31
+ space_id: "${AGENT_REVIEW_SAPCE_ID}"
32
+ list_id: "${AGENT_LIST_ID}"
33
+ action-plan:
34
+ repo: sequre/ai_knowledge
35
+ environment:
36
+ space_id: "${AGENT_REVIEW_SAPCE_ID}"
37
+ list_id: "${AGENT_ACTION_PLAN_LIST_ID}"
38
+ documentation-writer:
20
39
  repo: github/awesome-copilot
21
40
  path: skills/documentation-writer
22
- - name: find-skills
41
+ scope: local
42
+ find-skills:
23
43
  repo: vercel-labs/skills
24
44
  path: skills/find-skills
45
+ scope: local
46
+ opentelemetry:
47
+ repo: bobmatnyc/claude-mpm-skills
48
+ path: universal/observability/opentelemetry
49
+ scope: local
@@ -14,12 +14,12 @@ RSpec.describe ExisRay::Configuration do
14
14
  expect(config.propagation_trace_header).to eq("X-Amzn-Trace-Id")
15
15
  end
16
16
 
17
- it "reporter_class por defecto es Reporter" do
18
- expect(config.reporter_class).to eq("Reporter")
17
+ it "reporter_class por defecto es nil" do
18
+ expect(config.reporter_class).to be_nil
19
19
  end
20
20
 
21
- it "current_class por defecto es Current" do
22
- expect(config.current_class).to eq("Current")
21
+ it "current_class por defecto es nil" do
22
+ expect(config.current_class).to be_nil
23
23
  end
24
24
 
25
25
  it "log_format por defecto es :text" do
@@ -29,6 +29,75 @@ RSpec.describe ExisRay::Configuration do
29
29
  it "log_subscriber_class por defecto es nil" do
30
30
  expect(config.log_subscriber_class).to be_nil
31
31
  end
32
+
33
+ it "service_version por defecto es nil fuera de Rails" do
34
+ expect(config.service_version).to be_nil
35
+ end
36
+
37
+ it "deployment_environment por defecto es nil fuera de Rails" do
38
+ expect(config.deployment_environment).to be_nil
39
+ end
40
+ end
41
+
42
+ describe "resource attributes OTel" do
43
+ it "permite setear service_version" do
44
+ config = described_class.new
45
+ config.service_version = "2.0.0"
46
+
47
+ expect(config.service_version).to eq("2.0.0")
48
+ end
49
+
50
+ it "permite setear deployment_environment" do
51
+ config = described_class.new
52
+ config.deployment_environment = "staging"
53
+
54
+ expect(config.deployment_environment).to eq("staging")
55
+ end
56
+
57
+ describe "#default_service_version" do
58
+ it "retorna nil cuando Rails no está definido" do
59
+ expect(described_class.new.default_service_version).to be_nil
60
+ end
61
+
62
+ it "lee config.version (atributo directo) con prioridad sobre config.x.version" do
63
+ rails_config = Struct.new(:version).new("1.2.3")
64
+ allow(rails_config).to receive(:respond_to?).and_call_original
65
+ app = Struct.new(:config).new(rails_config)
66
+ stub_const("Rails", Class.new do
67
+ define_singleton_method(:application) { app }
68
+ end)
69
+
70
+ expect(described_class.new.default_service_version).to eq("1.2.3")
71
+ end
72
+
73
+ it "cae a config.x.version si config.version no está definido" do
74
+ x_config = Struct.new(:version).new("3.0.0")
75
+ rails_config = Struct.new(:x).new(x_config)
76
+ app = Struct.new(:config).new(rails_config)
77
+ stub_const("Rails", Class.new do
78
+ define_singleton_method(:application) { app }
79
+ end)
80
+
81
+ expect(described_class.new.default_service_version).to eq("3.0.0")
82
+ end
83
+
84
+ it "retorna nil si config.x.version es un OrderedOptions vacío (no un string)" do
85
+ x_config = ActiveSupport::OrderedOptions.new
86
+ rails_config = Struct.new(:x).new(x_config)
87
+ app = Struct.new(:config).new(rails_config)
88
+ stub_const("Rails", Class.new do
89
+ define_singleton_method(:application) { app }
90
+ end)
91
+
92
+ expect(described_class.new.default_service_version).to be_nil
93
+ end
94
+ end
95
+
96
+ describe "#default_deployment_environment" do
97
+ it "retorna nil cuando Rails no está definido" do
98
+ expect(described_class.new.default_deployment_environment).to be_nil
99
+ end
100
+ end
32
101
  end
33
102
 
34
103
  describe "#json_logs?" do
@@ -141,6 +141,78 @@ RSpec.describe ExisRay::JsonFormatter do
141
141
  end
142
142
  end
143
143
 
144
+ describe "severity_number (OTel Log Data Model)" do
145
+ it "inyecta severity_number=9 para INFO" do
146
+ result = JSON.parse(formatter.call("INFO", timestamp, progname, "event=x"))
147
+
148
+ expect(result["severity_number"]).to eq(9)
149
+ end
150
+
151
+ it "inyecta severity_number=13 para WARN" do
152
+ result = JSON.parse(formatter.call("WARN", timestamp, progname, "event=x"))
153
+
154
+ expect(result["severity_number"]).to eq(13)
155
+ end
156
+
157
+ it "inyecta severity_number=17 para ERROR" do
158
+ result = JSON.parse(formatter.call("ERROR", timestamp, progname, "event=x"))
159
+
160
+ expect(result["severity_number"]).to eq(17)
161
+ end
162
+
163
+ it "inyecta severity_number=5 para DEBUG" do
164
+ result = JSON.parse(formatter.call("DEBUG", timestamp, progname, "event=x"))
165
+
166
+ expect(result["severity_number"]).to eq(5)
167
+ end
168
+
169
+ it "inyecta severity_number=21 para FATAL" do
170
+ result = JSON.parse(formatter.call("FATAL", timestamp, progname, "event=x"))
171
+
172
+ expect(result["severity_number"]).to eq(21)
173
+ end
174
+
175
+ it "omite severity_number para niveles no estándar" do
176
+ result = JSON.parse(formatter.call("CUSTOM", timestamp, progname, "event=x"))
177
+
178
+ expect(result).not_to have_key("severity_number")
179
+ end
180
+ end
181
+
182
+ describe "resource attributes OTel (service_version, deployment_environment)" do
183
+ it "inyecta service_version cuando Configuration lo provee" do
184
+ allow(ExisRay.configuration).to receive_messages(service_version: "1.2.3", deployment_environment: nil)
185
+
186
+ result = call("event=boot")
187
+
188
+ expect(result["service_version"]).to eq("1.2.3")
189
+ end
190
+
191
+ it "inyecta deployment_environment cuando Configuration lo provee" do
192
+ allow(ExisRay.configuration).to receive_messages(service_version: nil, deployment_environment: "production")
193
+
194
+ result = call("event=boot")
195
+
196
+ expect(result["deployment_environment"]).to eq("production")
197
+ end
198
+
199
+ it "omite service_version cuando es nil" do
200
+ allow(ExisRay.configuration).to receive_messages(service_version: nil, deployment_environment: "test")
201
+
202
+ result = call("event=boot")
203
+
204
+ expect(result).not_to have_key("service_version")
205
+ end
206
+
207
+ it "omite deployment_environment cuando es nil" do
208
+ allow(ExisRay.configuration).to receive_messages(service_version: "1.0", deployment_environment: nil)
209
+
210
+ result = call("event=boot")
211
+
212
+ expect(result).not_to have_key("deployment_environment")
213
+ end
214
+ end
215
+
144
216
  describe "inyeccion de contexto de negocio" do
145
217
  let(:current_mock) do
146
218
  Class.new do