logbrew-sdk 0.1.2 → 0.1.4

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.
data/lib/logbrew.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
3
4
  require "json"
4
5
  require "logger"
5
6
  require "net/http"
@@ -8,6 +9,8 @@ require "time"
8
9
  require "timeout"
9
10
  require "uri"
10
11
 
12
+ require_relative "logbrew/version"
13
+
11
14
  module LogBrew
12
15
  SEVERITY_VALUES = %w[trace debug info warn warning error fatal critical].freeze
13
16
  SEVERITY_ALIASES = {
@@ -39,6 +42,11 @@ module LogBrew
39
42
  end
40
43
  end
41
44
 
45
+ require_relative "logbrew/telemetry_context_value"
46
+ require_relative "logbrew/telemetry_resource"
47
+ require_relative "logbrew/telemetry_context"
48
+ require_relative "logbrew/telemetry"
49
+
42
50
  require_relative "logbrew/event_batcher"
43
51
  require_relative "logbrew/persistent_event_store"
44
52
  require_relative "logbrew/bounded_event_queue"
@@ -369,6 +377,7 @@ module LogBrew
369
377
  event_id_prefix: DEFAULT_EVENT_ID_PREFIX,
370
378
  metadata: nil,
371
379
  timestamp_provider: nil,
380
+ include_exception_message: true,
372
381
  include_exception_backtrace: false,
373
382
  on_error: nil,
374
383
  raise_errors: false
@@ -384,10 +393,12 @@ module LogBrew
384
393
  @event_id_prefix = event_id_prefix
385
394
  @metadata = metadata || {}
386
395
  @timestamp_provider = timestamp_provider
396
+ @include_exception_message = include_exception_message
387
397
  @include_exception_backtrace = include_exception_backtrace
388
398
  @on_error = on_error
389
399
  @raise_errors = raise_errors
390
400
  @next_event_number = 0
401
+ @event_id_mutex = Mutex.new
391
402
  end
392
403
 
393
404
  def call(env)
@@ -428,19 +439,21 @@ module LogBrew
428
439
  end
429
440
 
430
441
  def capture_exception_issue(env, error)
431
- @client.issue(
432
- next_event_id("issue"),
433
- logbrew_timestamp,
434
- title: error.class.name,
435
- level: "error",
436
- message: error.message,
442
+ attributes = IssueDiagnostics.from_exception(
443
+ error,
444
+ message: @include_exception_message ? error.message : nil,
445
+ mechanism_type: exception_mechanism_type,
446
+ handled: false,
437
447
  metadata: exception_metadata(env, error)
438
448
  )
449
+ @client.issue(next_event_id("issue"), logbrew_timestamp, attributes)
439
450
  end
440
451
 
441
452
  def next_event_id(kind)
442
- @next_event_number += 1
443
- "#{@event_id_prefix}_#{kind}_#{@next_event_number}"
453
+ @event_id_mutex.synchronize do
454
+ @next_event_number += 1
455
+ "#{@event_id_prefix}_#{kind}_#{@next_event_number}"
456
+ end
444
457
  end
445
458
 
446
459
  def logbrew_timestamp
@@ -491,12 +504,39 @@ module LogBrew
491
504
 
492
505
  def exception_metadata(env, error)
493
506
  request_metadata(env, 500).tap do |metadata|
494
- metadata["exceptionType"] = error.class.name
495
- metadata["exceptionMessage"] = error.message
507
+ exception_type = IssueDiagnostics.safe_exception_type(error)
508
+ frames = IssueDiagnostics.stack_frames_from_exception(error)
509
+ first_frame = frames.first
510
+ metadata["exceptionType"] = exception_type
511
+ metadata["errorName"] = exception_type
512
+ metadata["handled"] = false
513
+ metadata["mechanism"] = exception_mechanism_type
514
+ metadata["issueGroupingKey"] = exception_grouping_key(env, exception_type, first_frame)
515
+ metadata["issueGroupingSource"] = "exception_type_route_file"
516
+ unless first_frame.nil?
517
+ metadata["errorFrameFile"] = first_frame.fetch("filename")
518
+ metadata["errorFrameLine"] = first_frame.fetch("line")
519
+ end
520
+ metadata["exceptionMessage"] = error.message if @include_exception_message
496
521
  metadata["exceptionBacktrace"] = error.backtrace.join("\n") if @include_exception_backtrace && error.backtrace
497
522
  end
498
523
  end
499
524
 
525
+ def exception_mechanism_type
526
+ "rack.middleware"
527
+ end
528
+
529
+ def exception_grouping_prefix
530
+ "rack-exception"
531
+ end
532
+
533
+ def exception_grouping_key(env, exception_type, first_frame)
534
+ route = request_path(env)
535
+ frame_file = first_frame.nil? ? "" : first_frame.fetch("filename")
536
+ material = [exception_type, route, frame_file].join("\n")
537
+ "#{exception_grouping_prefix}-#{Digest::SHA256.hexdigest(material)}"
538
+ end
539
+
500
540
  def rack_status(response)
501
541
  return response[0].to_i if response.respond_to?(:[]) && !response[0].nil?
502
542
 
@@ -573,6 +613,7 @@ module LogBrew
573
613
  event_id_prefix: DEFAULT_EVENT_ID_PREFIX,
574
614
  metadata: nil,
575
615
  timestamp_provider: nil,
616
+ include_exception_message: true,
576
617
  include_exception_backtrace: false,
577
618
  on_error: nil,
578
619
  raise_errors: false
@@ -586,23 +627,36 @@ module LogBrew
586
627
  @event_id_prefix = event_id_prefix
587
628
  @metadata = metadata || {}
588
629
  @timestamp_provider = timestamp_provider
630
+ @include_exception_message = include_exception_message
589
631
  @include_exception_backtrace = include_exception_backtrace
590
632
  @on_error = on_error
591
633
  @raise_errors = raise_errors
592
634
  @next_event_number = 0
635
+ @event_id_mutex = Mutex.new
593
636
  end
594
637
 
595
638
  def report(error, handled: true, severity: :error, context: nil, source: nil, **_options)
596
639
  capture_safely do
597
- @next_event_number += 1
598
- @client.issue(
599
- "#{@event_id_prefix}_#{@next_event_number}",
600
- logbrew_timestamp,
601
- title: error_title(error),
602
- level: issue_level(severity),
603
- message: error_message(error),
604
- metadata: rails_metadata(error, handled, severity, context, source)
605
- )
640
+ attributes = if error.is_a?(Exception)
641
+ IssueDiagnostics.from_exception(
642
+ error,
643
+ title: error_title(error),
644
+ level: issue_level(severity),
645
+ message: @include_exception_message ? error_message(error) : nil,
646
+ mechanism_type: "rails.error_reporter",
647
+ handled: handled,
648
+ metadata: rails_metadata(error, handled, severity, context, source)
649
+ )
650
+ else
651
+ {
652
+ title: error_title(error),
653
+ level: issue_level(severity),
654
+ metadata: rails_metadata(error, handled, severity, context, source)
655
+ }.tap do |fallback|
656
+ fallback[:message] = error_message(error) if @include_exception_message
657
+ end
658
+ end
659
+ @client.issue(next_event_id, logbrew_timestamp, attributes)
606
660
  flush_if_configured
607
661
  end
608
662
  end
@@ -629,6 +683,13 @@ module LogBrew
629
683
  error.inspect
630
684
  end
631
685
 
686
+ def next_event_id
687
+ @event_id_mutex.synchronize do
688
+ @next_event_number += 1
689
+ "#{@event_id_prefix}_#{@next_event_number}"
690
+ end
691
+ end
692
+
632
693
  def issue_level(severity)
633
694
  SEVERITY_TO_ISSUE_LEVEL.fetch(severity.to_s, "error")
634
695
  end
@@ -654,11 +715,30 @@ module LogBrew
654
715
  end
655
716
 
656
717
  def add_exception_metadata(metadata, exception)
657
- metadata["exceptionType"] = exception.class.name
658
- metadata["exceptionMessage"] = exception.message
718
+ exception_type = IssueDiagnostics.safe_exception_type(exception)
719
+ frames = IssueDiagnostics.stack_frames_from_exception(exception)
720
+ first_frame = frames.first
721
+ metadata["exceptionType"] = exception_type
722
+ metadata["errorName"] = exception_type
723
+ metadata["handled"] = metadata.fetch("rails.handled")
724
+ metadata["mechanism"] = "rails.error_reporter"
725
+ metadata["issueGroupingKey"] = rails_error_grouping_key(exception_type, metadata, first_frame)
726
+ metadata["issueGroupingSource"] = "exception_type_source_file"
727
+ unless first_frame.nil?
728
+ metadata["errorFrameFile"] = first_frame.fetch("filename")
729
+ metadata["errorFrameLine"] = first_frame.fetch("line")
730
+ end
731
+ metadata["exceptionMessage"] = exception.message if @include_exception_message
659
732
  metadata["exceptionBacktrace"] = exception.backtrace.join("\n") if @include_exception_backtrace && exception.backtrace
660
733
  end
661
734
 
735
+ def rails_error_grouping_key(exception_type, metadata, first_frame)
736
+ source = metadata.fetch("rails.source", "rails")
737
+ frame_file = first_frame.nil? ? "" : first_frame.fetch("filename")
738
+ material = [exception_type, source, frame_file].join("\n")
739
+ "rails-exception-#{Digest::SHA256.hexdigest(material)}"
740
+ end
741
+
662
742
  def copy_metadata(metadata)
663
743
  metadata.each_with_object({}) do |(key, value), copied|
664
744
  copied[key.to_s] = value if primitive_metadata_value?(value)
@@ -745,6 +825,26 @@ module LogBrew
745
825
  value
746
826
  end
747
827
 
828
+ def normalize_metric_description(value)
829
+ unless value.is_a?(String) && value.valid_encoding?
830
+ raise SdkError.new(
831
+ "validation_error",
832
+ "metric description must be a non-blank string of at most 1024 non-control characters"
833
+ )
834
+ end
835
+ normalized = value.strip
836
+ invalid = normalized.each_codepoint.any? do |character|
837
+ character <= 0x1f || (character >= 0x7f && character <= 0x9f) || character == 0x2028 || character == 0x2029
838
+ end
839
+ if normalized.empty? || normalized.each_codepoint.count > 1024 || invalid
840
+ raise SdkError.new(
841
+ "validation_error",
842
+ "metric description must be a non-blank string of at most 1024 non-control characters"
843
+ )
844
+ end
845
+ normalized
846
+ end
847
+
748
848
  def read(attributes, key)
749
849
  return nil unless attributes.is_a?(Hash)
750
850
 
@@ -776,12 +876,26 @@ module LogBrew
776
876
  on_event_dropped: nil,
777
877
  max_batch_size: EventBatcher::DEFAULT_MAX_SIZE,
778
878
  max_batch_bytes: EventBatcher::DEFAULT_MAX_BYTES,
779
- persistent_queue_path: nil
879
+ persistent_queue_path: nil,
880
+ context: nil,
881
+ capture_runtime_context: true
780
882
  )
781
883
  Validation.require_non_empty("api_key", api_key)
782
884
  Validation.require_non_empty("sdk_name", sdk_name)
783
885
  Validation.require_non_empty("sdk_version", sdk_version)
784
886
  raise SdkError.new("validation_error", "max_retries must be non-negative") if max_retries.negative?
887
+ unless context.nil? || context.is_a?(TelemetryContext)
888
+ raise SdkError.new("validation_error", "client context must be a LogBrew::TelemetryContext")
889
+ end
890
+ unless capture_runtime_context == true || capture_runtime_context == false
891
+ raise SdkError.new("validation_error", "capture_runtime_context must be a boolean")
892
+ end
893
+
894
+ resolved_context = if capture_runtime_context
895
+ TelemetryContext.merge(TelemetryContext.runtime_defaults, context)
896
+ else
897
+ TelemetryContext.merge(nil, context)
898
+ end
785
899
 
786
900
  new(
787
901
  api_key: api_key,
@@ -796,7 +910,8 @@ module LogBrew
796
910
  on_event_dropped: on_event_dropped,
797
911
  max_batch_size: max_batch_size,
798
912
  max_batch_bytes: max_batch_bytes,
799
- persistent_queue_path: persistent_queue_path
913
+ persistent_queue_path: persistent_queue_path,
914
+ context: resolved_context
800
915
  )
801
916
  end
802
917
 
@@ -846,10 +961,12 @@ module LogBrew
846
961
  on_event_dropped: nil,
847
962
  max_batch_size: EventBatcher::DEFAULT_MAX_SIZE,
848
963
  max_batch_bytes: EventBatcher::DEFAULT_MAX_BYTES,
849
- persistent_queue_path: nil
964
+ persistent_queue_path: nil,
965
+ context: nil
850
966
  )
