exis_ray 0.2.0 → 0.3.1

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: 72c313e5194f8252623ac026d98ab510c8ad50c7f4b7c7d8f362005c14d6d3e5
4
- data.tar.gz: 51b8c985ab03bff4389784477e9c5eb5f27e81138766e50356414b2e88121b68
3
+ metadata.gz: abb2008b31a7c76eea82b5f2375f973be40e038e3cc5276233aff2691ca315d3
4
+ data.tar.gz: e5821f58363bc7c6c312bfe391e4f93ff7a50097eff8b62430dc60544eeaa4ed
5
5
  SHA512:
6
- metadata.gz: 5bd0a72ee15676c2074306af62a71098230b30831f49d9b678bb02dd853528a687db9e2d354d0fa34f3bcf758e0bf63fbd5512a949963e256528817749163fda
7
- data.tar.gz: 485e39a4b8bf41a01a099d5a75c472f77683ac4a98bb1dcfa109e9abd16be0532930f78bebfee0a94beeb2a725cbae0a3696eabafaa070b0aae2a4cd24b13937
6
+ metadata.gz: f4629e46a37d5796585131503d80a60fbcfe227ef0d6f4b1b9cafc0e34c139d7cd3dcce526c68812c579759ac042f0cb32bde13c7ae382f7539fd6e50c78dc42
7
+ data.tar.gz: e6d71b9c66df65e7a8e77fb6da4353e3e363fb2a6b1aaac47e265b9a22b818516b0c12163e1939433d603f8ea91ec941a8b7669e120b29cae91182b5357f62f1
data/CHANGELOG.md CHANGED
@@ -1,4 +1,17 @@
1
- ## [Unreleased]
1
+ ## [0.3.1] - 2026-03-23
2
+
3
+ ### Changed
4
+ - **Standardized Service Name:** The `service` field in logs now always returns the Rails application name in `snake_case` (e.g., `cold_storage_service`).
5
+ - Removed dynamic service name overrides in `HttpMiddleware`, `Sidekiq::ServerMiddleware`, and `TaskMonitor`.
6
+ - **Enhanced Job/Task Visibility:** Added `sidekiq_job` and `task` fields to the JSON logs, providing specific context without overloading the `service` field.
7
+
8
+ ## [0.3.0] - 2026-03-23
9
+
10
+ ### Added
11
+ - **KV String Parser in JsonFormatter:** The `JsonFormatter` now automatically detects and parses messages in `key=value` format.
12
+ - Extracted pairs from strings are elevated to the JSON root, allowing structured logging from plain string messages.
13
+ - Support for quoted values with spaces (e.g., `message="some text"`) and escaped characters within logs.
14
+ - Added comprehensive RSpec suite for `ExisRay::JsonFormatter` to verify message processing logic.
2
15
 
3
16
  ## [0.2.0] - 2026-03-12
4
17
 
data/README.md CHANGED
@@ -250,6 +250,7 @@ end
250
250
  * **`ExisRay::Current`**: The business layer. Manages domain identity (`User`, `ISP`).
251
251
  * **`ExisRay::Reporter`**: The observability layer. Bridges the gap between your app and Sentry.
252
252
  * **`ExisRay::JsonFormatter`**: The central logging engine. Intercepts HTTP, Sidekiq, and Tasks to output clean JSON.
253
+ * **KV String Parser:** It automatically detects if a log message (String) uses `key=value` format. If so, it parses the pairs and elevates them to the root of the JSON. For example, `Rails.logger.info "event=boot status=ok"` becomes `{"event":"boot","status":"ok",...}`. It supports quoted values with spaces: `message="something went wrong"`.
253
254
  * **`ExisRay::TaskMonitor`**: The entry point for non-HTTP processes.
254
255
 
255
256
  ## License
data/Rakefile CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rubocop/rake_task"
5
+ require "rspec/core/rake_task"
5
6
 
6
7
  RuboCop::RakeTask.new
8
+ RSpec::Core::RakeTask.new(:spec)
7
9
 
8
- task default: :rubocop
10
+ task default: %i[spec rubocop]
@@ -4,13 +4,11 @@ module ExisRay
4
4
  class HttpMiddleware
