logbrew-sdk 0.1.0 → 0.1.2

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
@@ -9,10 +9,26 @@ require "timeout"
9
9
  require "uri"
10
10
 
11
11
  module LogBrew
12
- ISSUE_LEVELS = %w[info warning error critical].freeze
13
- LOG_LEVELS = %w[debug info warning error].freeze
12
+ SEVERITY_VALUES = %w[trace debug info warn warning error fatal critical].freeze
13
+ SEVERITY_ALIASES = {
14
+ "trace" => "info",
15
+ "debug" => "info",
16
+ "info" => "info",
17
+ "warn" => "warning",
18
+ "warning" => "warning",
19
+ "error" => "error",
20
+ "fatal" => "critical",
21
+ "critical" => "critical"
22
+ }.freeze
14
23
  SPAN_STATUSES = %w[ok error].freeze
15
24
  ACTION_STATUSES = %w[queued running success failure].freeze
25
+ METRIC_TEMPORALITIES_BY_KIND = {
26
+ "counter" => %w[delta cumulative].freeze,
27
+ "gauge" => %w[instant].freeze,
28
+ "histogram" => %w[delta cumulative].freeze
29
+ }.freeze
30
+ METRIC_KINDS = METRIC_TEMPORALITIES_BY_KIND.keys.freeze
31
+ NON_NEGATIVE_METRIC_KINDS = %w[counter histogram].freeze
16
32
 
17
33
  class SdkError < StandardError
18
34
  attr_reader :code
@@ -23,6 +39,10 @@ module LogBrew
23
39
  end
24
40
  end
25
41
 
42
+ require_relative "logbrew/event_batcher"
43
+ require_relative "logbrew/persistent_event_store"
44
+ require_relative "logbrew/bounded_event_queue"
45
+
26
46
  class TransportError < StandardError
27
47
  attr_reader :code, :retryable
28
48
 
@@ -38,13 +58,29 @@ module LogBrew
38
58
  end
39
59
 
40
60
  class TransportResponse
41
- attr_reader :status_code, :attempts
61
+ attr_reader :status_code, :attempts, :batches
42
62
 
43
- def initialize(status_code, attempts)
63
+ def initialize(status_code, attempts, batches = 1)
44
64
  @status_code = status_code
45
65
  @attempts = attempts
66
+ @batches = batches
67
+ end
68
+ end
69
+
70
+ class DeliveryFailure < SdkError
71
+ attr_reader :automatic_pause_reason
72
+
73
+ def initialize(code, message, automatic_retryable: false, automatic_pause_reason: nil)
74
+ @automatic_retryable = automatic_retryable
75
+ @automatic_pause_reason = automatic_pause_reason
76
+ super(code, message)
77
+ end
78
+
79
+ def automatic_retryable?
80
+ @automatic_retryable
46
81
  end
47
82
  end
83
+ private_constant :DeliveryFailure
48
84
 
49
85
  class RecordingTransport
50
86
  attr_reader :sent_bodies
@@ -76,7 +112,7 @@ module LogBrew
76
112
  end
77
113
 
78
114
  class HttpTransport
79
- DEFAULT_ENDPOINT = "https://api.logbrew.com/v1/events"
115
+ DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events"
80
116
  DEFAULT_TIMEOUT = 10
81
117
 
82
118
  attr_reader :endpoint, :headers, :timeout, :http_client
@@ -98,7 +134,9 @@ module LogBrew
98
134
  @headers.each { |name, value| request[name] = value }
99
135
  request.body = body
100
136
 
101
- response = @http_client ? @http_client.request(request) : request_with_default_client(request)
137
+ response = HttpClientTracing.suppress do
138
+ @http_client ? @http_client.request(request) : request_with_default_client(request)
139
+ end
102
140
  TransportResponse.new(response.code.to_i, 1)
103
141
  rescue TransportError
104
142
  raise
@@ -158,11 +196,11 @@ module LogBrew
158
196
  class Logger < ::Logger
159
197
  DEFAULT_LOGGER_NAME = "ruby-logger"
