logbrew-sdk 0.1.1 → 0.1.3

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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logbrew"
4
+ require "logbrew/rails" if defined?(::Rails::Railtie)
data/lib/logbrew.rb CHANGED
@@ -8,6 +8,8 @@ require "time"
8
8
  require "timeout"
9
9
  require "uri"
10
10
 
11
+ require_relative "logbrew/version"
12
+
11
13
  module LogBrew
12
14
  SEVERITY_VALUES = %w[trace debug info warn warning error fatal critical].freeze
13
15
  SEVERITY_ALIASES = {
@@ -39,6 +41,10 @@ module LogBrew
39
41
  end
40
42
  end
41
43
 
44
+ require_relative "logbrew/event_batcher"
45
+ require_relative "logbrew/persistent_event_store"
46
+ require_relative "logbrew/bounded_event_queue"
47
+
42
48
  class TransportError < StandardError
43
49
  attr_reader :code, :retryable
44
50
 
@@ -54,13 +60,29 @@ module LogBrew
54
60
  end
55
61
 
56
62
  class TransportResponse
57
- attr_reader :status_code, :attempts
63
+ attr_reader :status_code, :attempts, :batches
58
64
 
59
- def initialize(status_code, attempts)
65
+ def initialize(status_code, attempts, batches = 1)
60
66
  @status_code = status_code
61
67
  @attempts = attempts
68
+ @batches = batches
69
+ end
70
+ end
71
+
72
+ class DeliveryFailure < SdkError
73
+ attr_reader :automatic_pause_reason
74
+
75
+ def initialize(code, message, automatic_retryable: false, automatic_pause_reason: nil)
76
+ @automatic_retryable = automatic_retryable
77
+ @automatic_pause_reason = automatic_pause_reason
78
+ super(code, message)
79
+ end
80
+
81
+ def automatic_retryable?
82
+ @automatic_retryable
62
83
  end
63
84
  end
85
+ private_constant :DeliveryFailure
64
86
 
65
87
  class RecordingTransport
66
88
  attr_reader :sent_bodies
@@ -92,7 +114,7 @@ module LogBrew
92
114
  end
93
115
 
94
116
  class HttpTransport
95
- DEFAULT_ENDPOINT = "https://api.logbrew.com/v1/events"
117
+ DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events"
96
118
  DEFAULT_TIMEOUT = 10
97
119
 
98
120
  attr_reader :endpoint, :headers, :timeout, :http_client
@@ -114,7 +136,9 @@ module LogBrew
114
136
  @headers.each { |name, value| request[name] = value }
115
137
  request.body = body
116
138
 
117
- response = @http_client ? @http_client.request(request) : request_with_default_client(request)
139
+ response = HttpClientTracing.suppress do
140
+ @http_client ? @http_client.request(request) : request_with_default_client(request)
141
+ end
118
142
  TransportResponse.new(response.code.to_i, 1)
119
143
  rescue TransportError
120
144
  raise
@@ -347,6 +371,7 @@ module LogBrew
347
371
  event_id_prefix: DEFAULT_EVENT_ID_PREFIX,
348
372
  metadata: nil,
349
373
  timestamp_provider: nil,
374
+ include_exception_message: true,
350
375
  include_exception_backtrace: false,
351
376
  on_error: nil,
352
377
  raise_errors: false
@@ -362,10 +387,12 @@ module LogBrew
362
387
  @event_id_prefix = event_id_prefix
363
388
  @metadata = metadata || {}
364
389
  @timestamp_provider = timestamp_provider
390
+ @include_exception_message = include_exception_message
365
391
  @include_exception_backtrace = include_exception_backtrace
366
392
  @on_error = on_error
367
393
  @raise_errors = raise_errors
368
394
  @next_event_number = 0
395
+ @event_id_mutex = Mutex.new
369
396
  end
370
397
 
371
398
  def call(env)
@@ -406,19 +433,20 @@ module LogBrew
406
433
  end
407
434
 
408
435
  def capture_exception_issue(env, error)
409
- @client.issue(
410
- next_event_id("issue"),
411
- logbrew_timestamp,
436
+ attributes = {
412
437
  title: error.class.name,
413
438
  level: "error",
414
- message: error.message,
415
439
  metadata: exception_metadata(env, error)
416
- )
440
+ }
441
+ attributes[:message] = error.message if @include_exception_message
442
+ @client.issue(next_event_id("issue"), logbrew_timestamp, attributes)
417
443
  end
418
444
 
419
445
  def next_event_id(kind)
420
- @next_event_number += 1
421
- "#{@event_id_prefix}_#{kind}_#{@next_event_number}"
446
+ @event_id_mutex.synchronize do
447
+ @next_event_number += 1
448
+ "#{@event_id_prefix}_#{kind}_#{@next_event_number}"
449
+ end
422
450
  end
423
451
 
424
452
  def logbrew_timestamp
@@ -470,7 +498,7 @@ module LogBrew
470
498
  def exception_metadata(env, error)
471
499
  request_metadata(env, 500).tap do |metadata|
472
500
  metadata["exceptionType"] = error.class.name
473
- metadata["exceptionMessage"] = error.message
501
+ metadata["exceptionMessage"] = error.message if @include_exception_message
474
502
  metadata["exceptionBacktrace"] = error.backtrace.join("\n") if @include_exception_backtrace && error.backtrace
475
503
  end
476
504
  end
@@ -551,6 +579,7 @@ module LogBrew
551
579
  event_id_prefix: DEFAULT_EVENT_ID_PREFIX,
552
580
  metadata: nil,
553
581
  timestamp_provider: nil,
582
+ include_exception_message: true,
554
583
  include_exception_backtrace: false,
555
584
  on_error: nil,
556
585
  raise_errors: false
@@ -564,23 +593,23 @@ module LogBrew
564
593
  @event_id_prefix = event_id_prefix
565
594
  @metadata = metadata || {}
566
595
  @timestamp_provider = timestamp_provider
596
+ @include_exception_message = include_exception_message
567
597
  @include_exception_backtrace = include_exception_backtrace
568
598
  @on_error = on_error
569
599
  @raise_errors = raise_errors
570
600
  @next_event_number = 0
601
+ @event_id_mutex = Mutex.new
571
602
  end
572
603
 
573
604
  def report(error, handled: true, severity: :error, context: nil, source: nil, **_options)
574
605
  capture_safely do
575
- @next_event_number += 1
576
- @client.issue(
577
- "#{@event_id_prefix}_#{@next_event_number}",
578
- logbrew_timestamp,
606
+ attributes = {
579
607
  title: error_title(error),
580
608
  level: issue_level(severity),
581
- message: error_message(error),
582
609
  metadata: rails_metadata(error, handled, severity, context, source)
583
- )
610
+ }
611
+ attributes[:message] = error_message(error) if @include_exception_message
612
+ @client.issue(next_event_id, logbrew_timestamp, attributes)
584
613
  flush_if_configured
585
614
  end
586
615
  end
@@ -607,6 +636,13 @@ module LogBrew
607
636
  error.inspect
608
637
  end
609
638
 
639
+ def next_event_id
640
+ @event_id_mutex.synchronize do
641
+ @next_event_number += 1
642
+ "#{@event_id_prefix}_#{@next_event_number}"
643
+ end
644
+ end
645
+
610
646
  def issue_level(severity)
611
647
  SEVERITY_TO_ISSUE_LEVEL.fetch(severity.to_s, "error")
612
648
  end
@@ -633,7 +669,7 @@ module LogBrew
633
669
 
634
670
  def add_exception_metadata(metadata, exception)
635
671
  metadata["exceptionType"] = exception.class.name
636
- metadata["exceptionMessage"] = exception.message
672
+ metadata["exceptionMessage"] = exception.message if @include_exception_message
637
673
  metadata["exceptionBacktrace"] = exception.backtrace.join("\n") if @include_exception_backtrace && exception.backtrace
638
674
  end
639
675
 
@@ -668,7 +704,7 @@ module LogBrew
668
704
  module_function
669
705
 
670
706
  def require_non_empty(label, value)
671
- return if value.is_a?(String) && !value.strip.empty?
707
+ return if value.is_a?(String) && value.valid_encoding? && !value.strip.empty?
672
708
 
673
709
  raise SdkError.new("validation_error", "#{label} must be non-empty")
674
710
  end
@@ -739,7 +775,23 @@ module LogBrew
739
775
  end
740
776
 
741
777
  class Client
742
- def self.create(api_key:, sdk_name:, sdk_version:, max_retries: 2)
778
+ DEFAULT_AUTOMATIC_FLUSH_INTERVAL = 5.0
779
+ DEFAULT_AUTOMATIC_FLUSH_THRESHOLD = 100
780
+ DEFAULT_AUTOMATIC_RETRY_BASE_DELAY = 0.25
781
+ DEFAULT_AUTOMATIC_RETRY_MAX_DELAY = 30.0
782
+
783
+ def self.create(
784
+ api_key:,
785
+ sdk_name:,
786
+ sdk_version:,
787
+ max_retries: 2,
788
+ max_queue_size: BoundedEventQueue::DEFAULT_MAX_SIZE,
789
+ max_queue_bytes: BoundedEventQueue::DEFAULT_MAX_BYTES,
790
+ on_event_dropped: nil,
791
+ max_batch_size: EventBatcher::DEFAULT_MAX_SIZE,
792
+ max_batch_bytes: EventBatcher::DEFAULT_MAX_BYTES,
793
+ persistent_queue_path: nil
794
+ )
743
795
  Validation.require_non_empty("api_key", api_key)
744
796
  Validation.require_non_empty("sdk_name", sdk_name)
745
797
  Validation.require_non_empty("sdk_version", sdk_version)
@@ -747,25 +799,158 @@ module LogBrew
747
799
 
748
800
  new(
749
801
  api_key: api_key,
750
- sdk: { "name" => sdk_name, "language" => "ruby", "version" => sdk_version },
751
- max_retries: max_retries
802
+ sdk: {
803
+ "name" => sdk_name.dup.freeze,
804
+ "language" => "ruby",
805
+ "version" => sdk_version.dup.freeze
806
+ }.freeze,
807
+ max_retries: max_retries,
808
+ max_queue_size: max_queue_size,
809
+ max_queue_bytes: max_queue_bytes,
810
+ on_event_dropped: on_event_dropped,
811
+ max_batch_size: max_batch_size,
812
+ max_batch_bytes: max_batch_bytes,
813
+ persistent_queue_path: persistent_queue_path
814
+ )
815
+ end
816
+
817
+ def self.create_automatic(
818
+ api_key:,
819
+ sdk_name:,
820
+ sdk_version:,
821
+ transport:,
822
+ flush_interval: DEFAULT_AUTOMATIC_FLUSH_INTERVAL,
823
+ flush_threshold: DEFAULT_AUTOMATIC_FLUSH_THRESHOLD,
824
+ retry_base_delay: DEFAULT_AUTOMATIC_RETRY_BASE_DELAY,
825
+ retry_max_delay: DEFAULT_AUTOMATIC_RETRY_MAX_DELAY,
826
+ **options
827
+ )
828
+ max_queue_size = options.fetch(:max_queue_size, BoundedEventQueue::DEFAULT_MAX_SIZE)
829
+ AutomaticDelivery.validate!(
830
+ transport: transport,
831
+ flush_interval: flush_interval,
832
+ flush_threshold: flush_threshold,
833
+ retry_base_delay: retry_base_delay,
834
+ retry_max_delay: retry_max_delay,
835
+ max_queue_size: max_queue_size
836
+ )
837
+ client = create(
838
+ api_key: api_key,
839
+ sdk_name: sdk_name,
840
+ sdk_version: sdk_version,
841
+ **options
842
+ )
843
+ client.send(
844
+ :enable_automatic_delivery,
845
+ transport: transport,
846
+ flush_interval: flush_interval,
847
+ flush_threshold: flush_threshold,
848
+ retry_base_delay: retry_base_delay,
849
+ retry_max_delay: retry_max_delay
752
850
  )
851
+ client
753
852
  end
754
853
 
755
- def initialize(api_key:, sdk:, max_retries:)
854
+ def initialize(
855
+ api_key:,
856
+ sdk:,
857
+ max_retries:,
858
+ max_queue_size: BoundedEventQueue::DEFAULT_MAX_SIZE,
859
+ max_queue_bytes: BoundedEventQueue::DEFAULT_MAX_BYTES,
860
+ on_event_dropped: nil,
861
+ max_batch_size: EventBatcher::DEFAULT_MAX_SIZE,
862
+ max_batch_bytes: EventBatcher::DEFAULT_MAX_BYTES,
863
+ persistent_queue_path: nil
864
+ )
756
865
  @api_key = api_key
757
866
  @sdk = sdk
758
867
  @max_retries = max_retries
759
- @events = []
868
+ @event_batcher = EventBatcher.new(sdk: sdk, max_size: max_batch_size, max_bytes: max_batch_bytes)
869
+ event_store = nil
870
+ begin
871
+ event_store = PersistentEventStore.open(path: persistent_queue_path) unless persistent_queue_path.nil?
872
+ @event_queue = BoundedEventQueue.new(
873
+ max_size: max_queue_size,
874
+ max_bytes: max_queue_bytes,
875
+ max_event_bytes: @event_batcher.max_event_bytes,
876
+ on_event_dropped: on_event_dropped,
877
+ event_store: event_store
878
+ )
879
+ rescue StandardError
880
+ event_store&.close
881
+ raise
882
+ end
883
+ @state_mutex = Mutex.new
884
+ @flush_mutex = Mutex.new
760
885
  @closed = false
886
+ @closing = false
887
+ @retry_batch = nil
888
+ @automatic_delivery = nil
761
889
  end
762
890
 
763
891
  def pending_events
764
- @events.length
892
+ @event_queue.length
893
+ end
894
+
895
+ def pending_event_bytes
896
+ @event_queue.pending_bytes
897
+ end
898
+
899
+ def dropped_events
900
+ @event_queue.dropped_events
901
+ end
902
+
903
+ def delivery_health
904
+ controller = @automatic_delivery
905
+ unless controller.nil?
906
+ controller.assert_process_ownership!
907
+ return controller.snapshot
908
+ end
909
+
910
+ metrics = queue_metrics
911
+ state = @state_mutex.synchronize { @closed ? "closed" : "manual" }
912
+ DeliveryHealth.new(
913
+ state: state,
914
+ queued_events: metrics.fetch(:queued_events),
915
+ queued_bytes: metrics.fetch(:queued_bytes),
916
+ dropped_events: metrics.fetch(:dropped_events),
917
+ in_flight: false,
918
+ last_outcome: "none",
919
+ consecutive_failures: 0,
920
+ pause_reason: nil,
921
+ successful_flushes: 0,
922
+ failed_flushes: 0,
923
+ retry_delay_ms: 0
924
+ )
925
+ end
926
+
927
+ def recover_automatic_delivery
928
+ controller = require_automatic_delivery
929
+ controller.assert_process_ownership!
930
+ flush
931
+ end
932
+
933
+ def stop_automatic_delivery
934
+ controller = require_automatic_delivery
935
+ controller.stop
765
936
  end
766
937
 
767
938
  def preview_json
768
- JSON.pretty_generate("sdk" => @sdk, "events" => @events)
939
+ snapshot = @event_queue.snapshot
940
+ JSON.pretty_generate("sdk" => @sdk, "events" => snapshot.events)
941
+ end
942
+
943
+ def purge_pending_events
944
+ ensure_not_reentrant_flush
945
+ controller = @automatic_delivery
946
+ controller&.assert_process_ownership!
947
+ @flush_mutex.synchronize do
948
+ @state_mutex.synchronize { ensure_open }
949
+ @retry_batch = nil
950
+ removed = @event_queue.purge
951
+ controller&.queue_changed
952
+ removed
953
+ end
769
954
  end
770
955
 
771
956
  def release(id, timestamp, attributes)
@@ -796,59 +981,208 @@ module LogBrew
796
981
  push_event("action", id, timestamp, validate_action(attributes))
797
982
  end
798
983
 
799
- def flush(transport)
800
- raise SdkError.new("shutdown_error", "client is already shut down") if @closed
801
-
802
- flush_internal(transport)
984
+ def flush(transport = nil)
985
+ ensure_not_reentrant_flush
986
+ controller = @automatic_delivery
987
+ controller&.assert_process_ownership!
988
+ resolved_transport = resolve_transport(transport)
989
+ @flush_mutex.synchronize do
990
+ @state_mutex.synchronize { ensure_open }
991
+ previous = controller&.begin_explicit_flush
992
+ begin
993
+ response = flush_internal(resolved_transport)
994
+ controller&.complete_explicit_flush(previous, response)
995
+ response
996
+ rescue StandardError => error
997
+ controller&.fail_explicit_flush(previous, error)
998
+ raise
999
+ end
1000
+ end
803
1001
  end
804
1002
 
805
- def shutdown(transport)
806
- raise SdkError.new("shutdown_error", "client is already shut down") if @closed
1003
+ def shutdown(transport = nil)
1004
+ ensure_not_reentrant_flush
1005
+ controller = @automatic_delivery
1006
+ controller&.assert_process_ownership!
1007
+ resolved_transport = resolve_transport(transport)
1008
+ controller&.prepare_shutdown
1009
+ @flush_mutex.synchronize do
1010
+ @state_mutex.synchronize do
1011
+ ensure_open
1012
+ @closing = true
1013
+ end
807
1014
 
808
- response = flush_internal(transport)
809
- @closed = true
810
- response
1015
+ succeeded = false
1016
+ failure = nil
1017
+ begin
1018
+ response = flush_internal(resolved_transport)
1019
+ @event_queue.close
1020
+ succeeded = true
1021
+ controller&.complete_shutdown(response)
1022
+ response
1023
+ rescue StandardError => error
1024
+ failure = error
1025
+ raise
1026
+ ensure
1027
+ @state_mutex.synchronize do
1028
+ @closed = true if succeeded
1029
+ @closing = false
1030
+ end
1031
+ controller&.resume_after_failed_shutdown(failure) unless succeeded
1032
+ end
1033
+ end
811
1034
  end
812
1035
 
813
1036
  private
814
1037
 
815
1038
  def push_event(type, id, timestamp, attributes)
816
- raise SdkError.new("shutdown_error", "client is already shut down") if @closed
817
-
1039
+ @automatic_delivery&.assert_process_ownership!
818
1040
  Validation.require_non_empty("event id", id)
819
1041
  Validation.require_timestamp(timestamp)
820
- @events << {
1042
+ event = {
821
1043
  "type" => type,
822
1044
  "timestamp" => timestamp,
823
1045
  "id" => id,
824
1046
  "attributes" => attributes
825
1047
  }
1048
+ notice = @state_mutex.synchronize do
1049
+ ensure_open
1050
+ @event_queue.enqueue(event_id: id, event_type: type, event: event)
1051
+ end
1052
+ @event_queue.notify_drop(notice)
1053
+ @automatic_delivery&.notify_capture if notice.nil?
1054
+ end
1055
+
1056
+ def enable_automatic_delivery(transport:, flush_interval:, flush_threshold:, retry_base_delay:, retry_max_delay:)
1057
+ @automatic_delivery = AutomaticDelivery.new(
1058
+ client: self,
1059
+ transport: transport,
1060
+ flush_interval: flush_interval,
1061
+ flush_threshold: flush_threshold,
1062
+ retry_base_delay: retry_base_delay,
1063
+ retry_max_delay: retry_max_delay
1064
+ )
1065
+ end
1066
+
1067
+ def perform_automatic_flush(controller, generation, transport)
1068
+ @flush_mutex.synchronize do
1069
+ begin
1070
+ @state_mutex.synchronize { ensure_open }
1071
+ response = flush_internal(transport)
1072
+ controller.complete_automatic_flush(generation, response)
1073
+ rescue StandardError => error
1074
+ controller.fail_automatic_flush(generation, error)
1075
+ end
1076
+ end
826
1077
  end
827
1078
 
828
1079
  def flush_internal(transport)
829
- return TransportResponse.new(204, 0) if @events.empty?
1080
+ snapshot = @event_queue.snapshot
1081
+ return TransportResponse.new(204, 0, 0) if snapshot.event_count.zero?
1082
+
1083
+ remaining_events = snapshot.event_count
1084
+ attempts = 0
1085
+ batches = 0
1086
+ status_code = 204
1087
+
1088
+ while remaining_events.positive?
1089
+ batch = @retry_batch || @event_batcher.next_batch(
1090
+ @event_queue.snapshot.serialized_events,
1091
+ limit: remaining_events
1092
+ )
1093
+ @retry_batch = batch
1094
+ response = send_batch(transport, batch.body)
1095
+ compaction_error = @event_queue.acknowledge(batch)
1096
+ @retry_batch = nil
1097
+ raise compaction_error unless compaction_error.nil?
1098
+ remaining_events -= batch.event_count
1099
+ attempts += response.attempts
1100
+ batches += 1
1101
+ status_code = response.status_code
1102
+ end
1103
+
1104
+ TransportResponse.new(status_code, attempts, batches)
1105
+ end
830
1106
 
831
- body = preview_json
1107
+ def send_batch(transport, body)
832
1108
  max_attempts = @max_retries + 1
833
1109
  (1..max_attempts).each do |attempt|
834
1110
  begin
835
1111
  response = transport.send(@api_key, body)
836
- raise SdkError.new("unauthenticated", "transport rejected the API key") if response.status_code == 401
1112
+ if response.status_code == 401 || response.status_code == 403
1113
+ raise DeliveryFailure.new(
1114
+ "unauthenticated",
1115
+ "transport rejected the API key",
1116
+ automatic_pause_reason: "authentication"
1117
+ )
1118
+ end
837
1119
 
838
1120
  if response.status_code >= 200 && response.status_code < 300
839
- @events.clear
840
1121
  return TransportResponse.new(response.status_code, attempt)
841
1122
  end
842
- next if response.status_code >= 500 && attempt < max_attempts
1123
+ retryable_status = response.status_code == 408 || response.status_code >= 500
1124
+ next if retryable_status && attempt < max_attempts
1125
+
1126
+ if retryable_status
1127
+ raise DeliveryFailure.new(
1128
+ "transport_error",
1129
+ "unexpected transport status #{response.status_code}",
1130
+ automatic_retryable: true
1131
+ )
1132
+ end
1133
+
1134
+ pause_reason = case response.status_code
1135
+ when 429 then "quota"
1136
+ when 400, 422 then "validation"
1137
+ else "nonretryable"
1138
+ end
843
1139
 
844
- raise SdkError.new("transport_error", "unexpected transport status #{response.status_code}")
1140
+ raise DeliveryFailure.new(
1141
+ "transport_error",
1142
+ "unexpected transport status #{response.status_code}",
1143
+ automatic_pause_reason: pause_reason
1144
+ )
845
1145
  rescue TransportError => error
846
1146
  next if error.retryable && attempt < max_attempts
847
1147
 
848
- raise SdkError.new(error.code, error.message)
1148
+ raise DeliveryFailure.new(
1149
+ error.code,
1150
+ error.message,
1151
+ automatic_retryable: error.retryable
1152
+ )
849
1153
  end
850
1154
  end
851
- raise SdkError.new("transport_error", "exhausted retries")
1155
+ raise DeliveryFailure.new("transport_error", "exhausted retries", automatic_retryable: true)
1156
+ end
1157
+
1158
+ def require_automatic_delivery
1159
+ return @automatic_delivery unless @automatic_delivery.nil?
1160
+
1161
+ raise SdkError.new("automatic_delivery_error", "client does not own automatic delivery")
1162
+ end
1163
+
1164
+ def resolve_transport(transport)
1165
+ resolved = transport || @automatic_delivery&.transport
1166
+ unless resolved.respond_to?(:send)
1167
+ raise SdkError.new("validation_error", "transport must respond to send")
1168
+ end
1169
+
1170
+ resolved
1171
+ end
1172
+
1173
+ def queue_metrics
1174
+ @event_queue.metrics
1175
+ end
1176
+
1177
+ def ensure_not_reentrant_flush
1178
+ return unless @flush_mutex.owned?
1179
+
1180
+ raise SdkError.new("flush_error", "flush is already in progress")
1181
+ end
1182
+
1183
+ def ensure_open
1184
+ raise SdkError.new("shutdown_error", "client is already shut down") if @closed
1185
+ raise SdkError.new("shutdown_error", "client is shutting down") if @closing
852
1186
  end
853
1187
 
854
1188
  def validate_release(attributes)
@@ -937,6 +1271,7 @@ module LogBrew
937
1271
  if !duration_ms.nil? && (!duration_ms.is_a?(Numeric) || duration_ms.negative?)
938
1272
  raise SdkError.new("validation_error", "span durationMs must be non-negative")
939
1273
  end
1274
+ span_events = SpanEvents.validate(Validation.read(attributes, "events"))
940
1275
 
941
1276
  with_metadata(
942
1277
  {
@@ -947,6 +1282,7 @@ module LogBrew
947
1282
  payload["parentSpanId"] = parent_span_id unless parent_span_id.nil?
948
1283
  payload["status"] = status
949
1284
  payload["durationMs"] = duration_ms unless duration_ms.nil?
1285
+ payload["events"] = span_events unless span_events.nil?
950
1286
  end,
951
1287
  attributes
952
1288
  )
@@ -992,8 +1328,10 @@ module LogBrew
992
1328
  payload
993
1329
  end
994
1330
  end
1331
+
1332
+ require_relative "logbrew/automatic_delivery"
995
1333
  end
996
1334
 
997
- %w[product_timeline traceparent trace operation_tracing support_ticket].each do |path|
1335
+ %w[product_timeline traceparent trace span_events operation_tracing http_client_tracing support_ticket worker_lifecycle].each do |path|
998
1336
  require_relative "logbrew/#{path}"
999
1337
  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.1
4
+ version: 0.1.3
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-01 00:00:00.000000000 Z
11
+ date: 2026-08-01 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 automatic Rails request/error capture and
14
+ standard-library delivery.
15
15
  email:
16
16
  executables: []
17
17
  extensions: []
@@ -19,16 +19,32 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - README.md
21
21
  - examples/Makefile
22
+ - examples/automatic_delivery.rb
22
23
  - examples/first_useful_telemetry.rb
23
24
  - examples/http_trace_correlation.rb
25
+ - examples/persistent_worker_delivery.rb
24
26
  - examples/readme_example.rb
25
27
  - examples/real_user_smoke.rb
28
+ - examples/sidekiq_tracing.rb
29
+ - lib/logbrew-sdk.rb
26
30
  - lib/logbrew.rb
31
+ - lib/logbrew/automatic_delivery.rb
32
+ - lib/logbrew/bounded_event_queue.rb
33
+ - lib/logbrew/event_batcher.rb
34
+ - lib/logbrew/faraday_tracing.rb
35
+ - lib/logbrew/http_client_tracing.rb
27
36
  - lib/logbrew/operation_tracing.rb
37
+ - lib/logbrew/persistent_event_store.rb
28
38
  - lib/logbrew/product_timeline.rb
39
+ - lib/logbrew/rails.rb
40
+ - lib/logbrew/rails_integration.rb
41
+ - lib/logbrew/sidekiq.rb
42
+ - lib/logbrew/span_events.rb
29
43
  - lib/logbrew/support_ticket.rb
30
44
  - lib/logbrew/trace.rb
31
45
  - lib/logbrew/traceparent.rb
46
+ - lib/logbrew/version.rb
47
+ - lib/logbrew/worker_lifecycle.rb
32
48
  homepage: https://github.com/LogBrewCo/sdk
33
49
  licenses:
34
50
  - MIT