5
5
  def initialize(app)
6
6
  @app = app
7
- @base_service_name = defined?(Rails) ? Rails.application.class.module_parent_name : 'App'
8
7
  end
9
8
 
10
9
  def call(env)
11
10
  # 1. Hidratar Infraestructura
12
- ExisRay::Tracer.created_at = Time.now.utc.to_f
13
- ExisRay::Tracer.service_name = "#{@base_service_name}-HTTP"
11
+ ExisRay::Tracer.created_at = Time.now.utc.to_f
14
12
 
15
13
  trace_header_key = ExisRay.configuration.trace_header
16
14
 
@@ -53,6 +53,8 @@ module ExisRay
53
53
 
54
54
  payload[:root_id] = ExisRay::Tracer.root_id
55
55
  payload[:trace_id] = ExisRay::Tracer.trace_id if ExisRay::Tracer.trace_id
56
+ payload[:sidekiq_job] = ExisRay::Tracer.sidekiq_job if ExisRay::Tracer.sidekiq_job
57
+ payload[:task] = ExisRay::Tracer.task if ExisRay::Tracer.task
56
58
  end
57
59
 
58
60
  # Inyecta el contexto de negocio (ID de usuario, ISP, ID de correlación) en el payload.
@@ -84,7 +86,10 @@ module ExisRay
84
86
  # Procesa el cuerpo del mensaje recibido y lo fusiona con el payload.
85
87
  #
86
88
  # Si el mensaje es un `Hash` (como el que nos pasará Lograge para peticiones HTTP),
87
- # se hace un merge directo. Si es texto plano u otro objeto, se asigna a la clave `:message`.
89
+ # se hace un merge directo. Si es un String con formato key=value (ej: "event=foo bar=baz"),
90
+ # se parsea y los campos se elevan al nivel raíz del JSON. Valores con espacios deben estar
91
+ # entre comillas (ej: message="algo salió mal"). Si el String no sigue ese formato, se asigna
92
+ # a la clave `:message`.
88
93
  #
89
94
  # @param payload [Hash] El diccionario base del log.
90
95
  # @param msg [String, Hash, Object] El mensaje original recibido por el logger.
@@ -92,9 +97,33 @@ module ExisRay
92
97
  def process_message(payload, msg)
93
98
  if msg.is_a?(Hash)
94
99
  payload.merge!(msg)
100
+ elsif msg.is_a?(String) && kv_string?(msg)
101
+ payload.merge!(parse_kv_string(msg))
95
102
  else
96
103
  payload[:message] = msg.to_s
97
104
  end
98
105
  end
106
+
107
+ # Determina si un string tiene formato key=value.
108
+ # Considera que hay formato kv si el string comienza con `word=`.
109
+ #
110
+ # @param str [String]
111
+ # @return [Boolean]
112
+ def kv_string?(str)
113
+ str.match?(/\A\w+=/)
114
+ end
115
+
116
+ # Parsea un string con formato key=value y retorna un Hash.
117
+ # Soporta valores con espacios si están entre comillas dobles (ej: message="algo salió mal").
118
+ #
119
+ # @param str [String]
120
+ # @return [Hash]
121
+ def parse_kv_string(str)
122
+ result = {}
123
+ str.scan(/(\w+)=("(?:[^"\\]|\\.)*"|\S+)/) do |key, value|
124
+ result[key.to_sym] = value.start_with?('"') ? value[1..-2].gsub('\\"', '"') : value
125
+ end
126
+ result
127
+ end
99
128
  end
100
129
  end
@@ -45,7 +45,7 @@ module ExisRay
45
45
  # @return [void]
46
46
  def hydrate_tracer(worker, job)
47
47
  ExisRay::Tracer.created_at = Time.now.utc.to_f
48
- ExisRay::Tracer.service_name = "Sidekiq-#{worker.class.name}"
48
+ ExisRay::Tracer.sidekiq_job = worker.class.name
49
49
 
50
50
  if job["exis_ray_trace"]
51
51
  # Continuidad: Usamos la traza propagada desde el cliente (Web/Cron)