851
967
  @api_key = api_key
852
968
  @sdk = sdk
969
+ @context = context
853
970
  @max_retries = max_retries
854
971
  @event_batcher = EventBatcher.new(sdk: sdk, max_size: max_batch_size, max_bytes: max_batch_bytes)
855
972
  event_store = nil
@@ -940,31 +1057,31 @@ module LogBrew
940
1057
  end
941
1058
 
942
1059
  def release(id, timestamp, attributes)
943
- push_event("release", id, timestamp, validate_release(attributes))
1060
+ push_event("release", id, timestamp, validate_release(attributes), event_context(attributes))
944
1061
  end
945
1062
 
946
1063
  def environment(id, timestamp, attributes)
947
- push_event("environment", id, timestamp, validate_environment(attributes))
1064
+ push_event("environment", id, timestamp, validate_environment(attributes), event_context(attributes))
948
1065
  end
949
1066
 
950
1067
  def issue(id, timestamp, attributes)
951
- push_event("issue", id, timestamp, validate_issue(attributes))
1068
+ push_event("issue", id, timestamp, validate_issue(attributes), event_context(attributes))
952
1069
  end
953
1070
 
954
1071
  def log(id, timestamp, attributes)
955
- push_event("log", id, timestamp, validate_log(attributes))
1072
+ push_event("log", id, timestamp, validate_log(attributes), event_context(attributes))
956
1073
  end
