pg_eventstore 3.0.1 → 3.1.1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -8
  3. data/README.md +1 -0
  4. data/docs/admin_ui.md +5 -0
  5. data/docs/metrics.md +233 -0
  6. data/lib/pg_eventstore/chunks/subscription_events_index_chunk.rb +1 -3
  7. data/lib/pg_eventstore/queries/replica_queries.rb +8 -5
  8. data/lib/pg_eventstore/sql_builder.rb +1 -1
  9. data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +1 -1
  10. data/lib/pg_eventstore/subscriptions/runner_recovery_strategies/report_subscription_unrecoverable_error.rb +31 -0
  11. data/lib/pg_eventstore/subscriptions/runner_recovery_strategies.rb +1 -0
  12. data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +5 -0
  13. data/lib/pg_eventstore/utils.rb +2 -0
  14. data/lib/pg_eventstore/version.rb +1 -1
  15. data/lib/pg_eventstore/web/application.rb +7 -0
  16. data/lib/pg_eventstore/web/metrics/application.rb +64 -0
  17. data/lib/pg_eventstore/web/metrics/collectors/base.rb +69 -0
  18. data/lib/pg_eventstore/web/metrics/collectors/subscriptions_health.rb +78 -0
  19. data/lib/pg_eventstore/web/metrics/collectors/subscriptions_latency.rb +161 -0
  20. data/lib/pg_eventstore/web/metrics/collectors/subscriptions_throughput.rb +59 -0
  21. data/lib/pg_eventstore/web/metrics/formatter.rb +45 -0
  22. data/lib/pg_eventstore/web/metrics/helpers.rb +30 -0
  23. data/lib/pg_eventstore/web/metrics/metric_family.rb +40 -0
  24. data/lib/pg_eventstore/web/paginator/helpers.rb +27 -23
  25. data/lib/pg_eventstore/web/views/home/partials/events.erb +8 -6
  26. data/lib/pg_eventstore/web/views/home/partials/stream_filter.erb +1 -1
  27. data/lib/pg_eventstore/web/views/streams/partials/streams.erb +1 -1
  28. data/lib/pg_eventstore/web.rb +8 -0
  29. data/rbs_collection.lock.yaml +1 -1
  30. data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +1 -1
  31. data/sig/pg_eventstore/subscriptions/runner_recovery_strategies/report_subscription_unrecoverable_error.rbs +15 -0
  32. data/sig/pg_eventstore/web/application.rbs +2 -0
  33. data/sig/pg_eventstore/web/metrics/application.rbs +11 -0
  34. data/sig/pg_eventstore/web/metrics/collectors/base.rbs +33 -0
  35. data/sig/pg_eventstore/web/metrics/collectors/subscriptions_health.rbs +15 -0
  36. data/sig/pg_eventstore/web/metrics/collectors/subscriptions_latency.rbs +31 -0
  37. data/sig/pg_eventstore/web/metrics/collectors/subscriptions_throughput.rbs +15 -0
  38. data/sig/pg_eventstore/web/metrics/formatter.rbs +19 -0
  39. data/sig/pg_eventstore/web/metrics/helpers.rbs +11 -0
  40. data/sig/pg_eventstore/web/metrics/metric_family.rbs +19 -0
  41. data/sig/pg_eventstore/web/paginator/helpers.rbs +5 -3
  42. metadata +20 -1
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Web
5
+ module Metrics
6
+ module Collectors
7
+ # Liveness and error state of each reported subscription.
8
+ #
9
+ # The state column alone can not be trusted: a subscription killed without a graceful shutdown keeps
10
+ # state "running" and its lock forever. heartbeat_age_seconds is the discriminator - a subscription is
11
+ # really running only while its heartbeat stays below
12
+ # {PgEventstore::SubscriptionsLifecycle::HEARTBEAT_INTERVAL}.
13
+ class SubscriptionsHealth < Base
14
+ # @return [Array<PgEventstore::Web::Metrics::MetricFamily>]
15
+ def call
16
+ state = MetricFamily.new(
17
+ name: 'pg_eventstore_subscription_state',
18
+ type: 'gauge',
19
+ help: 'Last recorded state of the subscription. May be stale - correlate with ' \
20
+ 'pg_eventstore_subscription_heartbeat_age_seconds.'
21
+ )
22
+ locked = MetricFamily.new(
23
+ name: 'pg_eventstore_subscription_locked',
24
+ type: 'gauge',
25
+ help: 'Whether the subscription is locked by a subscriptions set.'
26
+ )
27
+ heartbeat_age = MetricFamily.new(
28
+ name: 'pg_eventstore_subscription_heartbeat_age_seconds',
29
+ type: 'gauge',
30
+ help: 'Seconds since the subscription row was last touched by its runner. A locked subscription ' \
31
+ 'with a stale heartbeat is a dead process that did not shut down gracefully.'
32
+ )
33
+ restarts = MetricFamily.new(
34
+ name: 'pg_eventstore_subscription_restarts_total',
35
+ type: 'counter',
36
+ help: 'Number of times the subscription was restarted after a failure.'
37
+ )
38
+ last_error_age = MetricFamily.new(
39
+ name: 'pg_eventstore_subscription_last_error_age_seconds',
40
+ type: 'gauge',
41
+ help: 'Seconds since the last error occurred. Absent when the subscription never failed.'
42
+ )
43
+ subscription_rows.each do |row|
44
+ labels = subscription_labels(row)
45
+ state.add_sample(labels: labels.merge(state: row['state']), value: 1)
46
+ locked.add_sample(labels:, value: row['locked'])
47
+ heartbeat_age.add_sample(labels:, value: row['heartbeat_age_seconds'])
48
+ restarts.add_sample(labels:, value: row['restart_count'])
49
+ last_error_age.add_sample(labels:, value: row['last_error_age_seconds']) if row['last_error_age_seconds']
50
+ end
51
+ [state, locked, heartbeat_age, restarts, last_error_age]
52
+ end
53
+
54
+ private
55
+
56
+ # @return [Array<Hash>]
57
+ def subscription_rows
58
+ builder = subscriptions_sql_builder
59
+ builder.select(<<~SQL)
60
+ s.set,
61
+ s.name,
62
+ s.state,
63
+ (s.locked_by is not null)::int as locked,
64
+ extract(epoch from ((now() at time zone 'utc') - s.updated_at))::float8 as heartbeat_age_seconds,
65
+ s.restart_count,
66
+ case when s.last_error_occurred_at is not null
67
+ then extract(epoch from ((now() at time zone 'utc') - s.last_error_occurred_at))::float8
68
+ end as last_error_age_seconds
69
+ SQL
70
+ with_safe_conn do |conn|
71
+ conn.exec_params(*builder.to_exec_params)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Web
5
+ module Metrics
6
+ module Collectors
7
+ # How far behind each subscription is.
8
+ #
9
+ # A subscription checkpoint is measured in the same units as the subscription positions frontier, not in
10
+ # events.global_position units - the global position sequence contains gaps and runs ahead of the frontier,
11
+ # so comparing a checkpoint against it over-reports by orders of magnitude. Lag is therefore always measured
12
+ # against the frontier.
13
+ #
14
+ # - lag_events: how much of the store the subscription still has to walk through before it reaches the
15
+ # frontier - that is, before it starts processing newly appended events.
16
+ # - lag_seconds: age of the oldest event the subscription has not processed yet; 0 when caught up.
17
+ #
18
+ # The frontier only advances while at least one subscriptions process is running. When they are all down lag
19
+ # stops growing - that situation is reported by the heartbeat metric of the health collector, not here.
20
+ class SubscriptionsLatency < Base
21
+ # @return [Array<PgEventstore::Web::Metrics::MetricFamily>]
22
+ def call
23
+ lag_events = MetricFamily.new(
24
+ name: 'pg_eventstore_subscription_lag_events',
25
+ type: 'gauge',
26
+ help: 'How many events the subscription still has to catch up on before it reaches the edge of the ' \
27
+ '"all" stream.'
28
+ )
29
+ lag_seconds = MetricFamily.new(
30
+ name: 'pg_eventstore_subscription_lag_seconds',
31
+ type: 'gauge',
32
+ help: 'Age in seconds of the oldest event the subscription has not processed yet. 0 when caught up.'
33
+ )
34
+ frontier_position, head_global_position = *positions
35
+ rows = subscription_rows(frontier_position)
36
+ created_at_by_position = resolve_created_at(rows)
37
+ now = Time.now.utc
38
+ rows.each { add_samples(_1, created_at_by_position, now, lag_events, lag_seconds) }
39
+ [lag_events, lag_seconds, *store_families(frontier_position, head_global_position)]
40
+ end
41
+
42
+ private
43
+
44
+ # @param row [Hash]
45
+ # @param created_at_by_position [Hash<Integer => Time>]
46
+ # @param now [Time]
47
+ # @param lag_events [PgEventstore::Web::Metrics::MetricFamily]
48
+ # @param lag_seconds [PgEventstore::Web::Metrics::MetricFamily]
49
+ # @return [void]
50
+ def add_samples(row, created_at_by_position, now, lag_events, lag_seconds)
51
+ labels = subscription_labels(row)
52
+ lag_events.add_sample(labels:, value: row['lag_events'])
53
+ position = row['event_global_position']
54
+ # Caught up - nothing is left to process, so there is no unprocessed event to age. Deliberately a float:
55
+ # the metric is seconds everywhere else, and a gauge that renders as "0" here and "12.5" there is a wart.
56
+ return lag_seconds.add_sample(labels:, value: 0.0) if position.nil?
57
+
58
+ created_at = created_at_by_position[position]
59
+ # An unprocessed position whose event no longer exists (the event or its stream was deleted). Reporting 0
60
+ # would read as "caught up", the opposite of the truth, so report nothing - lag_events still carries the
61
+ # backlog.
62
+ return if created_at.nil?
63
+
64
+ lag_seconds.add_sample(labels:, value: [(now - created_at).to_f, 0].max)
65
+ end
66
+
67
+ # One index range scan per subscription over idx_event_subscription_positions_sposition_n_gposition. The
68
+ # cost does not grow with the size of the backlog: the oldest unprocessed position is taken with
69
+ # "order by subscription_position limit 1" rather than by aggregating over the whole backlog.
70
+ # @param frontier_position [Integer]
71
+ # @return [Array<Hash>]
72
+ def subscription_rows(frontier_position)
73
+ builder = subscriptions_sql_builder
74
+ builder.select(<<~SQL)
75
+ s.set,
76
+ s.name,
77
+ greatest(#{frontier_position} - coalesce(s.current_position, 0), 0) as lag_events,
78
+ next_event.global_position as event_global_position,
79
+ next_event.event_type_partition_id as event_type_partition_id
80
+ SQL
81
+ builder.join(<<~SQL)
82
+ left join lateral (
83
+ select egi.global_position, egi.event_type_partition_id
84
+ from event_subscription_positions esp
85
+ join events_global_index egi on egi.global_position = esp.global_position
86
+ where esp.subscription_position > coalesce(s.current_position, 0)
87
+ order by esp.subscription_position
88
+ limit 1
89
+ ) next_event on true
90
+ SQL
91
+ with_safe_conn do |conn|
92
+ conn.exec_params(*builder.to_exec_params)
93
+ end
94
+ end
95
+
96
+ # Resolves the creation time of every subscription's oldest unprocessed event.
97
+ #
98
+ # The events table is partitioned, and these events are known only by global position - which is not the
99
+ # partition key. Looking them up in events by global position alone would have to visit every partition and
100
+ # lock all of them, which stops being viable long before a store reaches five figures of partitions.
101
+ # events_global_index records the partition of each event, so the read API can resolve them partition-wise
102
+ # instead.
103
+ # @param subscription_rows [Array<Hash>]
104
+ # @return [Hash<Integer => Time>]
105
+ def resolve_created_at(subscription_rows)
106
+ indexes = subscription_rows.filter_map do |attrs|
107
+ next if attrs['event_global_position'].nil?
108
+
109
+ EventGlobalIndex::ReadApiRepr.new(
110
+ global_position: attrs['event_global_position'],
111
+ event_type_partition_id: attrs['event_type_partition_id']
112
+ )
113
+ end
114
+ return {} if indexes.empty?
115
+
116
+ resolved = events_global_index_queries.resolve_indexes(indexes, resolve_link_tos: false)
117
+ resolved.to_h { [_1['global_position'], _1['created_at']] }
118
+ end
119
+
120
+ # @return [PgEventstore::EventsGlobalIndexQueries]
121
+ def events_global_index_queries
122
+ EventsGlobalIndexQueries.new(connection, QueryStrategy::Foreground.new(connection))
123
+ end
124
+
125
+ # @param frontier_position [Integer]
126
+ # @param head_global_position [Integer]
127
+ # @return [Array<PgEventstore::Web::Metrics::MetricFamily>]
128
+ def store_families(frontier_position, head_global_position)
129
+ frontier = MetricFamily.new(
130
+ name: 'pg_eventstore_store_frontier_position',
131
+ type: 'gauge',
132
+ help: 'Latest assigned subscription position. Subscription checkpoints are measured against this.'
133
+ )
134
+ frontier.add_sample(labels: {}, value: frontier_position)
135
+ head = MetricFamily.new(
136
+ name: 'pg_eventstore_store_head_global_position',
137
+ type: 'gauge',
138
+ help: 'Global position of the newest event in the store. Contains gaps; do not compare subscription ' \
139
+ 'checkpoints against it.'
140
+ )
141
+ head.add_sample(labels: {}, value: head_global_position)
142
+ [frontier, head]
143
+ end
144
+
145
+ # @return [Array[Integer]]
146
+ def positions
147
+ res = with_safe_conn do |conn|
148
+ conn.exec(<<~SQL)
149
+ select
150
+ (select coalesce(max(subscription_position), 0) from event_subscription_positions)
151
+ as frontier_position,
152
+ (select coalesce(max(global_position), 0) from events_global_index) as head_global_position
153
+ SQL
154
+ end
155
+ res.first.values_at('frontier_position', 'head_global_position')
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Web
5
+ module Metrics
6
+ module Collectors
7
+ # Processing volume and speed of each reported subscription.
8
+ #
9
+ # Two deliberately different numbers:
10
+ # - processed_events_total is a counter; rate() over it gives the actual current throughput and correctly
11
+ # drops to 0 when no matching events arrive.
12
+ # - capacity_events_per_second derives from the average handler execution time of the last
13
+ # {PgEventstore::SubscriptionHandlerPerformance::TIMINGS_TO_KEEP} processed events - whenever they
14
+ # happened. It answers "how fast can this handler go when fed", is sticky while the subscription is idle
15
+ # and must not be read as current throughput.
16
+ class SubscriptionsThroughput < Base
17
+ # @return [Array<PgEventstore::Web::Metrics::MetricFamily>]
18
+ def call
19
+ processed = MetricFamily.new(
20
+ name: 'pg_eventstore_subscription_processed_events_total',
21
+ type: 'counter',
22
+ help: 'Total number of events processed by the subscription. Use rate() for current throughput.'
23
+ )
24
+ capacity = MetricFamily.new(
25
+ name: 'pg_eventstore_subscription_capacity_events_per_second',
26
+ type: 'gauge',
27
+ help: 'Average processing speed over the last few processed events, whenever they happened. ' \
28
+ 'Sticky while idle - this is handler capacity, not current throughput.'
29
+ )
30
+ subscription_rows.each do |row|
31
+ labels = subscription_labels(row)
32
+ processed.add_sample(labels:, value: row['total_processed_events'])
33
+ capacity.add_sample(labels:, value: row['capacity_eps']) if row['capacity_eps']
34
+ end
35
+ [processed, capacity]
36
+ end
37
+
38
+ private
39
+
40
+ # @return [Array<Hash>]
41
+ def subscription_rows
42
+ builder = subscriptions_sql_builder
43
+ builder.select(<<~SQL)
44
+ s.set,
45
+ s.name,
46
+ s.total_processed_events,
47
+ case when s.average_event_processing_time > 0
48
+ then (1.0 / s.average_event_processing_time)::float8
49
+ end as capacity_eps
50
+ SQL
51
+ with_safe_conn do |conn|
52
+ conn.exec_params(*builder.to_exec_params)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Web
5
+ module Metrics
6
+ # Renders metric families into the Prometheus text exposition format (version 0.0.4).
7
+ class Formatter
8
+ # @return [String]
9
+ CONTENT_TYPE = 'text/plain; version=0.0.4; charset=utf-8'
10
+
11
+ # @param families [Array<PgEventstore::Web::Metrics::MetricFamily>]
12
+ # @return [String]
13
+ def call(families)
14
+ "#{families.map { |family| format_family(family) }.join("\n")}\n"
15
+ end
16
+
17
+ private
18
+
19
+ # @param family [PgEventstore::Web::Metrics::MetricFamily]
20
+ # @return [String]
21
+ def format_family(family)
22
+ lines = ["# HELP #{family.name} #{family.help}", "# TYPE #{family.name} #{family.type}"]
23
+ family.samples.each do |sample|
24
+ lines.push("#{family.name}#{format_labels(sample[:labels])} #{sample[:value]}")
25
+ end
26
+ lines.join("\n")
27
+ end
28
+
29
+ # @param labels [Hash<Symbol => String>]
30
+ # @return [String]
31
+ def format_labels(labels)
32
+ return '' if labels.empty?
33
+
34
+ "{#{labels.map { |name, value| %(#{name}="#{escape_label_value(value.to_s)}") }.join(',')}}"
35
+ end
36
+
37
+ # @param value [String]
38
+ # @return [String]
39
+ def escape_label_value(value)
40
+ value.gsub('\\', '\\\\\\\\').gsub("\n", '\\n').gsub('"', '\\"')
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Web
5
+ module Metrics
6
+ # Route helpers of {Metrics::Application}.
7
+ module Helpers
8
+ # @param collector_classes [Array<Class<PgEventstore::Web::Metrics::Collectors::Base>>]
9
+ # @return [String]
10
+ def metrics_response(collector_classes)
11
+ connection = metrics_connection
12
+ families = collector_classes.flat_map { _1.new(connection, sets: requested_sets).call }
13
+ content_type(Formatter::CONTENT_TYPE)
14
+ Formatter.new.call(families)
15
+ end
16
+
17
+ # Subscription sets a scrape asks for. Accepts a repeated param ("?set=A&set=B" - what Prometheus emits for
18
+ # `params: {set: [A, B]}`) or a comma separated list ("?set=A,B"). Empty means every set.
19
+ #
20
+ # The query string is parsed directly because Sinatra's `params` keeps only the last value of a repeated key
21
+ # unless it is written as "set[]", which Prometheus does not do.
22
+ # @return [Array<String>]
23
+ def requested_sets
24
+ query = Rack::Utils.parse_query(request.query_string)
25
+ Array(query['set'] || query['set[]']).flat_map { _1.to_s.split(',') }.map(&:strip).reject(&:empty?)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgEventstore
4
+ module Web
5
+ module Metrics
6
+ # A collection of samples of a single Prometheus metric.
7
+ class MetricFamily
8
+ # @!attribute name
9
+ # @return [String]
10
+ attr_reader :name
11
+ # @!attribute type
12
+ # @return [String] "gauge" or "counter"
13
+ attr_reader :type
14
+ # @!attribute help
15
+ # @return [String]
16
+ attr_reader :help
17
+ # @!attribute samples
18
+ # @return [Array<Hash>]
19
+ attr_reader :samples
20
+
21
+ # @param name [String]
22
+ # @param type [String]
23
+ # @param help [String]
24
+ def initialize(name:, type:, help:)
25
+ @name = name
26
+ @type = type
27
+ @help = help
28
+ @samples = []
29
+ end
30
+
31
+ # @param labels [Hash<Symbol => String>]
32
+ # @param value [Integer, Float]
33
+ # @return [void]
34
+ def add_sample(labels:, value:)
35
+ @samples.push({ labels:, value: })
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -30,7 +30,7 @@ module PgEventstore
30
30
 
31
31
  # @return [String]
32
32
  def first_page_link
33
- path = build_path(params.slice(*(params.keys - ['starting_id'])))
33
+ path = url(build_path(params.slice(*(params.keys - ['starting_id']))))
34
34
  <<~HTML
35
35
  <li class="page-item">
36
36
  <a class="page-link" href="#{path}" tabindex="-1">First</a>
@@ -41,17 +41,19 @@ module PgEventstore
41
41
  # @param per_page [String] string representation of items per page. E.g. "10", "20", etc.
42
42
  # @return [String]
43
43
  def per_page_url(per_page)
44
- build_path(params.merge(per_page:))
44
+ url(build_path(params.merge(per_page:)))
45
45
  end
46
46
 
47
47
  # @param order [String] "asc"/"desc"
48
48
  # @return [String]
49
49
  def sort_url(order)
50
- build_path(params.merge(order:))
50
+ url(build_path(params.merge(order:)))
51
51
  end
52
52
 
53
+ # @param should_resolve [Boolean]
54
+ # @return [String]
53
55
  def resolve_link_tos_url(should_resolve)
54
- build_path(params.merge(resolve_link_tos: should_resolve))
56
+ url(build_path(params.merge(resolve_link_tos: should_resolve)))
55
57
  end
56
58
 
57
59
  # @param number [Integer] total number of events by the current filter
@@ -82,27 +84,29 @@ module PgEventstore
82
84
 
83
85
  # @param stream [PgEventstore::Stream]
84
86
  # @return [String]
85
- def stream_path(stream)
86
- build_path(
87
- {
88
- filter: {
89
- streams: [
90
- {
91
- context: escape_empty_string(stream.context),
92
- stream_name: escape_empty_string(stream.stream_name),
93
- stream_id: escape_empty_string(stream.stream_id),
94
- },
95
- ],
87
+ def stream_url(stream)
88
+ url(
89
+ build_path(
90
+ {
91
+ filter: {
92
+ streams: [
93
+ {
94
+ context: escape_empty_string(stream.context),
95
+ stream_name: escape_empty_string(stream.stream_name),
96
+ stream_id: escape_empty_string(stream.stream_id),
97
+ },
98
+ ],
99
+ },
96
100
  },
97
- },
98
- base_url: '/'
101
+ base_path: '/'
102
+ )
99
103
  )
100
104
  end
101
105
 
102
106
  # @param marker [String]
103
107
  # @return [String]
104
- def event_marker_path(marker)
105
- build_path({ filter: { markers: [escape_empty_string(marker)] } }, base_url: '/')
108
+ def event_marker_url(marker)
109
+ url(build_path({ filter: { markers: [escape_empty_string(marker)] } }, base_path: '/'))
106
110
  end
107
111
 
108
112
  # @param str [String]
@@ -120,16 +124,16 @@ module PgEventstore
120
124
  def build_starting_id_link(starting_id)
121
125
  return 'javascript: void(0);' unless starting_id
122
126
 
123
- build_path(params.merge(starting_id:))
127
+ url(build_path(params.merge(starting_id:)))
124
128
  end
125
129
 
126
130
  # @param params [Hash, Array]
127
131
  # @return [String]
128
- def build_path(params, base_url: request.path)
132
+ def build_path(params, base_path: request.path_info)
129
133
  encoded_params = Rack::Utils.build_nested_query(params)
130
- return base_url if encoded_params.empty?
134
+ return base_path if encoded_params.empty?
131
135
 
132
- "#{base_url}?#{encoded_params}"
136
+ "#{base_path}?#{encoded_params}"
133
137
  end
134
138
  end
135
139
  end
@@ -12,7 +12,7 @@
12
12
  <td><%= empty_characters_fallback(h event.stream.context) %></td>
13
13
  <td><%= empty_characters_fallback(h event.stream.stream_name) %></td>
14
14
  <td>
15
- <a href="<%= stream_path(event.stream) %>"><%= empty_characters_fallback(h event.stream.stream_id) %></a>
15
+ <a href="<%= stream_url(event.stream) %>"><%= empty_characters_fallback(h event.stream.stream_id) %></a>
16
16
  <a role="button" href="#" data-title="Copy stream definition." class="copy-to-clipboard"
17
17
  data-clipboard-content="<%= h "PgEventstore::Stream.new(context: #{event.stream.context.inspect}, stream_name: #{event.stream.stream_name.inspect}, stream_id: #{event.stream.stream_id.inspect})" %>">
18
18
  <i class="fa fa-clipboard"></i>
@@ -33,9 +33,11 @@
33
33
  <a href="javascript: void(0);" class="d-inline-block text-nowrap toggle-event-data">
34
34
  JSON <i class="fa fa-eye"></i>
35
35
  </a>
36
- <a href="javascript: void(0);" class="ml-2 btn btn-danger btn-small delete-event-btn" data-global-position="<%= event.global_position %>" data-url="<%= delete_event_url(event.global_position) %>" data-toggle="modal" data-target="#delete-event-modal">
37
- Delete
38
- </a>
36
+ <% if can_delete? %>
37
+ <a href="javascript: void(0);" class="ml-2 btn btn-danger btn-small delete-event-btn" data-global-position="<%= event.global_position %>" data-url="<%= delete_event_url(event.global_position) %>" data-toggle="modal" data-target="#delete-event-modal">
38
+ Delete
39
+ </a>
40
+ <% end %>
39
41
  </td>
40
42
  </tr>
41
43
  <tr class="event-payload d-none">
@@ -55,7 +57,7 @@
55
57
  <% event.feature_markers.each do |feature_marker| %>
56
58
  <li>
57
59
  <%= "#{h feature_marker.description}:" if feature_marker.description %>
58
- <a href="<%= event_marker_path(feature_marker.marker) %>">
60
+ <a href="<%= event_marker_url(feature_marker.marker) %>">
59
61
  <%= empty_characters_fallback(h feature_marker.marker) %>
60
62
  </a>
61
63
  </li>
@@ -66,7 +68,7 @@
66
68
  Event markers
67
69
  <ul>
68
70
  <% event.markers.each do |marker| %>
69
- <li> <a href="<%= event_marker_path(marker) %>"><%= empty_characters_fallback(h marker) %></a> </li>
71
+ <li> <a href="<%= event_marker_url(marker) %>"><%= empty_characters_fallback(h marker) %></a> </li>
70
72
  <% end %>
71
73
  </ul>
72
74
  <% end %>
@@ -39,7 +39,7 @@
39
39
  <a class="btn btn-default remove-filter" href="javascript: void(0);">
40
40
  <i class="fa fa-minus-circle"></i>
41
41
  </a>
42
- <% if stream[:context] && stream[:stream_name] && stream[:stream_id] %>
42
+ <% if stream[:context] && stream[:stream_name] && stream[:stream_id] && can_delete? %>
43
43
  <a class="btn btn-danger btn-small delete-stream" data-confirm="You are about to delete all events in <%= h stream.inspect %> stream. This action is irreversible Continue?" data-confirm-title="Delete Stream" data-method="post" href="<%= delete_stream_url(stream) %>">
44
44
  Delete stream
45
45
  </a>
@@ -4,7 +4,7 @@
4
4
  <td><%= empty_characters_fallback(h stream.context) %></td>
5
5
  <td><%= empty_characters_fallback(h stream.stream_name) %></td>
6
6
  <td>
7
- <a href="<%= stream_path(stream) %>" target="_blank"><%= empty_characters_fallback(h stream.stream_id) %></a>
7
+ <a href="<%= stream_url(stream) %>" target="_blank"><%= empty_characters_fallback(h stream.stream_id) %></a>
8
8
  </td>
9
9
  <td><%= stream.stream_revision %></td>
10
10
  </tr>
@@ -19,6 +19,14 @@ require_relative 'web/subscriptions/with_state/set_collection'
19
19
  require_relative 'web/subscriptions/with_state/subscriptions'
20
20
  require_relative 'web/subscriptions/with_state/subscriptions_set'
21
21
  require_relative 'web/subscriptions/helpers'
22
+ require_relative 'web/metrics/metric_family'
23
+ require_relative 'web/metrics/formatter'
24
+ require_relative 'web/metrics/collectors/base'
25
+ require_relative 'web/metrics/collectors/subscriptions_latency'
26
+ require_relative 'web/metrics/collectors/subscriptions_health'
27
+ require_relative 'web/metrics/collectors/subscriptions_throughput'
28
+ require_relative 'web/metrics/helpers'
29
+ require_relative 'web/metrics/application'
22
30
  require_relative 'web/application'
23
31
 
24
32
  module PgEventstore
@@ -6,7 +6,7 @@ gems:
6
6
  source:
7
7
  type: git
8
8
  name: ruby/gem_rbs_collection
9
- revision: 0cb6351d218704700cf4df797ff8966b3a418fcd
9
+ revision: 9ab762c8247028f90620fc93c4e7aac20eb693d7
10
10
  remote: https://github.com/ruby/gem_rbs_collection.git
11
11
  repo_dir: gems
12
12
  - name: forwardable
@@ -15,7 +15,7 @@ module PgEventstore
15
15
  Integer events_number
16
16
  ) -> void
17
17
 
18
- def self.update_subscription_error: (Subscription subscription, WrappedException error) -> void
18
+ def self.update_subscription_error: (Subscription subscription, StandardError error) -> void
19
19
 
20
20
  def self.update_subscription_chunk_stats: (Subscription subscription, Integer subscription_position) -> void
21
21
 
@@ -0,0 +1,15 @@
1
+ module PgEventstore
2
+ module RunnerRecoveryStrategies
3
+ class ReportSubscriptionUnrecoverableError
4
+ include RunnerRecoveryStrategy
5
+
6
+ @failed_subscription_notifier: _FailedSubscriptionNotifier?
7
+ @subscription: Subscription
8
+
9
+ def initialize: (
10
+ subscription: Subscription,
11
+ failed_subscription_notifier: _FailedSubscriptionNotifier?
12
+ )-> untyped
13
+ end
14
+ end
15
+ end
@@ -11,6 +11,8 @@ module PgEventstore
11
11
 
12
12
  def asset_url: (String path) -> String
13
13
 
14
+ def can_delete?: -> bool
15
+
14
16
  def connection: -> Connection
15
17
 
16
18
  def current_config: -> Symbol
@@ -0,0 +1,11 @@
1
+ module PgEventstore
2
+ module Web
3
+ module Metrics
4
+ class Application
5
+ def metrics_connection: -> Connection
6
+
7
+ def config_name: -> Symbol
8
+ end
9
+ end
10
+ end
11
+ end