@@ -52,7 +52,7 @@ module ExisRay
52
52
  # @param task_name [String, Symbol] El nombre de la tarea en ejecución.
53
53
  # @return [void]
54
54
  def self.setup_tracer(task_name)
55
- ExisRay::Tracer.service_name = task_name.to_s.tr(":", "-").camelize
55
+ ExisRay::Tracer.task = task_name.to_s
56
56
  ExisRay::Tracer.request_id = SecureRandom.uuid
57
57
  ExisRay::Tracer.created_at = Time.now.utc.to_f
58
58
 
@@ -10,14 +10,14 @@ module ExisRay
10
10
  #
11
11
  # @see https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html Documentación de AWS X-Ray
12
12
  class Tracer < ActiveSupport::CurrentAttributes
13
- attribute :trace_id, :request_id, :root_id, :self_id, :called_from, :total_time_so_far, :created_at, :service_name
13
+ attribute :trace_id, :request_id, :root_id, :self_id, :called_from, :total_time_so_far, :created_at, :sidekiq_job, :task
14
14
 
15
- # Devuelve el nombre del servicio actual.
16
- # Si no se ha definido manualmente, hace fallback al nombre de la aplicación Rails.
15
+ # Devuelve el nombre de la aplicación en snake_case (ej: "cold_storage_service").
16
+ # Se utiliza como identificador global del servicio en logs y trazabilidad.
17
17
  #
18
- # @return [String] El nombre del servicio (ej: "Wispro", "Wispro-Worker", "App").
18
+ # @return [String]
19
19
  def self.service_name
20
- super || (defined?(Rails) ? Rails.application.class.module_parent_name : 'App')
20
+ @service_name ||= (defined?(Rails) ? Rails.application.class.module_parent_name.underscore : "app")
21
21
  end
22
22
 
23
23
  # Genera un ID de correlación compuesto, útil para logs y auditoría.
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ExisRay
4
4
  # Versión actual de la gema.
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.1"
6
6
  end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe ExisRay::JsonFormatter do