957
1074
 
958
1075
  def span(id, timestamp, attributes)
959
- push_event("span", id, timestamp, validate_span(attributes))
1076
+ push_event("span", id, timestamp, validate_span(attributes), event_context(attributes))
960
1077
  end
961
1078
 
962
1079
  def metric(id, timestamp, attributes)
963
- push_event("metric", id, timestamp, validate_metric(attributes))
1080
+ push_event("metric", id, timestamp, validate_metric(attributes), event_context(attributes))
964
1081
  end
965
1082
 
966
1083
  def action(id, timestamp, attributes)
967
- push_event("action", id, timestamp, validate_action(attributes))
1084
+ push_event("action", id, timestamp, validate_action(attributes), event_context(attributes))
968
1085
  end
969
1086
 
970
1087
  def flush(transport = nil)
@@ -1021,10 +1138,18 @@ module LogBrew
1021
1138
 
1022
1139
  private
1023
1140
 
1024
- def push_event(type, id, timestamp, attributes)
1141
+ def push_event(type, id, timestamp, attributes, event_context)
1025
1142
  @automatic_delivery&.assert_process_ownership!
1026
1143
  Validation.require_non_empty("event id", id)
1027
1144
  Validation.require_timestamp(timestamp)
1145
+ ambient_context = Telemetry.current_context
1146
+ active_trace = defined?(LogBrew::Trace) ? LogBrew::Trace.current : nil
1147
+ ambient_context = TelemetryContext.with_trace(ambient_context, active_trace) unless active_trace.nil?
1148
+ merged_context = TelemetryContext.merge(
1149
+ TelemetryContext.merge(@context, ambient_context),
1150
+ event_context
1151
+ )
1152
+ attributes["context"] = merged_context.to_h unless merged_context.nil?
1028
1153
  event = {
1029
1154
  "type" => type,
1030
1155
  "timestamp" => timestamp,
@@ -1203,20 +1328,7 @@ module LogBrew
1203
1328
  end
1204
1329
 
1205
1330
  def validate_issue(attributes)
1206
- title = Validation.read(attributes, "title")
1207
- level = Validation.read(attributes, "level")
1208
- Validation.require_non_empty("issue title", title)
1209
- level = normalize_severity("issue level", level)
1210
- with_metadata(
1211
- {
1212
- "title" => title,
1213
- "level" => level
1214
- }.tap do |payload|
1215
- message = Validation.read(attributes, "message")
1216
- payload["message"] = message unless message.nil?
1217
- end,
1218
- attributes
1219
- )
1331
+ IssueDiagnostics.validate_issue_attributes(attributes)
1220
1332
  end
1221
1333
 
1222
1334
  def validate_log(attributes)
@@ -1276,6 +1388,8 @@ module LogBrew
1276
1388
 
1277
1389
  def validate_metric(attributes)
1278
1390
  name = Validation.read(attributes, "name")
1391
+ description_present = attributes.is_a?(Hash) && (attributes.key?("description") || attributes.key?(:description))
1392
+ description = Validation.normalize_metric_description(Validation.read(attributes, "description")) if description_present
1279
1393
  kind = Validation.read(attributes, "kind")
1280
1394
  unit = Validation.read(attributes, "unit")
1281
1395
  temporality = Validation.read(attributes, "temporality")
@@ -1291,11 +1405,12 @@ module LogBrew
1291
1405
  with_metadata(
1292
1406
  {
1293
1407
  "name" => name,
1408
+ "description" => description,
1294
1409
  "kind" => kind,
1295
1410
  "value" => value,
1296
1411
  "unit" => unit,
1297
1412
  "temporality" => temporality
1298
- },
1413
+ }.compact,
1299
1414
  attributes
1300
1415
  )
