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.
- checksums.yaml +4 -4
- data/README.md +334 -14
- data/examples/Makefile +13 -1
- data/examples/automatic_delivery.rb +32 -0
- data/examples/persistent_worker_delivery.rb +33 -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 +16 -1
- data/lib/logbrew/persistent_event_store.rb +403 -0
- data/lib/logbrew/rails.rb +105 -0
- data/lib/logbrew/rails_integration.rb +622 -0
- data/lib/logbrew/sidekiq.rb +466 -0
- data/lib/logbrew/span_events.rb +34 -0
- data/lib/logbrew/version.rb +5 -0
- data/lib/logbrew/worker_lifecycle.rb +211 -0
- data/lib/logbrew-sdk.rb +4 -0
- data/lib/logbrew.rb +386 -48
- metadata +20 -4
data/lib/logbrew-sdk.rb
ADDED
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
@
|
|
421
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
751
|
-
|
|
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(
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
817
|
-
|
|
1039
|
+
@automatic_delivery&.assert_process_ownership!
|
|
818
1040
|
Validation.require_non_empty("event id", id)
|
|
819
1041
|
Validation.require_timestamp(timestamp)
|
|
820
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1148
|
+
raise DeliveryFailure.new(
|
|
1149
|
+
error.code,
|
|
1150
|
+
error.message,
|
|
1151
|
+
automatic_retryable: error.retryable
|
|
1152
|
+
)
|
|
849
1153
|
end
|
|
850
1154
|
end
|
|
851
|
-
raise
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-08-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description: Public LogBrew Ruby SDK
|
|
14
|
-
|
|
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
|