6
+ subject(:formatter) { described_class.new }
7
+
8
+ let(:severity) { "INFO" }
9
+ let(:timestamp) { Time.utc(2025, 9, 1, 12, 0, 0) }
10
+ let(:progname) { nil }
11
+
12
+ before do
13
+ # Stub Tracer y Current para aislar el formatter de dependencias externas
14
+ stub_const("ExisRay::Tracer", Module.new do
15
+ def self.service_name = "test-service"
16
+ def self.root_id = nil
17
+ def self.trace_id = nil
18
+ end)
19
+
20
+ allow(ExisRay).to receive(:current_class).and_return(nil)
21
+
22
+ # current_tags es un método de ActiveSupport::TaggedLogging::Formatter que depende
23
+ # de IsolatedExecutionState (thread-local). Lo stubeamos para entornos sin Rails.
24
+ allow(formatter).to receive(:current_tags).and_return([])
25
+ end
26
+
27
+ def call(msg)
28
+ JSON.parse(formatter.call(severity, timestamp, progname, msg))
29
+ end
30
+
31
+ describe "#call" do
32
+ context "cuando el mensaje es un Hash" do
33
+ it "eleva los campos al nivel raíz del JSON" do
34
+ result = call({ event: "archive_lookup", cutoff: "2025-09-01" })
35
+
36
+ expect(result).to include(
37
+ "event" => "archive_lookup",
38
+ "cutoff" => "2025-09-01",
39
+ "level" => "INFO",
40
+ "service" => "test-service"
41
+ )
42
+ expect(result).not_to have_key("message")
43
+ end
44
+
45
+ it "convierte claves symbol y string indistintamente" do
46
+ result = call({ "event" => "foo", bar: "baz" })
47
+
48
+ expect(result).to include("event" => "foo", "bar" => "baz")
49
+ end
50
+ end
51
+
52
+ context "cuando el mensaje es un String con formato key=value" do
53
+ it "parsea los pares y los eleva al nivel raíz" do
54
+ result = call("event=archive_lookup cutoff=2025-09-01")
55
+
56
+ expect(result).to include(
57
+ "event" => "archive_lookup",
58
+ "cutoff" => "2025-09-01",
59
+ "level" => "INFO",
60
+ "service" => "test-service"
61
+ )
62
+ expect(result).not_to have_key("message")
63
+ end
64
+
65
+ it "soporta un único par key=value" do
66
+ result = call("event=boot")
67
+
68
+ expect(result["event"]).to eq("event=boot".split("=").last)
69
+ expect(result).not_to have_key("message")
70
+ end
71
+
72
+ it "soporta valores con espacios entre comillas dobles" do
73
+ result = call('event=error message="algo salió mal"')
74
+
75
+ expect(result["event"]).to eq("error")
76
+ expect(result["message"]).to eq("algo salió mal")
77
+ end
78
+
79
+ it "soporta comillas escapadas dentro del valor" do
80
+ result = call('msg="dijo \"hola\""')
81
+
82
+ expect(result["msg"]).to eq('dijo "hola"')
83
+ end
84
+
85
+ it "soporta múltiples pares con tipos de valor variados" do
86
+ result = call("component=orders event=invoice_generated duration_ms=42.5 retries=0")
87
+
88
+ expect(result).to include(
89
+ "component" => "orders",
90
+ "event" => "invoice_generated",
91
+ "duration_ms" => "42.5",
92
+ "retries" => "0"
93
+ )
94
+ end
95
+ end
96
+
97
+ context "cuando el mensaje es un String libre (sin formato key=value)" do
98
+ it "asigna el string completo al campo message" do
99
+ result = call("algo salió mal")
100
+
101
+ expect(result["message"]).to eq("algo salió mal")
102
+ expect(result).to include("level" => "INFO", "service" => "test-service")
103
+ end
104
+
105
+ it "asigna un string vacío al campo message" do
106
+ result = call("")
107
+
108
+ expect(result["message"]).to eq("")
109
+ end
110
+
111
+ it "convierte objetos arbitrarios a string via to_s" do
112
+ result = call(42)
113
+
114
+ expect(result["message"]).to eq("42")
115
+ end
116
+ end
117
+
118
+ describe "campos base" do
119
+ it "incluye time en formato ISO8601 UTC" do
120
+ result = call("event=boot")
121
+
122
+ expect(result["time"]).to eq("2025-09-01T12:00:00Z")
123
+ end
124
+
125
+ it "incluye level y service" do
126
+ result = call("event=boot")
127
+
128
+ expect(result["level"]).to eq("INFO")
129
+ expect(result["service"]).to eq("test-service")
130
+ end
131
+ end
132
+ end
133
+
134
+ describe "#kv_string? (privado)" do
135
+ it "retorna true si el string empieza con word=" do
136
+ expect(formatter.send(:kv_string?, "event=foo")).to be true
137
+ end
138
+
139
+ it "retorna false para strings libres" do
140
+ expect(formatter.send(:kv_string?, "algo salió mal")).to be false
141
+ expect(formatter.send(:kv_string?, "")).to be false
142
+ expect(formatter.send(:kv_string?, "=sinkey")).to be false
143
+ end
144
+ end
145
+
146
+ describe "#parse_kv_string (privado)" do
147
+ it "retorna un hash con claves symbol" do
148
+ result = formatter.send(:parse_kv_string, "a=1 b=2")
149
+
150
+ expect(result).to eq({ a: "1", b: "2" })
151
+ end
152
+
153
+ it "desenvuelve las comillas dobles de los valores" do
154
+ result = formatter.send(:parse_kv_string, 'msg="hello world"')
155
+
156
+ expect(result).to eq({ msg: "hello world" })
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "json"
5
+ require "active_support/tagged_logging"
6
+ require "exis_ray/json_formatter"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exis_ray
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabriel Edera
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-12 00:00:00.000000000 Z
11
+ date: 2026-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -78,6 +78,8 @@ files:
78
78
  - lib/exis_ray/tracer.rb
79
79
  - lib/exis_ray/version.rb
80
80
  - sig/exis_ray.rbs
81
+ - spec/exis_ray/json_formatter_spec.rb
82
+ - spec/spec_helper.rb
81
83
  homepage: https://github.com/gedera/exis_ray
82
84
  licenses:
83
85
  - MIT