1301
1416
  end
@@ -1308,6 +1423,23 @@ module LogBrew
1308
1423
  with_metadata({ "name" => name, "status" => status }, attributes)
1309
1424
  end
1310
1425
 
1426
+ def event_context(attributes)
1427
+ return nil unless attributes.is_a?(Hash)
1428
+
1429
+ string_key = attributes.key?("context")
1430
+ symbol_key = attributes.key?(:context)
1431
+ if string_key && symbol_key
1432
+ raise SdkError.new("validation_error", "event context must use one context field")
1433
+ end
1434
+ return nil unless string_key || symbol_key
1435
+
1436
+ value = string_key ? attributes["context"] : attributes[:context]
1437
+ unless value.is_a?(TelemetryContext)
1438
+ raise SdkError.new("validation_error", "event context must be a LogBrew::TelemetryContext")
1439
+ end
1440
+ value
1441
+ end
1442
+
1311
1443
  def with_metadata(payload, attributes)
1312
1444
  metadata = Validation.require_metadata(Validation.read(attributes, "metadata"))
1313
1445
  payload["metadata"] = metadata unless metadata.nil?
@@ -1318,6 +1450,6 @@ module LogBrew
1318
1450
  require_relative "logbrew/automatic_delivery"
1319
1451
  end
1320
1452
 