160
198
  SEVERITY_TO_LOGBREW_LEVEL = {
161
- ::Logger::DEBUG => "debug",
199
+ ::Logger::DEBUG => "info",
162
200
  ::Logger::INFO => "info",
163
201
  ::Logger::WARN => "warning",
164
202
  ::Logger::ERROR => "error",
165
- ::Logger::FATAL => "error",
203
+ ::Logger::FATAL => "critical",
166
204
  ::Logger::UNKNOWN => "error"
167
205
  }.freeze
168
206
 
@@ -275,7 +313,7 @@ module LogBrew
275
313
  end
276
314
 
277
315
  def logbrew_level(severity)
278
- SEVERITY_TO_LOGBREW_LEVEL.fetch(severity.to_i, severity.to_i >= ::Logger::WARN ? "error" : "info")
316
+ SEVERITY_TO_LOGBREW_LEVEL.fetch(severity.to_i, severity.to_i >= ::Logger::FATAL ? "critical" : "info")
279
317
  end
280
318
 
281
319
  def event_logger_name(progname)
@@ -314,7 +352,6 @@ module LogBrew
314
352
  value.is_a?(Float) && value.finite?
315
353
  end
316
354
  end
317
-
318
355
  # Rack-compatible middleware for Rails, Sinatra, and other Rack-based Ruby apps.
319
356
  #
320
357
  # The middleware captures completed requests as span events and unhandled app
@@ -653,7 +690,7 @@ module LogBrew
653
690
  module_function
654
691
 
655
692
  def require_non_empty(label, value)
656
- return if value.is_a?(String) && !value.strip.empty?
693
+ return if value.is_a?(String) && value.valid_encoding? && !value.strip.empty?
657
694
 
658
695
  raise SdkError.new("validation_error", "#{label} must be non-empty")
659
696
  end
@@ -699,6 +736,15 @@ module LogBrew
699
736
  end
700
737
  end
701
738
 
739
+ def require_finite_number(label, value)
740
+ unless value.is_a?(Integer) || value.is_a?(Float)
741
+ raise SdkError.new("validation_error", "#{label} must be a finite number")
742
+ end
743
+ raise SdkError.new("validation_error", "#{label} must be a finite number") if numeric_nan?(value)
744
+
745
+ value
746
+ end
747
+
702
748
  def read(attributes, key)
703
749
  return nil unless attributes.is_a?(Hash)
704
750
 
@@ -715,7 +761,23 @@ module LogBrew
715
761
  end
716
762
 
717
763
  class Client
718
- def self.create(api_key:, sdk_name:, sdk_version:, max_retries: 2)
764
+ DEFAULT_AUTOMATIC_FLUSH_INTERVAL = 5.0
765
+ DEFAULT_AUTOMATIC_FLUSH_THRESHOLD = 100
766
+ DEFAULT_AUTOMATIC_RETRY_BASE_DELAY = 0.25
767
+ DEFAULT_AUTOMATIC_RETRY_MAX_DELAY = 30.0
768
+
769
+ def self.create(
770
+ api_key:,
771
+ sdk_name:,
772
+ sdk_version:,
773
+ max_retries: 2,
774
+ max_queue_size: BoundedEventQueue::DEFAULT_MAX_SIZE,
775
+ max_queue_bytes: BoundedEventQueue::DEFAULT_MAX_BYTES,
776
+ on_event_dropped: nil,
777
+ max_batch_size: EventBatcher::DEFAULT_MAX_SIZE,
778
+ max_batch_bytes: EventBatcher::DEFAULT_MAX_BYTES,
779
+ persistent_queue_path: nil
780
+ )
719
781
  Validation.require_non_empty("api_key", api_key)
720
782
  Validation.require_non_empty("sdk_name", sdk_name)
721
783
  Validation.require_non_empty("sdk_version", sdk_version)
@@ -723,25 +785,158 @@ module LogBrew
723
785
 
724
786
  new(
725
787
  api_key: api_key,
726
- sdk: { "name" => sdk_name, "language" => "ruby", "version" => sdk_version },
727
- max_retries: max_retries
788
+ sdk: {
789
+ "name" => sdk_name.dup.freeze,
790
+ "language" => "ruby",
791
+ "version" => sdk_version.dup.freeze
792
+ }.freeze,
793
+ max_retries: max_retries,
794
+ max_queue_size: max_queue_size,
795
+ max_queue_bytes: max_queue_bytes,
796
+ on_event_dropped: on_event_dropped,
797
+ max_batch_size: max_batch_size,
798
+ max_batch_bytes: max_batch_bytes,
799
+ persistent_queue_path: persistent_queue_path
728
800
  )
729
801
  end
730
802
 
731
- def initialize(api_key:, sdk:, max_retries:)
803
+ def self.create_automatic(
804
+ api_key:,
805
+ sdk_name:,
806
+ sdk_version:,
807
+ transport:,
808
+ flush_interval: DEFAULT_AUTOMATIC_FLUSH_INTERVAL,
809
+ flush_threshold: DEFAULT_AUTOMATIC_FLUSH_THRESHOLD,
810
+ retry_base_delay: DEFAULT_AUTOMATIC_RETRY_BASE_DELAY,
811
+ retry_max_delay: DEFAULT_AUTOMATIC_RETRY_MAX_DELAY,
812
+ **options
813
+ )
814
+ max_queue_size = options.fetch(:max_queue_size, BoundedEventQueue::DEFAULT_MAX_SIZE)
815
+ AutomaticDelivery.validate!(
816
+ transport: transport,
817
+ flush_interval: flush_interval,
818
+ flush_threshold: flush_threshold,
819
+ retry_base_delay: retry_base_delay,
820
+ retry_max_delay: retry_max_delay,
821
+ max_queue_size: max_queue_size
822
+ )
823
+ client = create(
824
+ api_key: api_key,
825
+ sdk_name: sdk_name,
826
+ sdk_version: sdk_version,
827
+ **options
828
+ )
829
+ client.send(
830
+ :enable_automatic_delivery,
831
+ transport: transport,
832
+ flush_interval: flush_interval,
833
+ flush_threshold: flush_threshold,
834
+ retry_base_delay: retry_base_delay,
835
+ retry_max_delay: retry_max_delay
836
+ )
837
+ client
838
+ end
839
+
840
+ def initialize(
841
+ api_key:,
842
+ sdk:,
843
+ max_retries:,
844
+ max_queue_size: BoundedEventQueue::DEFAULT_MAX_SIZE,
845
+ max_queue_bytes: BoundedEventQueue::DEFAULT_MAX_BYTES,
846
+ on_event_dropped: nil,
847
+ max_batch_size: EventBatcher::DEFAULT_MAX_SIZE,
848
+ max_batch_bytes: EventBatcher::DEFAULT_MAX_BYTES,
849
+ persistent_queue_path: nil
850
+ )
732
851
  @api_key = api_key
733
852
  @sdk = sdk
734
853
  @max_retries = max_retries
735
- @events = []
854
+ @event_batcher = EventBatcher.new(sdk: sdk, max_size: max_batch_size, max_bytes: max_batch_bytes)
855
+ event_store = nil
856
+ begin
857
+ event_store = PersistentEventStore.open(path: persistent_queue_path) unless persistent_queue_path.nil?
858
+ @event_queue = BoundedEventQueue.new(
859
+ max_size: max_queue_size,
860
+ max_bytes: max_queue_bytes,
861
+ max_event_bytes: @event_batcher.max_event_bytes,
862
+ on_event_dropped: on_event_dropped,
863
+ event_store: event_store
864
+ )
865
+ rescue StandardError
866
+ event_store&.close
867
+ raise
868
+ end
869
+ @state_mutex = Mutex.new
870
+ @flush_mutex = Mutex.new
736
871
  @closed = false
872
+ @closing = false
873
+ @retry_batch = nil
874
+ @automatic_delivery = nil
737
875
  end
738
876
 
739
877
  def pending_events
740
- @events.length
878
+ @event_queue.length
879
+ end
880
+
881
+ def pending_event_bytes
882
+ @event_queue.pending_bytes
883
+ end
884
+
885
+ def dropped_events
886
+ @event_queue.dropped_events
887
+ end
888
+
889
+ def delivery_health
890
+ controller = @automatic_delivery
891
+ unless controller.nil?
892
+ controller.assert_process_ownership!
893
+ return controller.snapshot
894
+ end
895
+
896
+ metrics = queue_metrics
897
+ state = @state_mutex.synchronize { @closed ? "closed" : "manual" }
898
+ DeliveryHealth.new(
899
+ state: state,
900
+ queued_events: metrics.fetch(:queued_events),
901
+ queued_bytes: metrics.fetch(:queued_bytes),
902
+ dropped_events: metrics.fetch(:dropped_events),
903
+ in_flight: false,
904
+ last_outcome: "none",
905
+ consecutive_failures: 0,
906
+ pause_reason: nil,
907
+ successful_flushes: 0,
908
+ failed_flushes: 0,
909
+ retry_delay_ms: 0
910
+ )
911
+ end
912
+
913
+ def recover_automatic_delivery
914
+ controller = require_automatic_delivery
915
+ controller.assert_process_ownership!
916
+ flush
917
+ end
918
+
919
+ def stop_automatic_delivery
920
+ controller = require_automatic_delivery
921
+ controller.stop
741
922
  end
742
923
 
743
924
  def preview_json
744
- JSON.pretty_generate("sdk" => @sdk, "events" => @events)
925
+ snapshot = @event_queue.snapshot
926
+ JSON.pretty_generate("sdk" => @sdk, "events" => snapshot.events)
927
+ end
928
+
929
+ def purge_pending_events
930
+ ensure_not_reentrant_flush
931
+ controller = @automatic_delivery
932
+ controller&.assert_process_ownership!
933
+ @flush_mutex.synchronize do
934
+ @state_mutex.synchronize { ensure_open }
935
+ @retry_batch = nil
936
+ removed = @event_queue.purge
937
+ controller&.queue_changed
938
+ removed
939
+ end
745
940
  end
746
941
 
747
942
  def release(id, timestamp, attributes)
@@ -764,63 +959,216 @@ module LogBrew
764
959
  push_event("span", id, timestamp, validate_span(attributes))
765
960
  end
766
961
 
962
+ def metric(id, timestamp, attributes)
963
+ push_event("metric", id, timestamp, validate_metric(attributes))
964
+ end
965
+
767
966
  def action(id, timestamp, attributes)
768
967
  push_event("action", id, timestamp, validate_action(attributes))
769
968
  end
770
969
 
771
- def flush(transport)
772
- raise SdkError.new("shutdown_error", "client is already shut down") if @closed
773
-
774
- flush_internal(transport)
970
+ def flush(transport = nil)
971
+ ensure_not_reentrant_flush
972
+ controller = @automatic_delivery
973
+ controller&.assert_process_ownership!
974
+ resolved_transport = resolve_transport(transport)
975
+ @flush_mutex.synchronize do
976
+ @state_mutex.synchronize { ensure_open }
977
+ previous = controller&.begin_explicit_flush
978
+ begin
979
+ response = flush_internal(resolved_transport)
980
+ controller&.complete_explicit_flush(previous, response)
981
+ response
982
+ rescue StandardError => error
983
+ controller&.fail_explicit_flush(previous, error)
984
+ raise
985
+ end
986
+ end
775
987
  end
776
988
 
777
- def shutdown(transport)
778
- raise SdkError.new("shutdown_error", "client is already shut down") if @closed
989
+ def shutdown(transport = nil)
990
+ ensure_not_reentrant_flush
991
+ controller = @automatic_delivery
992
+ controller&.assert_process_ownership!
993
+ resolved_transport = resolve_transport(transport)
994
+ controller&.prepare_shutdown
995
+ @flush_mutex.synchronize do
996
+ @state_mutex.synchronize do
997
+ ensure_open
998
+ @closing = true
999
+ end
779
1000
 
780
- response = flush_internal(transport)
781
- @closed = true
782
- response
1001
+ succeeded = false
1002
+ failure = nil
1003
+ begin
1004
+ response = flush_internal(resolved_transport)
1005
+ @event_queue.close
1006
+ succeeded = true
1007
+ controller&.complete_shutdown(response)
1008
+ response
1009
+ rescue StandardError => error
1010
+ failure = error
1011
+ raise
1012
+ ensure
1013
+ @state_mutex.synchronize do
1014
+ @closed = true if succeeded
1015
+ @closing = false
1016
+ end
1017
+ controller&.resume_after_failed_shutdown(failure) unless succeeded
1018
+ end
1019
+ end
783
1020
  end
