logbrew-sdk 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +223 -64
- data/examples/Makefile +5 -1
- data/examples/first_useful_telemetry.rb +90 -72
- data/examples/http_trace_correlation.rb +1 -0
- data/examples/issue_diagnostics.rb +55 -0
- data/examples/readme_example.rb +2 -1
- data/examples/real_user_smoke.rb +4 -2
- data/lib/logbrew/issue_diagnostics.rb +576 -0
- data/lib/logbrew/product_timeline.rb +24 -3
- data/lib/logbrew/rails_integration.rb +22 -1
- data/lib/logbrew/telemetry.rb +60 -0
- data/lib/logbrew/telemetry_context.rb +306 -0
- data/lib/logbrew/telemetry_context_value.rb +152 -0
- data/lib/logbrew/telemetry_resource.rb +161 -0
- data/lib/logbrew/version.rb +1 -1
- data/lib/logbrew.rb +158 -40
- metadata +10 -4
data/lib/logbrew.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
3
4
|
require "json"
|
|
4
5
|
require "logger"
|
|
5
6
|
require "net/http"
|
|
@@ -41,6 +42,11 @@ module LogBrew
|
|
|
41
42
|
end
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
require_relative "logbrew/telemetry_context_value"
|
|
46
|
+
require_relative "logbrew/telemetry_resource"
|
|
47
|
+
require_relative "logbrew/telemetry_context"
|
|
48
|
+
require_relative "logbrew/telemetry"
|
|
49
|
+
|
|
44
50
|
require_relative "logbrew/event_batcher"
|
|
45
51
|
require_relative "logbrew/persistent_event_store"
|
|
46
52
|
require_relative "logbrew/bounded_event_queue"
|
|
@@ -433,12 +439,13 @@ module LogBrew
|
|
|
433
439
|
end
|
|
434
440
|
|
|
435
441
|
def capture_exception_issue(env, error)
|
|
436
|
-
attributes =
|
|
437
|
-
|
|
438
|
-
|
|
442
|
+
attributes = IssueDiagnostics.from_exception(
|
|
443
|
+
error,
|
|
444
|
+
message: @include_exception_message ? error.message : nil,
|
|
445
|
+
mechanism_type: exception_mechanism_type,
|
|
446
|
+
handled: false,
|
|
439
447
|
metadata: exception_metadata(env, error)
|
|
440
|
-
|
|
441
|
-
attributes[:message] = error.message if @include_exception_message
|
|
448
|
+
)
|
|
442
449
|
@client.issue(next_event_id("issue"), logbrew_timestamp, attributes)
|
|
443
450
|
end
|
|
444
451
|
|
|
@@ -497,12 +504,39 @@ module LogBrew
|
|
|
497
504
|
|
|
498
505
|
def exception_metadata(env, error)
|
|
499
506
|
request_metadata(env, 500).tap do |metadata|
|
|
500
|
-
|
|
507
|
+
exception_type = IssueDiagnostics.safe_exception_type(error)
|
|
508
|
+
frames = IssueDiagnostics.stack_frames_from_exception(error)
|
|
509
|
+
first_frame = frames.first
|
|
510
|
+
metadata["exceptionType"] = exception_type
|
|
511
|
+
metadata["errorName"] = exception_type
|
|
512
|
+
metadata["handled"] = false
|
|
513
|
+
metadata["mechanism"] = exception_mechanism_type
|
|
514
|
+
metadata["issueGroupingKey"] = exception_grouping_key(env, exception_type, first_frame)
|
|
515
|
+
metadata["issueGroupingSource"] = "exception_type_route_file"
|
|
516
|
+
unless first_frame.nil?
|
|
517
|
+
metadata["errorFrameFile"] = first_frame.fetch("filename")
|
|
518
|
+
metadata["errorFrameLine"] = first_frame.fetch("line")
|
|
519
|
+
end
|
|
501
520
|
metadata["exceptionMessage"] = error.message if @include_exception_message
|
|
502
521
|
metadata["exceptionBacktrace"] = error.backtrace.join("\n") if @include_exception_backtrace && error.backtrace
|
|
503
522
|
end
|
|
504
523
|
end
|
|
505
524
|
|
|
525
|
+
def exception_mechanism_type
|
|
526
|
+
"rack.middleware"
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def exception_grouping_prefix
|
|
530
|
+
"rack-exception"
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def exception_grouping_key(env, exception_type, first_frame)
|
|
534
|
+
route = request_path(env)
|
|
535
|
+
frame_file = first_frame.nil? ? "" : first_frame.fetch("filename")
|
|
536
|
+
material = [exception_type, route, frame_file].join("\n")
|
|
537
|
+
"#{exception_grouping_prefix}-#{Digest::SHA256.hexdigest(material)}"
|
|
538
|
+
end
|
|
539
|
+
|
|
506
540
|
def rack_status(response)
|
|
507
541
|
return response[0].to_i if response.respond_to?(:[]) && !response[0].nil?
|
|
508
542
|
|
|
@@ -603,12 +637,25 @@ module LogBrew
|
|
|
603
637
|
|
|
604
638
|
def report(error, handled: true, severity: :error, context: nil, source: nil, **_options)
|
|
605
639
|
capture_safely do
|
|
606
|
-
attributes =
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
640
|
+
attributes = if error.is_a?(Exception)
|
|
641
|
+
IssueDiagnostics.from_exception(
|
|
642
|
+
error,
|
|
643
|
+
title: error_title(error),
|
|
644
|
+
level: issue_level(severity),
|
|
645
|
+
message: @include_exception_message ? error_message(error) : nil,
|
|
646
|
+
mechanism_type: "rails.error_reporter",
|
|
647
|
+
handled: handled,
|
|
648
|
+
metadata: rails_metadata(error, handled, severity, context, source)
|
|
649
|
+
)
|
|
650
|
+
else
|
|
651
|
+
{
|
|
652
|
+
title: error_title(error),
|
|
653
|
+
level: issue_level(severity),
|
|
654
|
+
metadata: rails_metadata(error, handled, severity, context, source)
|
|
655
|
+
}.tap do |fallback|
|
|
656
|
+
fallback[:message] = error_message(error) if @include_exception_message
|
|
657
|
+
end
|
|
658
|
+
end
|
|
612
659
|
@client.issue(next_event_id, logbrew_timestamp, attributes)
|
|
613
660
|
flush_if_configured
|
|
614
661
|
end
|
|
@@ -668,11 +715,30 @@ module LogBrew
|
|
|
668
715
|
end
|
|
669
716
|
|
|
670
717
|
def add_exception_metadata(metadata, exception)
|
|
671
|
-
|
|
718
|
+
exception_type = IssueDiagnostics.safe_exception_type(exception)
|
|
719
|
+
frames = IssueDiagnostics.stack_frames_from_exception(exception)
|
|
720
|
+
first_frame = frames.first
|
|
721
|
+
metadata["exceptionType"] = exception_type
|
|
722
|
+
metadata["errorName"] = exception_type
|
|
723
|
+
metadata["handled"] = metadata.fetch("rails.handled")
|
|
724
|
+
metadata["mechanism"] = "rails.error_reporter"
|
|
725
|
+
metadata["issueGroupingKey"] = rails_error_grouping_key(exception_type, metadata, first_frame)
|
|
726
|
+
metadata["issueGroupingSource"] = "exception_type_source_file"
|
|
727
|
+
unless first_frame.nil?
|
|
728
|
+
metadata["errorFrameFile"] = first_frame.fetch("filename")
|
|
729
|
+
metadata["errorFrameLine"] = first_frame.fetch("line")
|
|
730
|
+
end
|
|
672
731
|
metadata["exceptionMessage"] = exception.message if @include_exception_message
|
|
673
732
|
metadata["exceptionBacktrace"] = exception.backtrace.join("\n") if @include_exception_backtrace && exception.backtrace
|
|
674
733
|
end
|
|
675
734
|
|
|
735
|
+
def rails_error_grouping_key(exception_type, metadata, first_frame)
|
|
736
|
+
source = metadata.fetch("rails.source", "rails")
|
|
737
|
+
frame_file = first_frame.nil? ? "" : first_frame.fetch("filename")
|
|
738
|
+
material = [exception_type, source, frame_file].join("\n")
|
|
739
|
+
"rails-exception-#{Digest::SHA256.hexdigest(material)}"
|
|
740
|
+
end
|
|
741
|
+
|
|
676
742
|
def copy_metadata(metadata)
|
|
677
743
|
metadata.each_with_object({}) do |(key, value), copied|
|
|
678
744
|
copied[key.to_s] = value if primitive_metadata_value?(value)
|
|
@@ -759,6 +825,26 @@ module LogBrew
|
|
|
759
825
|
value
|
|
760
826
|
end
|
|
761
827
|
|
|
828
|
+
def normalize_metric_description(value)
|
|
829
|
+
unless value.is_a?(String) && value.valid_encoding?
|
|
830
|
+
raise SdkError.new(
|
|
831
|
+
"validation_error",
|
|
832
|
+
"metric description must be a non-blank string of at most 1024 non-control characters"
|
|
833
|
+
)
|
|
834
|
+
end
|
|
835
|
+
normalized = value.strip
|
|
836
|
+
invalid = normalized.each_codepoint.any? do |character|
|
|
837
|
+
character <= 0x1f || (character >= 0x7f && character <= 0x9f) || character == 0x2028 || character == 0x2029
|
|
838
|
+
end
|
|
839
|
+
if normalized.empty? || normalized.each_codepoint.count > 1024 || invalid
|
|
840
|
+
raise SdkError.new(
|
|
841
|
+
"validation_error",
|
|
842
|
+
"metric description must be a non-blank string of at most 1024 non-control characters"
|
|
843
|
+
)
|
|
844
|
+
end
|
|
845
|
+
normalized
|
|
846
|
+
end
|
|
847
|
+
|
|
762
848
|
def read(attributes, key)
|
|
763
849
|
return nil unless attributes.is_a?(Hash)
|
|
764
850
|
|
|
@@ -790,12 +876,26 @@ module LogBrew
|
|
|
790
876
|
on_event_dropped: nil,
|
|
791
877
|
max_batch_size: EventBatcher::DEFAULT_MAX_SIZE,
|
|
792
878
|
max_batch_bytes: EventBatcher::DEFAULT_MAX_BYTES,
|
|
793
|
-
persistent_queue_path: nil
|
|
879
|
+
persistent_queue_path: nil,
|
|
880
|
+
context: nil,
|
|
881
|
+
capture_runtime_context: true
|
|
794
882
|
)
|
|
795
883
|
Validation.require_non_empty("api_key", api_key)
|
|
796
884
|
Validation.require_non_empty("sdk_name", sdk_name)
|
|
797
885
|
Validation.require_non_empty("sdk_version", sdk_version)
|
|
798
886
|
raise SdkError.new("validation_error", "max_retries must be non-negative") if max_retries.negative?
|
|
887
|
+
unless context.nil? || context.is_a?(TelemetryContext)
|
|
888
|
+
raise SdkError.new("validation_error", "client context must be a LogBrew::TelemetryContext")
|
|
889
|
+
end
|
|
890
|
+
unless capture_runtime_context == true || capture_runtime_context == false
|
|
891
|
+
raise SdkError.new("validation_error", "capture_runtime_context must be a boolean")
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
resolved_context = if capture_runtime_context
|
|
895
|
+
TelemetryContext.merge(TelemetryContext.runtime_defaults, context)
|
|
896
|
+
else
|
|
897
|
+
TelemetryContext.merge(nil, context)
|
|
898
|
+
end
|
|
799
899
|
|
|
800
900
|
new(
|
|
801
901
|
api_key: api_key,
|
|
@@ -810,7 +910,8 @@ module LogBrew
|
|
|
810
910
|
on_event_dropped: on_event_dropped,
|
|
811
911
|
max_batch_size: max_batch_size,
|
|
812
912
|
max_batch_bytes: max_batch_bytes,
|
|
813
|
-
persistent_queue_path: persistent_queue_path
|
|
913
|
+
persistent_queue_path: persistent_queue_path,
|
|
914
|
+
context: resolved_context
|
|
814
915
|
)
|
|
815
916
|
end
|
|
816
917
|
|
|
@@ -860,10 +961,12 @@ module LogBrew
|
|
|
860
961
|
on_event_dropped: nil,
|
|
861
962
|
max_batch_size: EventBatcher::DEFAULT_MAX_SIZE,
|
|
862
963
|
max_batch_bytes: EventBatcher::DEFAULT_MAX_BYTES,
|
|
863
|
-
persistent_queue_path: nil
|
|
964
|
+
persistent_queue_path: nil,
|
|
965
|
+
context: nil
|
|
864
966
|
)
|
|
865
967
|
@api_key = api_key
|
|
866
968
|
@sdk = sdk
|
|
969
|
+
@context = context
|
|
867
970
|
@max_retries = max_retries
|
|
868
971
|
@event_batcher = EventBatcher.new(sdk: sdk, max_size: max_batch_size, max_bytes: max_batch_bytes)
|
|
869
972
|
event_store = nil
|
|
@@ -954,31 +1057,31 @@ module LogBrew
|
|
|
954
1057
|
end
|
|
955
1058
|
|
|
956
1059
|
def release(id, timestamp, attributes)
|
|
957
|
-
push_event("release", id, timestamp, validate_release(attributes))
|
|
1060
|
+
push_event("release", id, timestamp, validate_release(attributes), event_context(attributes))
|
|
958
1061
|
end
|
|
959
1062
|
|
|
960
1063
|
def environment(id, timestamp, attributes)
|
|
961
|
-
push_event("environment", id, timestamp, validate_environment(attributes))
|
|
1064
|
+
push_event("environment", id, timestamp, validate_environment(attributes), event_context(attributes))
|
|
962
1065
|
end
|
|
963
1066
|
|
|
964
1067
|
def issue(id, timestamp, attributes)
|
|
965
|
-
push_event("issue", id, timestamp, validate_issue(attributes))
|
|
1068
|
+
push_event("issue", id, timestamp, validate_issue(attributes), event_context(attributes))
|
|
966
1069
|
end
|
|
967
1070
|
|
|
968
1071
|
def log(id, timestamp, attributes)
|
|
969
|
-
push_event("log", id, timestamp, validate_log(attributes))
|
|
1072
|
+
push_event("log", id, timestamp, validate_log(attributes), event_context(attributes))
|
|
970
1073
|
end
|
|
971
1074
|
|
|
972
1075
|
def span(id, timestamp, attributes)
|
|
973
|
-
push_event("span", id, timestamp, validate_span(attributes))
|
|
1076
|
+
push_event("span", id, timestamp, validate_span(attributes), event_context(attributes))
|
|
974
1077
|
end
|
|
975
1078
|
|
|
976
1079
|
def metric(id, timestamp, attributes)
|
|
977
|
-
push_event("metric", id, timestamp, validate_metric(attributes))
|
|
1080
|
+
push_event("metric", id, timestamp, validate_metric(attributes), event_context(attributes))
|
|
978
1081
|
end
|
|
979
1082
|
|
|
980
1083
|
def action(id, timestamp, attributes)
|
|
981
|
-
push_event("action", id, timestamp, validate_action(attributes))
|
|
1084
|
+
push_event("action", id, timestamp, validate_action(attributes), event_context(attributes))
|
|
982
1085
|
end
|
|
983
1086
|
|
|
984
1087
|
def flush(transport = nil)
|
|
@@ -1035,10 +1138,18 @@ module LogBrew
|
|
|
1035
1138
|
|
|
1036
1139
|
private
|
|
1037
1140
|
|
|
1038
|
-
def push_event(type, id, timestamp, attributes)
|
|
1141
|
+
def push_event(type, id, timestamp, attributes, event_context)
|
|
1039
1142
|
@automatic_delivery&.assert_process_ownership!
|
|
1040
1143
|
Validation.require_non_empty("event id", id)
|
|
1041
1144
|
Validation.require_timestamp(timestamp)
|
|
1145
|
+
ambient_context = Telemetry.current_context
|
|
1146
|
+
active_trace = defined?(LogBrew::Trace) ? LogBrew::Trace.current : nil
|
|
1147
|
+
ambient_context = TelemetryContext.with_trace(ambient_context, active_trace) unless active_trace.nil?
|
|
1148
|
+
merged_context = TelemetryContext.merge(
|
|
1149
|
+
TelemetryContext.merge(@context, ambient_context),
|
|
1150
|
+
event_context
|
|
1151
|
+
)
|
|
1152
|
+
attributes["context"] = merged_context.to_h unless merged_context.nil?
|
|
1042
1153
|
event = {
|
|
1043
1154
|
"type" => type,
|
|
1044
1155
|
"timestamp" => timestamp,
|
|
@@ -1217,20 +1328,7 @@ module LogBrew
|
|
|
1217
1328
|
end
|
|
1218
1329
|
|
|
1219
1330
|
def validate_issue(attributes)
|
|
1220
|
-
|
|
1221
|
-
level = Validation.read(attributes, "level")
|
|
1222
|
-
Validation.require_non_empty("issue title", title)
|
|
1223
|
-
level = normalize_severity("issue level", level)
|
|
1224
|
-
with_metadata(
|
|
1225
|
-
{
|
|
1226
|
-
"title" => title,
|
|
1227
|
-
"level" => level
|
|
1228
|
-
}.tap do |payload|
|
|
1229
|
-
message = Validation.read(attributes, "message")
|
|
1230
|
-
payload["message"] = message unless message.nil?
|
|
1231
|
-
end,
|
|
1232
|
-
attributes
|
|
1233
|
-
)
|
|
1331
|
+
IssueDiagnostics.validate_issue_attributes(attributes)
|
|
1234
1332
|
end
|
|
1235
1333
|
|
|
1236
1334
|
def validate_log(attributes)
|
|
@@ -1290,6 +1388,8 @@ module LogBrew
|
|
|
1290
1388
|
|
|
1291
1389
|
def validate_metric(attributes)
|
|
1292
1390
|
name = Validation.read(attributes, "name")
|
|
1391
|
+
description_present = attributes.is_a?(Hash) && (attributes.key?("description") || attributes.key?(:description))
|
|
1392
|
+
description = Validation.normalize_metric_description(Validation.read(attributes, "description")) if description_present
|
|
1293
1393
|
kind = Validation.read(attributes, "kind")
|
|
1294
1394
|
unit = Validation.read(attributes, "unit")
|
|
1295
1395
|
temporality = Validation.read(attributes, "temporality")
|
|
@@ -1305,11 +1405,12 @@ module LogBrew
|
|
|
1305
1405
|
with_metadata(
|
|
1306
1406
|
{
|
|
1307
1407
|
"name" => name,
|
|
1408
|
+
"description" => description,
|
|
1308
1409
|
"kind" => kind,
|
|
1309
1410
|
"value" => value,
|
|
1310
1411
|
"unit" => unit,
|
|
1311
1412
|
"temporality" => temporality
|
|
1312
|
-
},
|
|
1413
|
+
}.compact,
|
|
1313
1414
|
attributes
|
|
1314
1415
|
)
|
|
1315
1416
|
end
|
|
@@ -1322,6 +1423,23 @@ module LogBrew
|
|
|
1322
1423
|
with_metadata({ "name" => name, "status" => status }, attributes)
|
|
1323
1424
|
end
|
|
1324
1425
|
|
|
1426
|
+
def event_context(attributes)
|
|
1427
|
+
return nil unless attributes.is_a?(Hash)
|
|
1428
|
+
|
|
1429
|
+
string_key = attributes.key?("context")
|
|
1430
|
+
symbol_key = attributes.key?(:context)
|
|
1431
|
+
if string_key && symbol_key
|
|
1432
|
+
raise SdkError.new("validation_error", "event context must use one context field")
|
|
1433
|
+
end
|
|
1434
|
+
return nil unless string_key || symbol_key
|
|
1435
|
+
|
|
1436
|
+
value = string_key ? attributes["context"] : attributes[:context]
|
|
1437
|
+
unless value.is_a?(TelemetryContext)
|
|
1438
|
+
raise SdkError.new("validation_error", "event context must be a LogBrew::TelemetryContext")
|
|
1439
|
+
end
|
|
1440
|
+
value
|
|
1441
|
+
end
|
|
1442
|
+
|
|
1325
1443
|
def with_metadata(payload, attributes)
|
|
1326
1444
|
metadata = Validation.require_metadata(Validation.read(attributes, "metadata"))
|
|
1327
1445
|
payload["metadata"] = metadata unless metadata.nil?
|
|
@@ -1332,6 +1450,6 @@ module LogBrew
|
|
|
1332
1450
|
require_relative "logbrew/automatic_delivery"
|
|
1333
1451
|
end
|
|
1334
1452
|
|
|
1335
|
-
%w[product_timeline traceparent trace span_events operation_tracing http_client_tracing support_ticket worker_lifecycle].each do |path|
|
|
1453
|
+
%w[issue_diagnostics product_timeline traceparent trace span_events operation_tracing http_client_tracing support_ticket worker_lifecycle].each do |path|
|
|
1336
1454
|
require_relative "logbrew/#{path}"
|
|
1337
1455
|
end
|
metadata
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: logbrew-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LogBrew
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description: Public LogBrew Ruby SDK with
|
|
14
|
-
standard-library delivery.
|
|
13
|
+
description: Public LogBrew Ruby SDK with typed issue diagnostics, automatic Rails
|
|
14
|
+
request/error capture, and standard-library delivery.
|
|
15
15
|
email:
|
|
16
16
|
executables: []
|
|
17
17
|
extensions: []
|
|
@@ -22,6 +22,7 @@ files:
|
|
|
22
22
|
- examples/automatic_delivery.rb
|
|
23
23
|
- examples/first_useful_telemetry.rb
|
|
24
24
|
- examples/http_trace_correlation.rb
|
|
25
|
+
- examples/issue_diagnostics.rb
|
|
25
26
|
- examples/persistent_worker_delivery.rb
|
|
26
27
|
- examples/readme_example.rb
|
|
27
28
|
- examples/real_user_smoke.rb
|
|
@@ -33,6 +34,7 @@ files:
|
|
|
33
34
|
- lib/logbrew/event_batcher.rb
|
|
34
35
|
- lib/logbrew/faraday_tracing.rb
|
|
35
36
|
- lib/logbrew/http_client_tracing.rb
|
|
37
|
+
- lib/logbrew/issue_diagnostics.rb
|
|
36
38
|
- lib/logbrew/operation_tracing.rb
|
|
37
39
|
- lib/logbrew/persistent_event_store.rb
|
|
38
40
|
- lib/logbrew/product_timeline.rb
|
|
@@ -41,6 +43,10 @@ files:
|
|
|
41
43
|
- lib/logbrew/sidekiq.rb
|
|
42
44
|
- lib/logbrew/span_events.rb
|
|
43
45
|
- lib/logbrew/support_ticket.rb
|
|
46
|
+
- lib/logbrew/telemetry.rb
|
|
47
|
+
- lib/logbrew/telemetry_context.rb
|
|
48
|
+
- lib/logbrew/telemetry_context_value.rb
|
|
49
|
+
- lib/logbrew/telemetry_resource.rb
|
|
44
50
|
- lib/logbrew/trace.rb
|
|
45
51
|
- lib/logbrew/traceparent.rb
|
|
46
52
|
- lib/logbrew/version.rb
|