1321
- %w[product_timeline traceparent trace span_events operation_tracing http_client_tracing support_ticket worker_lifecycle].each do |path|
1453
+ %w[issue_diagnostics product_timeline traceparent trace span_events operation_tracing http_client_tracing support_ticket worker_lifecycle].each do |path|
1322
1454
  require_relative "logbrew/#{path}"
1323
1455
  end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logbrew-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - LogBrew
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-23 00:00:00.000000000 Z
11
+ date: 2026-08-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Public LogBrew Ruby SDK for building, validating, and flushing event
14
- batches.
13
+ description: Public LogBrew Ruby SDK with typed issue diagnostics, automatic Rails
14
+ request/error capture, and standard-library delivery.
15
15
  email:
16
16
  executables: []
17
17
  extensions: []
@@ -22,24 +22,34 @@ files:
22
22
  - examples/automatic_delivery.rb
23
23
  - examples/first_useful_telemetry.rb
24
24
  - examples/http_trace_correlation.rb
25
+ - examples/issue_diagnostics.rb
25
26
  - examples/persistent_worker_delivery.rb
26
27
  - examples/readme_example.rb
27
28
  - examples/real_user_smoke.rb
28
29
  - examples/sidekiq_tracing.rb
30
+ - lib/logbrew-sdk.rb
29
31
  - lib/logbrew.rb
30
32
  - lib/logbrew/automatic_delivery.rb
31
33
  - lib/logbrew/bounded_event_queue.rb
32
34
  - lib/logbrew/event_batcher.rb
33
35
  - lib/logbrew/faraday_tracing.rb
34
36
  - lib/logbrew/http_client_tracing.rb
37
+ - lib/logbrew/issue_diagnostics.rb
35
38
  - lib/logbrew/operation_tracing.rb
36
39
  - lib/logbrew/persistent_event_store.rb
37
40
  - lib/logbrew/product_timeline.rb
41
+ - lib/logbrew/rails.rb
42
+ - lib/logbrew/rails_integration.rb
38
43
  - lib/logbrew/sidekiq.rb
39
44
  - lib/logbrew/span_events.rb
40
45
  - lib/logbrew/support_ticket.rb
46
+ - lib/logbrew/telemetry.rb
47
+ - lib/logbrew/telemetry_context.rb
48
+ - lib/logbrew/telemetry_context_value.rb
49
+ - lib/logbrew/telemetry_resource.rb
41
50
  - lib/logbrew/trace.rb
42
51
  - lib/logbrew/traceparent.rb
52
+ - lib/logbrew/version.rb
43
53
  - lib/logbrew/worker_lifecycle.rb
44
54
  homepage: https://github.com/LogBrewCo/sdk
45
55
  licenses: