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.
- checksums.yaml +4 -4
- data/README.md +425 -21
- data/examples/Makefile +21 -1
- data/examples/automatic_delivery.rb +32 -0
- data/examples/first_useful_telemetry.rb +118 -0
- data/examples/http_trace_correlation.rb +99 -0
- data/examples/persistent_worker_delivery.rb +33 -0
- data/examples/real_user_smoke.rb +15 -0
- data/examples/sidekiq_tracing.rb +22 -0
- data/lib/logbrew/automatic_delivery.rb +493 -0
- data/lib/logbrew/bounded_event_queue.rb +196 -0
- data/lib/logbrew/event_batcher.rb +67 -0
- data/lib/logbrew/faraday_tracing.rb +55 -0
- data/lib/logbrew/http_client_tracing.rb +282 -0
- data/lib/logbrew/operation_tracing.rb +194 -0
- data/lib/logbrew/persistent_event_store.rb +403 -0
- data/lib/logbrew/product_timeline.rb +153 -0
- data/lib/logbrew/sidekiq.rb +466 -0
- data/lib/logbrew/span_events.rb +34 -0
- data/lib/logbrew/support_ticket.rb +187 -0
- data/lib/logbrew/trace.rb +298 -0
- data/lib/logbrew/traceparent.rb +145 -0
- data/lib/logbrew/worker_lifecycle.rb +211 -0
- data/lib/logbrew.rb +427 -40
- metadata +21 -2
data/lib/logbrew.rb
CHANGED
|
@@ -9,10 +9,26 @@ require "timeout"
|
|
|
9
9
|
require "uri"
|
|
10
10
|
|
|
11
11
|
module LogBrew
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
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 =
|
|
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 => "
|
|
199
|
+
::Logger::DEBUG => "info",
|
|
162
200
|
::Logger::INFO => "info",
|
|
163
201
|
::Logger::WARN => "warning",
|
|
164
202
|
::Logger::ERROR => "error",
|
|
165
|
-
::Logger::FATAL => "
|
|
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::
|
|
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
|
-
|
|
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: {
|
|
727
|
-
|
|
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
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
789
|
-
|
|
1025
|
+
@automatic_delivery&.assert_process_ownership!
|
|
790
1026
|
Validation.require_non_empty("event id", id)
|
|
791
1027
|
Validation.require_timestamp(timestamp)
|
|
792
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1134
|
+
raise DeliveryFailure.new(
|
|
1135
|
+
error.code,
|
|
1136
|
+
error.message,
|
|
1137
|
+
automatic_retryable: error.retryable
|
|
1138
|
+
)
|
|
821
1139
|
end
|
|
822
1140
|
end
|
|
823
|
-
raise
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|