784
1021
 
785
1022
  private
786
1023
 
787
1024
  def push_event(type, id, timestamp, attributes)
788
- raise SdkError.new("shutdown_error", "client is already shut down") if @closed
789
-
1025
+ @automatic_delivery&.assert_process_ownership!
790
1026
  Validation.require_non_empty("event id", id)
791
1027
  Validation.require_timestamp(timestamp)
792
- @events << {
1028
+ event = {
793
1029
  "type" => type,
794
1030
  "timestamp" => timestamp,
795
1031
  "id" => id,
796
1032
  "attributes" => attributes
797
1033
  }
1034
+ notice = @state_mutex.synchronize do
1035
+ ensure_open
1036
+ @event_queue.enqueue(event_id: id, event_type: type, event: event)
1037
+ end
1038
+ @event_queue.notify_drop(notice)
1039
+ @automatic_delivery&.notify_capture if notice.nil?
1040
+ end
1041
+
1042
+ def enable_automatic_delivery(transport:, flush_interval:, flush_threshold:, retry_base_delay:, retry_max_delay:)
1043
+ @automatic_delivery = AutomaticDelivery.new(
1044
+ client: self,
1045
+ transport: transport,
1046
+ flush_interval: flush_interval,
1047
+ flush_threshold: flush_threshold,
1048
+ retry_base_delay: retry_base_delay,
1049
+ retry_max_delay: retry_max_delay
1050
+ )
1051
+ end
1052
+
1053
+ def perform_automatic_flush(controller, generation, transport)
1054
+ @flush_mutex.synchronize do
1055
+ begin
1056
+ @state_mutex.synchronize { ensure_open }
1057
+ response = flush_internal(transport)
1058
+ controller.complete_automatic_flush(generation, response)
1059
+ rescue StandardError => error
1060
+ controller.fail_automatic_flush(generation, error)
1061
+ end
1062
+ end
798
1063
  end
799
1064
 
800
1065
  def flush_internal(transport)
801
- return TransportResponse.new(204, 0) if @events.empty?
1066
+ snapshot = @event_queue.snapshot
1067
+ return TransportResponse.new(204, 0, 0) if snapshot.event_count.zero?
1068
+
1069
+ remaining_events = snapshot.event_count
1070
+ attempts = 0
1071
+ batches = 0
1072
+ status_code = 204
1073
+
1074
+ while remaining_events.positive?
1075
+ batch = @retry_batch || @event_batcher.next_batch(
1076
+ @event_queue.snapshot.serialized_events,
1077
+ limit: remaining_events
1078
+ )
1079
+ @retry_batch = batch
1080
+ response = send_batch(transport, batch.body)
1081
+ compaction_error = @event_queue.acknowledge(batch)
1082
+ @retry_batch = nil
1083
+ raise compaction_error unless compaction_error.nil?
1084
+ remaining_events -= batch.event_count
1085
+ attempts += response.attempts
1086
+ batches += 1
1087
+ status_code = response.status_code
1088
+ end
1089
+
1090
+ TransportResponse.new(status_code, attempts, batches)
1091
+ end
802
1092
 
803
- body = preview_json
1093
+ def send_batch(transport, body)
804
1094
  max_attempts = @max_retries + 1
805
1095
  (1..max_attempts).each do |attempt|
806
1096
  begin
807
1097
  response = transport.send(@api_key, body)
808
- raise SdkError.new("unauthenticated", "transport rejected the API key") if response.status_code == 401
1098
+ if response.status_code == 401 || response.status_code == 403
1099
+ raise DeliveryFailure.new(
1100
+ "unauthenticated",
1101
+ "transport rejected the API key",
1102
+ automatic_pause_reason: "authentication"
1103
+ )
1104
+ end
809
1105
 
810
1106
  if response.status_code >= 200 && response.status_code < 300
811
- @events.clear
812
1107
  return TransportResponse.new(response.status_code, attempt)
813
1108
  end
814
- next if response.status_code >= 500 && attempt < max_attempts
1109
+ retryable_status = response.status_code == 408 || response.status_code >= 500
1110
+ next if retryable_status && attempt < max_attempts
1111
+
1112
+ if retryable_status
1113
+ raise DeliveryFailure.new(
1114
+ "transport_error",
1115
+ "unexpected transport status #{response.status_code}",
1116
+ automatic_retryable: true
1117
+ )
1118
+ end
1119
+
1120
+ pause_reason = case response.status_code
1121
+ when 429 then "quota"
1122
+ when 400, 422 then "validation"
1123
+ else "nonretryable"
1124
+ end
815
1125
 
816
- raise SdkError.new("transport_error", "unexpected transport status #{response.status_code}")
1126
+ raise DeliveryFailure.new(
1127
+ "transport_error",
1128
+ "unexpected transport status #{response.status_code}",
1129
+ automatic_pause_reason: pause_reason
1130
+ )
817
1131
  rescue TransportError => error
818
1132
  next if error.retryable && attempt < max_attempts
819
1133
 
820
- raise SdkError.new(error.code, error.message)
1134
+ raise DeliveryFailure.new(
1135
+ error.code,
1136
+ error.message,
1137
+ automatic_retryable: error.retryable
1138
+ )
821
1139
  end
822
1140
  end
823
- raise SdkError.new("transport_error", "exhausted retries")
1141
+ raise DeliveryFailure.new("transport_error", "exhausted retries", automatic_retryable: true)
1142
+ end
1143
+
1144
+ def require_automatic_delivery
1145
+ return @automatic_delivery unless @automatic_delivery.nil?
1146
+
1147
+ raise SdkError.new("automatic_delivery_error", "client does not own automatic delivery")
1148
+ end
1149
+
1150
+ def resolve_transport(transport)
1151
+ resolved = transport || @automatic_delivery&.transport
1152
+ unless resolved.respond_to?(:send)
1153
+ raise SdkError.new("validation_error", "transport must respond to send")
1154
+ end
1155
+
1156
+ resolved
1157
+ end
1158
+
1159
+ def queue_metrics
1160
+ @event_queue.metrics
1161
+ end
1162
+
1163
+ def ensure_not_reentrant_flush
1164
+ return unless @flush_mutex.owned?
1165
+
1166
+ raise SdkError.new("flush_error", "flush is already in progress")
1167
+ end
1168
+
1169
+ def ensure_open
1170
+ raise SdkError.new("shutdown_error", "client is already shut down") if @closed
1171
+ raise SdkError.new("shutdown_error", "client is shutting down") if @closing
824
1172
  end
825
1173
 
826
1174
  def validate_release(attributes)
@@ -858,7 +1206,7 @@ module LogBrew
858
1206
  title = Validation.read(attributes, "title")
859
1207
  level = Validation.read(attributes, "level")
860
1208
  Validation.require_non_empty("issue title", title)
861
- Validation.require_allowed_value("issue level", level, ISSUE_LEVELS)
1209
+ level = normalize_severity("issue level", level)
862
1210
  with_metadata(
863
1211
  {
864
1212
  "title" => title,
@@ -875,7 +1223,7 @@ module LogBrew
875
1223
  message = Validation.read(attributes, "message")
876
1224
  level = Validation.read(attributes, "level")
877
1225
  Validation.require_non_empty("log message", message)
878
- Validation.require_allowed_value("log level", level, LOG_LEVELS)
1226
+ level = normalize_severity("log level", level)
879
1227
  with_metadata(
880
1228
  {
881
1229
  "message" => message,
@@ -888,6 +1236,11 @@ module LogBrew
888
1236
  )
889
1237
  end
890
1238
 
1239
+ def normalize_severity(label, level)
1240
+ Validation.require_allowed_value(label, level, SEVERITY_VALUES)
1241
+ SEVERITY_ALIASES.fetch(level)
1242
+ end
1243
+
891
1244
  def validate_span(attributes)
892
1245
  name = Validation.read(attributes, "name")
893
1246
  trace_id = Validation.read(attributes, "traceId")
@@ -904,6 +1257,7 @@ module LogBrew
904
1257
  if !duration_ms.nil? && (!duration_ms.is_a?(Numeric) || duration_ms.negative?)
905
1258
  raise SdkError.new("validation_error", "span durationMs must be non-negative")
906
1259
  end
1260
+ span_events = SpanEvents.validate(Validation.read(attributes, "events"))
907
1261
 
908
1262
  with_metadata(
909
1263
  {
@@ -914,11 +1268,38 @@ module LogBrew
914
1268
  payload["parentSpanId"] = parent_span_id unless parent_span_id.nil?
915
1269
  payload["status"] = status
916
1270
  payload["durationMs"] = duration_ms unless duration_ms.nil?
1271
+ payload["events"] = span_events unless span_events.nil?
917
1272
  end,
918
1273
  attributes
919
1274
  )
920
1275
  end
921
1276
 
1277
+ def validate_metric(attributes)
1278
+ name = Validation.read(attributes, "name")
1279
+ kind = Validation.read(attributes, "kind")
1280
+ unit = Validation.read(attributes, "unit")
1281
+ temporality = Validation.read(attributes, "temporality")
1282
+ Validation.require_non_empty("metric name", name)
1283
+ Validation.require_allowed_value("metric kind", kind, METRIC_KINDS)
1284
+ value = Validation.require_finite_number("metric value", Validation.read(attributes, "value"))
1285
+ Validation.require_non_empty("metric unit", unit)
1286
+ Validation.require_allowed_value("metric temporality for #{kind}", temporality, METRIC_TEMPORALITIES_BY_KIND[kind])
1287
+ if NON_NEGATIVE_METRIC_KINDS.include?(kind) && value.negative?
1288
+ raise SdkError.new("validation_error", "metric #{kind} value must be non-negative")
1289
+ end
1290
+
1291
+ with_metadata(
1292
+ {
1293
+ "name" => name,
1294
+ "kind" => kind,
1295
+ "value" => value,
1296
+ "unit" => unit,
1297
+ "temporality" => temporality
1298
+ },
1299
+ attributes
1300
+ )
1301
+ end
1302
+
922
1303
  def validate_action(attributes)
923
1304
  name = Validation.read(attributes, "name")
924
1305
  status = Validation.read(attributes, "status")
@@ -933,4 +1314,10 @@ module LogBrew
933
1314
  payload
934
1315
  end
935
1316
  end
1317
+
1318
+ require_relative "logbrew/automatic_delivery"
1319
+ end
1320
+
1321
+ %w[product_timeline traceparent trace span_events operation_tracing http_client_tracing support_ticket worker_lifecycle].each do |path|
1322
+ require_relative "logbrew/#{path}"
936
1323
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logbrew-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - LogBrew
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-05 00:00:00.000000000 Z
11
+ date: 2026-07-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Public LogBrew Ruby SDK for building, validating, and flushing event
14
14
  batches.
@@ -19,9 +19,28 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - README.md
21
21
  - examples/Makefile
22
+ - examples/automatic_delivery.rb
23
+ - examples/first_useful_telemetry.rb
24
+ - examples/http_trace_correlation.rb
25
+ - examples/persistent_worker_delivery.rb
22
26
  - examples/readme_example.rb
23
27
  - examples/real_user_smoke.rb
28
+ - examples/sidekiq_tracing.rb
24
29
  - lib/logbrew.rb
30
+ - lib/logbrew/automatic_delivery.rb
31
+ - lib/logbrew/bounded_event_queue.rb
32
+ - lib/logbrew/event_batcher.rb
33
+ - lib/logbrew/faraday_tracing.rb
34
+ - lib/logbrew/http_client_tracing.rb
35
+ - lib/logbrew/operation_tracing.rb
36
+ - lib/logbrew/persistent_event_store.rb
37
+ - lib/logbrew/product_timeline.rb
38
+ - lib/logbrew/sidekiq.rb
39
+ - lib/logbrew/span_events.rb
40
+ - lib/logbrew/support_ticket.rb
41
+ - lib/logbrew/trace.rb
42
+ - lib/logbrew/traceparent.rb
43
+ - lib/logbrew/worker_lifecycle.rb
25
44
  homepage: https://github.com/LogBrewCo/sdk
26
45
  licenses:
27
46
  - MIT