logbrew-sdk 0.1.3 → 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"
@@ -41,6 +42,11 @@ module LogBrew
41
42
  end
42
43
  end
43
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
+
44
50
  require_relative "logbrew/event_batcher"
45
51
  require_relative "logbrew/persistent_event_store"
46
52
  require_relative "logbrew/bounded_event_queue"
@@ -433,12 +439,13 @@ module LogBrew
433
439
  end
434
440
 
435
441
  def capture_exception_issue(env, error)
436
- attributes = {
437
- title: error.class.name,
438
- level: "error",
442
+ attributes = IssueDiagnostics.from_exception(
443
+ error,
444
+ message: @include_exception_message ? error.message : nil,
445
+ mechanism_type: exception_mechanism_type,
446
+ handled: false,
439
447
  metadata: exception_metadata(env, error)
440
- }
441
- attributes[:message] = error.message if @include_exception_message
448
+ )
442
449
  @client.issue(next_event_id("issue"), logbrew_timestamp, attributes)
443
450
  end
444
451
 
@@ -497,12 +504,39 @@ module LogBrew
497
504
 
498
505
  def exception_metadata(env, error)
499
506
  request_metadata(env, 500).tap do |metadata|
500
- metadata["exceptionType"] = error.class.name
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
501
520
  metadata["exceptionMessage"] = error.message if @include_exception_message
502
521
  metadata["exceptionBacktrace"] = error.backtrace.join("\n") if @include_exception_backtrace && error.backtrace
503
522
  end
504
523
  end
505
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
+
506
540
  def rack_status(response)
507
541
  return response[0].to_i if response.respond_to?(:[]) && !response[0].nil?
508
542
 
@@ -603,12 +637,25 @@ module LogBrew
603
637
 
604
638
  def report(error, handled: true, severity: :error, context: nil, source: nil, **_options)
605
639
  capture_safely do
606
- attributes = {
607
- title: error_title(error),
608
- level: issue_level(severity),
609
- metadata: rails_metadata(error, handled, severity, context, source)
610
- }
611
- attributes[:message] = error_message(error) if @include_exception_message
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
612
659
  @client.issue(next_event_id, logbrew_timestamp, attributes)
613
660
  flush_if_configured
614
661
  end
@@ -668,11 +715,30 @@ module LogBrew
668
715
  end
669
716
 
670
717
  def add_exception_metadata(metadata, exception)
671
- metadata["exceptionType"] = exception.class.name
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
672
731
  metadata["exceptionMessage"] = exception.message if @include_exception_message
673
732
  metadata["exceptionBacktrace"] = exception.backtrace.join("\n") if @include_exception_backtrace && exception.backtrace
674
733
  end
675
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
+
676
742
  def copy_metadata(metadata)
677
743
  metadata.each_with_object({}) do |(key, value), copied|
678
744
  copied[key.to_s] = value if primitive_metadata_value?(value)
@@ -759,6 +825,26 @@ module LogBrew
759
825
  value
760
826
  end
761
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
+
762
848
  def read(attributes, key)
763
849
  return nil unless attributes.is_a?(Hash)
764
850
 
@@ -790,12 +876,26 @@ module LogBrew
790
876
  on_event_dropped: nil,
791
877
  max_batch_size: EventBatcher::DEFAULT_MAX_SIZE,
792
878
  max_batch_bytes: EventBatcher::DEFAULT_MAX_BYTES,
793
- persistent_queue_path: nil
879
+ persistent_queue_path: nil,
880
+ context: nil,
881
+ capture_runtime_context: true
794
882
  )
795
883
  Validation.require_non_empty("api_key", api_key)
796
884
  Validation.require_non_empty("sdk_name", sdk_name)
797
885
  Validation.require_non_empty("sdk_version", sdk_version)
798
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
799
899
 
800
900
  new(
801
901
  api_key: api_key,
@@ -810,7 +910,8 @@ module LogBrew
810
910
  on_event_dropped: on_event_dropped,
811
911
  max_batch_size: max_batch_size,
812
912
  max_batch_bytes: max_batch_bytes,
813
- persistent_queue_path: persistent_queue_path
913
+ persistent_queue_path: persistent_queue_path,
914
+ context: resolved_context
814
915
  )
815
916
  end
816
917
 
@@ -860,10 +961,12 @@ module LogBrew
860
961
  on_event_dropped: nil,
861
962
  max_batch_size: EventBatcher::DEFAULT_MAX_SIZE,
862
963
  max_batch_bytes: EventBatcher::DEFAULT_MAX_BYTES,
863
- persistent_queue_path: nil
964
+ persistent_queue_path: nil,
965
+ context: nil
864
966
  )
865
967
  @api_key = api_key
866
968
  @sdk = sdk
969
+ @context = context
867
970
  @max_retries = max_retries
868
971
  @event_batcher = EventBatcher.new(sdk: sdk, max_size: max_batch_size, max_bytes: max_batch_bytes)
869
972
  event_store = nil
@@ -954,31 +1057,31 @@ module LogBrew
954
1057
  end
955
1058
 
956
1059
  def release(id, timestamp, attributes)
957
- push_event("release", id, timestamp, validate_release(attributes))
1060
+ push_event("release", id, timestamp, validate_release(attributes), event_context(attributes))
958
1061
  end
959
1062
 
960
1063
  def environment(id, timestamp, attributes)
961
- push_event("environment", id, timestamp, validate_environment(attributes))
1064
+ push_event("environment", id, timestamp, validate_environment(attributes), event_context(attributes))
962
1065
  end
963
1066
 
964
1067
  def issue(id, timestamp, attributes)
965
- push_event("issue", id, timestamp, validate_issue(attributes))
1068
+ push_event("issue", id, timestamp, validate_issue(attributes), event_context(attributes))
966
1069
  end
967
1070
 
968
1071
  def log(id, timestamp, attributes)
969
- push_event("log", id, timestamp, validate_log(attributes))
1072
+ push_event("log", id, timestamp, validate_log(attributes), event_context(attributes))
970
1073
  end
971
1074
 
972
1075
  def span(id, timestamp, attributes)
973
- push_event("span", id, timestamp, validate_span(attributes))
1076
+ push_event("span", id, timestamp, validate_span(attributes), event_context(attributes))
974
1077
  end
975
1078
 
976
1079
  def metric(id, timestamp, attributes)
977
- push_event("metric", id, timestamp, validate_metric(attributes))
1080
+ push_event("metric", id, timestamp, validate_metric(attributes), event_context(attributes))
978
1081
  end
979
1082
 
980
1083
  def action(id, timestamp, attributes)
981
- push_event("action", id, timestamp, validate_action(attributes))
1084
+ push_event("action", id, timestamp, validate_action(attributes), event_context(attributes))
982
1085
  end
983
1086
 
984
1087
  def flush(transport = nil)
@@ -1035,10 +1138,18 @@ module LogBrew
1035
1138
 
1036
1139
  private
1037
1140
 
1038
- def push_event(type, id, timestamp, attributes)
1141
+ def push_event(type, id, timestamp, attributes, event_context)
1039
1142
  @automatic_delivery&.assert_process_ownership!
1040
1143
  Validation.require_non_empty("event id", id)
1041
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?
1042
1153
  event = {
1043
1154
  "type" => type,
1044
1155
  "timestamp" => timestamp,
@@ -1217,20 +1328,7 @@ module LogBrew
1217
1328
  end
1218
1329
 
1219
1330
  def validate_issue(attributes)
1220
- title = Validation.read(attributes, "title")
1221
- level = Validation.read(attributes, "level")
1222
- Validation.require_non_empty("issue title", title)
1223
- level = normalize_severity("issue level", level)
1224
- with_metadata(
1225
- {
1226
- "title" => title,
1227
- "level" => level
1228
- }.tap do |payload|
1229
- message = Validation.read(attributes, "message")
1230
- payload["message"] = message unless message.nil?
1231
- end,
1232
- attributes
1233
- )
1331
+ IssueDiagnostics.validate_issue_attributes(attributes)
1234
1332
  end
1235
1333
 
1236
1334
  def validate_log(attributes)
@@ -1290,6 +1388,8 @@ module LogBrew
1290
1388
 
1291
1389
  def validate_metric(attributes)
1292
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
1293
1393
  kind = Validation.read(attributes, "kind")
1294
1394
  unit = Validation.read(attributes, "unit")
1295
1395
  temporality = Validation.read(attributes, "temporality")
@@ -1305,11 +1405,12 @@ module LogBrew
1305
1405
  with_metadata(
1306
1406
  {
1307
1407
  "name" => name,
1408
+ "description" => description,
1308
1409
  "kind" => kind,
1309
1410
  "value" => value,
1310
1411
  "unit" => unit,
1311
1412
  "temporality" => temporality
1312
- },
1413
+ }.compact,
1313
1414
  attributes
1314
1415
  )
1315
1416
  end
@@ -1322,6 +1423,23 @@ module LogBrew
1322
1423
  with_metadata({ "name" => name, "status" => status }, attributes)
1323
1424
  end
1324
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
+
1325
1443
  def with_metadata(payload, attributes)
1326
1444
  metadata = Validation.require_metadata(Validation.read(attributes, "metadata"))
1327
1445
  payload["metadata"] = metadata unless metadata.nil?
@@ -1332,6 +1450,6 @@ module LogBrew
1332
1450
  require_relative "logbrew/automatic_delivery"
1333
1451
  end
1334
1452
 
1335
- %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|
1336
1454
  require_relative "logbrew/#{path}"
1337
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.3
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-08-01 00:00:00.000000000 Z
11
+ date: 2026-08-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Public LogBrew Ruby SDK with automatic Rails request/error capture and
14
- standard-library delivery.
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,6 +22,7 @@ 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
@@ -33,6 +34,7 @@ files:
33
34
  - lib/logbrew/event_batcher.rb
34
35
  - lib/logbrew/faraday_tracing.rb
35
36
  - lib/logbrew/http_client_tracing.rb
37
+ - lib/logbrew/issue_diagnostics.rb
36
38
  - lib/logbrew/operation_tracing.rb
37
39
  - lib/logbrew/persistent_event_store.rb
38
40
  - lib/logbrew/product_timeline.rb
@@ -41,6 +43,10 @@ files:
41
43
  - lib/logbrew/sidekiq.rb
42
44
  - lib/logbrew/span_events.rb
43
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
44
50
  - lib/logbrew/trace.rb
45
51
  - lib/logbrew/traceparent.rb
46
52
  - lib/logbrew/version.rb