trifle-stats 2.5.0 → 2.7.0
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/Gemfile.lock +2 -2
- data/README.md +1 -0
- data/lib/trifle/stats/configuration.rb +1 -0
- data/lib/trifle/stats/driver/api.rb +150 -0
- data/lib/trifle/stats/nocturnal.rb +151 -78
- data/lib/trifle/stats/operations/timeseries/increment.rb +24 -6
- data/lib/trifle/stats/operations/timeseries/set.rb +24 -6
- data/lib/trifle/stats/version.rb +1 -1
- data/lib/trifle/stats.rb +2 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70c1c7a45dc8833c358ebab3d93a55e15f1f42f4459eb0d5fab14b7383de0a04
|
|
4
|
+
data.tar.gz: c09506de6ef9cd69b35733a616d43a09aa62ad8c6013f32552d1a0b7dcf870f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c980da761d8029b971ad9520d898d1971ba164c7cb2a1d7ffdbad60c67820cbb7eecf0ca25fa547518f06615dbe71411f376e20f2672dac1c9fca5c5dddaa5aa
|
|
7
|
+
data.tar.gz: 23266f716645654a5b642abf1558bbc85667b069dfb1caa98cfa1cc0899fd732769d2e0628107d06397b9b50996630b24a8f4ad6b634c5a8851d9984a66c7ad0
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
trifle-stats (2.
|
|
4
|
+
trifle-stats (2.7.0)
|
|
5
5
|
tzinfo (~> 2.0)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -11,7 +11,7 @@ GEM
|
|
|
11
11
|
bigdecimal (4.0.1)
|
|
12
12
|
bson (4.12.1)
|
|
13
13
|
byebug (11.1.3)
|
|
14
|
-
concurrent-ruby (1.3.
|
|
14
|
+
concurrent-ruby (1.3.8)
|
|
15
15
|
diff-lcs (1.4.4)
|
|
16
16
|
dotenv (2.7.6)
|
|
17
17
|
mini_portile2 (2.8.9)
|
data/README.md
CHANGED
|
@@ -80,6 +80,7 @@ series.aggregate.sum(path: 'count')
|
|
|
80
80
|
|
|
81
81
|
| Driver | Backend | Best for |
|
|
82
82
|
|--------|---------|----------|
|
|
83
|
+
| **API** | Trifle Cloud Projects | Hosted metrics without your own database |
|
|
83
84
|
| **Postgres** | JSONB upsert | Most production apps |
|
|
84
85
|
| **Redis** | Hash increment | High-throughput counters |
|
|
85
86
|
| **MongoDB** | Document upsert | Document-oriented stacks |
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
require 'time'
|
|
7
|
+
require 'uri'
|
|
8
|
+
require 'zlib'
|
|
9
|
+
require_relative '../version'
|
|
10
|
+
|
|
11
|
+
module Trifle
|
|
12
|
+
module Stats
|
|
13
|
+
module Driver
|
|
14
|
+
class Api
|
|
15
|
+
ENDPOINT = URI('https://app.trifle.io/api/v1/metrics')
|
|
16
|
+
TIMEOUT = 10
|
|
17
|
+
ERROR_BODY_LIMIT = 1024
|
|
18
|
+
|
|
19
|
+
class Error < Trifle::Stats::Error
|
|
20
|
+
attr_reader :status, :response_body, :retry_after, :delivery_unknown
|
|
21
|
+
|
|
22
|
+
def initialize(message, status: nil, response_body: nil, retry_after: nil, delivery_unknown: false)
|
|
23
|
+
super(message)
|
|
24
|
+
@status = status
|
|
25
|
+
@response_body = response_body
|
|
26
|
+
@retry_after = retry_after
|
|
27
|
+
@delivery_unknown = delivery_unknown
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class NetHttpTransport
|
|
32
|
+
def call(uri:, request:, timeout:)
|
|
33
|
+
Net::HTTP.start(
|
|
34
|
+
uri.hostname,
|
|
35
|
+
uri.port,
|
|
36
|
+
use_ssl: uri.scheme == 'https',
|
|
37
|
+
open_timeout: timeout,
|
|
38
|
+
read_timeout: timeout
|
|
39
|
+
) { |http| http.request(request) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
attr_reader :token, :project_id
|
|
44
|
+
|
|
45
|
+
def initialize(token:, project_id:, transport: NetHttpTransport.new)
|
|
46
|
+
raise ArgumentError, 'token must not be empty' if token.to_s.strip.empty?
|
|
47
|
+
raise ArgumentError, 'project_id must not be empty' if project_id.to_s.strip.empty?
|
|
48
|
+
|
|
49
|
+
@token = token.to_s
|
|
50
|
+
@project_id = project_id.to_s
|
|
51
|
+
@transport = transport
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def description
|
|
55
|
+
self.class.name
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def bypass_buffer?
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def direct_write(operation:, key:, at:, values:, untracked: false)
|
|
63
|
+
payload = api_payload(operation, key, at, values, untracked)
|
|
64
|
+
deliver(build_request(payload))
|
|
65
|
+
rescue Error
|
|
66
|
+
raise
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
raise Error.new("Trifle API request failed: #{e.message}", delivery_unknown: true), cause: e
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def inc(*)
|
|
72
|
+
unsupported!(:track)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def set(*)
|
|
76
|
+
unsupported!(:assert)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def get(*)
|
|
80
|
+
unsupported!(:values)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def ping(*)
|
|
84
|
+
unsupported!(:beam)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def scan(*)
|
|
88
|
+
unsupported!(:scan)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def api_payload(operation, key, at, values, untracked)
|
|
94
|
+
{
|
|
95
|
+
operation: operation.to_s,
|
|
96
|
+
key: key,
|
|
97
|
+
at: at.to_time.iso8601(6),
|
|
98
|
+
values: values,
|
|
99
|
+
untracked: untracked
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def build_request(payload)
|
|
104
|
+
request = Net::HTTP::Post.new(ENDPOINT, request_headers)
|
|
105
|
+
request.body = gzip(JSON.generate(payload))
|
|
106
|
+
request
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def request_headers
|
|
110
|
+
{
|
|
111
|
+
'Authorization' => "Bearer #{token}",
|
|
112
|
+
'X-Trifle-Source-Id' => project_id,
|
|
113
|
+
'Content-Type' => 'application/json',
|
|
114
|
+
'Accept' => 'application/json',
|
|
115
|
+
'Content-Encoding' => 'gzip',
|
|
116
|
+
'User-Agent' => "trifle-stats-ruby/#{Trifle::Stats::VERSION}"
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def deliver(request)
|
|
121
|
+
response = @transport.call(uri: ENDPOINT, request: request, timeout: TIMEOUT)
|
|
122
|
+
return true if response.code.to_i.between?(200, 299)
|
|
123
|
+
|
|
124
|
+
raise_response_error(response)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def raise_response_error(response)
|
|
128
|
+
raise Error.new(
|
|
129
|
+
"Trifle API returned HTTP #{response.code}",
|
|
130
|
+
status: response.code.to_i,
|
|
131
|
+
response_body: response.body.to_s.byteslice(0, ERROR_BODY_LIMIT),
|
|
132
|
+
retry_after: response['Retry-After']
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def gzip(value)
|
|
137
|
+
output = StringIO.new
|
|
138
|
+
Zlib::GzipWriter.wrap(output) { |writer| writer.write(value) }
|
|
139
|
+
output.string
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def unsupported!(operation)
|
|
143
|
+
message = "#{operation} must be called through Trifle::Stats; " \
|
|
144
|
+
'API driver does not support direct storage operations'
|
|
145
|
+
raise Error, message
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -26,7 +26,8 @@ module Trifle
|
|
|
26
26
|
item = from.dup
|
|
27
27
|
while item <= to
|
|
28
28
|
list << item
|
|
29
|
-
|
|
29
|
+
candidate = new(item, config: config).add(offset, unit)
|
|
30
|
+
item = new(candidate, config: config).floor(offset, unit)
|
|
30
31
|
end
|
|
31
32
|
list
|
|
32
33
|
end
|
|
@@ -49,53 +50,21 @@ module Trifle
|
|
|
49
50
|
|
|
50
51
|
case unit
|
|
51
52
|
when :second
|
|
52
|
-
|
|
53
|
+
add_elapsed(offset)
|
|
53
54
|
when :minute
|
|
54
|
-
|
|
55
|
+
add_elapsed(offset * 60)
|
|
55
56
|
when :hour
|
|
56
|
-
|
|
57
|
+
add_elapsed(offset * 3600)
|
|
57
58
|
when :day
|
|
58
|
-
|
|
59
|
+
add_days(calendar_offset(offset))
|
|
59
60
|
when :week
|
|
60
|
-
|
|
61
|
+
add_days(calendar_offset(offset) * 7)
|
|
61
62
|
when :month
|
|
62
|
-
|
|
63
|
-
result = time
|
|
64
|
-
offset.times do
|
|
65
|
-
result = add_one_month(result)
|
|
66
|
-
end
|
|
67
|
-
result
|
|
63
|
+
add_months(calendar_offset(offset))
|
|
68
64
|
when :quarter
|
|
69
|
-
|
|
70
|
-
result = time
|
|
71
|
-
(offset * 3).times do
|
|
72
|
-
result = add_one_month(result)
|
|
73
|
-
end
|
|
74
|
-
result
|
|
65
|
+
add_months(calendar_offset(offset) * 3)
|
|
75
66
|
when :year
|
|
76
|
-
|
|
77
|
-
begin
|
|
78
|
-
Time.new(
|
|
79
|
-
time.year + offset,
|
|
80
|
-
time.month,
|
|
81
|
-
time.day,
|
|
82
|
-
time.hour,
|
|
83
|
-
time.min,
|
|
84
|
-
time.sec,
|
|
85
|
-
config.tz.utc_offset
|
|
86
|
-
)
|
|
87
|
-
rescue ArgumentError
|
|
88
|
-
# Handle edge cases like Feb 29 in non-leap years
|
|
89
|
-
Time.new(
|
|
90
|
-
time.year + offset,
|
|
91
|
-
time.month,
|
|
92
|
-
[time.day, days_in_month(time.year + offset, time.month)].min,
|
|
93
|
-
time.hour,
|
|
94
|
-
time.min,
|
|
95
|
-
time.sec,
|
|
96
|
-
time.utc_offset
|
|
97
|
-
)
|
|
98
|
-
end
|
|
67
|
+
add_years(calendar_offset(offset))
|
|
99
68
|
end
|
|
100
69
|
end
|
|
101
70
|
|
|
@@ -104,89 +73,193 @@ module Trifle
|
|
|
104
73
|
raise ArgumentError, 'Segment size must be positive' unless offset.positive?
|
|
105
74
|
raise ArgumentError, "Invalid unit: #{unit}" unless Trifle::Stats::Nocturnal::UNIT_MAP.values.include?(unit)
|
|
106
75
|
|
|
76
|
+
source = localized_time
|
|
77
|
+
|
|
107
78
|
case unit
|
|
108
79
|
when :second
|
|
109
80
|
# Floor to second segment boundary
|
|
110
|
-
total_seconds =
|
|
81
|
+
total_seconds = source.sec
|
|
111
82
|
floored_seconds = (total_seconds / offset) * offset
|
|
112
|
-
|
|
83
|
+
subtract_elapsed(source, (total_seconds - floored_seconds) + source.subsec)
|
|
113
84
|
|
|
114
85
|
when :minute
|
|
115
86
|
# Floor to minute segment boundary (segments start from beginning of hour)
|
|
116
|
-
minutes_from_hour_start =
|
|
87
|
+
minutes_from_hour_start = source.min
|
|
117
88
|
floored_minutes = (minutes_from_hour_start / offset) * offset
|
|
118
|
-
|
|
89
|
+
seconds = ((minutes_from_hour_start - floored_minutes) * 60) + source.sec + source.subsec
|
|
90
|
+
subtract_elapsed(source, seconds)
|
|
119
91
|
|
|
120
92
|
when :hour
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
93
|
+
# Hour segments are elapsed durations anchored at local day start.
|
|
94
|
+
day_start = local_midnight(source.to_date, source)
|
|
95
|
+
segment = offset * 3600
|
|
96
|
+
elapsed = source - day_start
|
|
97
|
+
add_elapsed_to(day_start, (elapsed / segment).floor * segment)
|
|
125
98
|
|
|
126
99
|
when :day
|
|
127
100
|
# Floor to day segment boundary (segments start from beginning of year)
|
|
128
|
-
days_from_year_start =
|
|
101
|
+
days_from_year_start = source.yday - 1 # yday is 1-indexed, we want 0-indexed
|
|
129
102
|
floored_days = (days_from_year_start / offset) * offset
|
|
130
|
-
|
|
131
|
-
result_date
|
|
132
|
-
Time.new(result_date.year, result_date.month, result_date.day, 0, 0, 0, time.utc_offset)
|
|
103
|
+
result_date = Date.new(source.year, 1, 1) + floored_days
|
|
104
|
+
local_midnight(result_date, source)
|
|
133
105
|
|
|
134
106
|
when :week
|
|
135
107
|
# Floor to week segment boundary (segments start from beginning of year)
|
|
136
|
-
|
|
108
|
+
year_start_date = Date.new(source.year, 1, 1)
|
|
137
109
|
|
|
138
110
|
# Find the first week boundary of the year based on week_start
|
|
139
|
-
week_start_offset = DAYS_INTO_WEEK.fetch(
|
|
140
|
-
|
|
141
|
-
)
|
|
142
|
-
year_start_wday = year_start.wday
|
|
111
|
+
week_start_offset = DAYS_INTO_WEEK.fetch(config.beginning_of_week)
|
|
112
|
+
year_start_wday = year_start_date.wday
|
|
143
113
|
days_to_first_week_start = (week_start_offset - year_start_wday) % 7
|
|
144
|
-
first_week_start =
|
|
145
|
-
first_week_start_time = Time.new(first_week_start.year, first_week_start.month, first_week_start.day, 0, 0, 0, time.utc_offset) # rubocop:disable Layout/LineLength
|
|
114
|
+
first_week_start = year_start_date + days_to_first_week_start
|
|
146
115
|
|
|
147
116
|
# If current time is before first week boundary, use year start
|
|
148
|
-
if
|
|
149
|
-
|
|
117
|
+
if source.to_date < first_week_start
|
|
118
|
+
local_midnight(year_start_date, source)
|
|
150
119
|
else
|
|
151
|
-
|
|
152
|
-
weeks_since_first = ((time - first_week_start_time) / (7 * 86_400)).to_i
|
|
120
|
+
weeks_since_first = ((source.to_date - first_week_start).to_i / 7)
|
|
153
121
|
floored_weeks = (weeks_since_first / offset) * offset
|
|
154
122
|
|
|
155
123
|
result_date = first_week_start + (floored_weeks * 7)
|
|
156
|
-
|
|
124
|
+
local_midnight(result_date, source)
|
|
157
125
|
end
|
|
158
126
|
|
|
159
127
|
when :month
|
|
160
128
|
# Floor to month segment boundary (from start of year)
|
|
161
|
-
months_from_jan =
|
|
129
|
+
months_from_jan = source.month - 1 # 0-indexed
|
|
162
130
|
floored_months = (months_from_jan / offset) * offset
|
|
163
|
-
|
|
131
|
+
date = Date.new(source.year, floored_months + 1, 1)
|
|
132
|
+
local_midnight(date, source)
|
|
164
133
|
|
|
165
134
|
when :quarter
|
|
166
135
|
# Floor to quarter segment boundary
|
|
167
|
-
current_quarter = ((
|
|
136
|
+
current_quarter = ((source.month - 1) / 3) # 0-indexed quarters
|
|
168
137
|
floored_quarters = (current_quarter / offset) * offset
|
|
169
138
|
quarter_start_month = (floored_quarters * 3) + 1
|
|
170
|
-
|
|
139
|
+
date = Date.new(source.year, quarter_start_month, 1)
|
|
140
|
+
local_midnight(date, source)
|
|
171
141
|
|
|
172
142
|
when :year
|
|
173
143
|
# Floor to year segment boundary
|
|
174
|
-
floored_years = (
|
|
175
|
-
|
|
144
|
+
floored_years = (source.year / offset) * offset
|
|
145
|
+
date = Date.new(floored_years, 1, 1)
|
|
146
|
+
local_midnight(date, source)
|
|
176
147
|
end
|
|
177
148
|
end
|
|
178
149
|
|
|
179
150
|
private
|
|
180
151
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
152
|
+
def add_elapsed(seconds)
|
|
153
|
+
add_elapsed_to(time, seconds)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def add_elapsed_to(source, seconds)
|
|
157
|
+
config.tz.utc_to_local((source + seconds).getutc)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def subtract_elapsed(source, seconds)
|
|
161
|
+
config.tz.utc_to_local((source - seconds).getutc)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def add_days(days)
|
|
165
|
+
source = localized_time
|
|
166
|
+
local_time_for_source(source.to_date + days, source)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def add_months(months)
|
|
170
|
+
source = localized_time
|
|
171
|
+
total_months = (source.year * 12) + (source.month - 1) + months
|
|
172
|
+
year, month_index = total_months.divmod(12)
|
|
173
|
+
month = month_index + 1
|
|
174
|
+
day = [source.day, days_in_month(year, month)].min
|
|
175
|
+
|
|
176
|
+
local_time_for_source(Date.new(year, month, day), source)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def add_years(years)
|
|
180
|
+
source = localized_time
|
|
181
|
+
year = source.year + years
|
|
182
|
+
day = [source.day, days_in_month(year, source.month)].min
|
|
183
|
+
|
|
184
|
+
local_time_for_source(Date.new(year, source.month, day), source)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def localized_time
|
|
188
|
+
config.tz.utc_to_local(time.getutc)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def calendar_offset(offset)
|
|
192
|
+
return offset if offset.is_a?(Integer)
|
|
193
|
+
|
|
194
|
+
raise ArgumentError, 'Offset must be an integer for calendar units'
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def local_midnight(date, source)
|
|
198
|
+
local_time(date, source: source, hour: 0, minute: 0, second: 0)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def local_time_for_source(date, source)
|
|
202
|
+
local_time(
|
|
203
|
+
date,
|
|
204
|
+
source: source,
|
|
205
|
+
hour: source.hour,
|
|
206
|
+
minute: source.min,
|
|
207
|
+
second: source.sec,
|
|
208
|
+
sub_second: source.subsec
|
|
209
|
+
)
|
|
210
|
+
end
|
|
184
211
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
def local_time(date, source:, **clock)
|
|
213
|
+
clock = { hour: 0, minute: 0, second: 0, sub_second: 0 }.merge(clock)
|
|
214
|
+
config.tz.local_time(
|
|
215
|
+
date.year, date.month, date.day,
|
|
216
|
+
clock[:hour], clock[:minute], clock[:second], clock[:sub_second], source.dst?
|
|
217
|
+
)
|
|
218
|
+
rescue TZInfo::AmbiguousTime
|
|
219
|
+
resolve_ambiguous_time(date, clock, source)
|
|
220
|
+
rescue TZInfo::PeriodNotFound
|
|
221
|
+
resolve_missing_time(date, clock, source)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def resolve_ambiguous_time(date, clock, source)
|
|
225
|
+
timestamp = local_timestamp(date, clock)
|
|
226
|
+
periods = config.tz.periods_for_local(timestamp)
|
|
227
|
+
period = periods.find { |candidate| candidate.observed_utc_offset == source.utc_offset } || periods.first
|
|
228
|
+
config.tz.utc_to_local(Time.at(timestamp.to_r - period.observed_utc_offset).utc)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def resolve_missing_time(date, clock, source)
|
|
232
|
+
timestamp = local_timestamp(date, clock)
|
|
233
|
+
transition = missing_time_transition(timestamp)
|
|
234
|
+
raise TZInfo::PeriodNotFound, timestamp.to_s unless transition
|
|
235
|
+
|
|
236
|
+
gap = transition.offset.observed_utc_offset - transition.previous_offset.observed_utc_offset
|
|
237
|
+
shifted = timestamp + gap
|
|
238
|
+
shifted_clock = { hour: shifted.hour, minute: shifted.min, second: shifted.sec, sub_second: shifted.subsec }
|
|
239
|
+
local_time(shifted.to_date, source: source, **shifted_clock)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def missing_time_transition(timestamp)
|
|
243
|
+
config.tz.transitions_up_to(timestamp + 172_800, timestamp - 172_800).find do |transition|
|
|
244
|
+
missing_time_transition?(timestamp, transition)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def missing_time_transition?(timestamp, transition)
|
|
249
|
+
previous_offset = transition.previous_offset.observed_utc_offset
|
|
250
|
+
next_offset = transition.offset.observed_utc_offset
|
|
251
|
+
return false unless next_offset > previous_offset
|
|
252
|
+
|
|
253
|
+
start_at = transition.at.to_time + previous_offset
|
|
254
|
+
end_at = transition.at.to_time + next_offset
|
|
255
|
+
timestamp >= start_at && timestamp < end_at
|
|
256
|
+
end
|
|
188
257
|
|
|
189
|
-
|
|
258
|
+
def local_timestamp(date, clock)
|
|
259
|
+
Time.utc(
|
|
260
|
+
date.year, date.month, date.day,
|
|
261
|
+
clock[:hour], clock[:minute], clock[:second] + clock[:sub_second]
|
|
262
|
+
)
|
|
190
263
|
end
|
|
191
264
|
|
|
192
265
|
def days_in_month(year, month)
|
|
@@ -26,6 +26,30 @@ module Trifle
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def perform
|
|
29
|
+
return direct_write if direct_write?
|
|
30
|
+
|
|
31
|
+
buffered_write
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def tracking_key
|
|
35
|
+
@untracked ? '__untracked__' : nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def direct_write?
|
|
41
|
+
config.driver.respond_to?(:direct_write)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def direct_write
|
|
45
|
+
config.driver.direct_write(**direct_payload)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def direct_payload
|
|
49
|
+
{ operation: :track, key: key, at: @at, values: values, untracked: @untracked }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def buffered_write
|
|
29
53
|
payload = {
|
|
30
54
|
keys: config.granularities.map { |granularity| key_for(granularity: granularity) },
|
|
31
55
|
values: values
|
|
@@ -38,12 +62,6 @@ module Trifle
|
|
|
38
62
|
end
|
|
39
63
|
end
|
|
40
64
|
|
|
41
|
-
def tracking_key
|
|
42
|
-
@untracked ? '__untracked__' : nil
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
65
|
def localized_time(time)
|
|
48
66
|
base_time = time.is_a?(Time) ? time : time.to_time
|
|
49
67
|
config.tz.utc_to_local(base_time.getutc)
|
|
@@ -26,6 +26,30 @@ module Trifle
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def perform
|
|
29
|
+
return direct_write if direct_write?
|
|
30
|
+
|
|
31
|
+
buffered_write
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def tracking_key
|
|
35
|
+
@untracked ? '__untracked__' : nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def direct_write?
|
|
41
|
+
config.driver.respond_to?(:direct_write)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def direct_write
|
|
45
|
+
config.driver.direct_write(**direct_payload)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def direct_payload
|
|
49
|
+
{ operation: :assert, key: key, at: @at, values: values, untracked: @untracked }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def buffered_write
|
|
29
53
|
payload = {
|
|
30
54
|
keys: config.granularities.map { |granularity| key_for(granularity: granularity) },
|
|
31
55
|
values: values
|
|
@@ -38,12 +62,6 @@ module Trifle
|
|
|
38
62
|
end
|
|
39
63
|
end
|
|
40
64
|
|
|
41
|
-
def tracking_key
|
|
42
|
-
@untracked ? '__untracked__' : nil
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
65
|
def localized_time(time)
|
|
48
66
|
base_time = time.is_a?(Time) ? time : time.to_time
|
|
49
67
|
config.tz.utc_to_local(base_time.getutc)
|
data/lib/trifle/stats/version.rb
CHANGED
data/lib/trifle/stats.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trifle-stats
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jozef Vaclavik
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -225,6 +225,7 @@ files:
|
|
|
225
225
|
- lib/trifle/stats/designator/geometric.rb
|
|
226
226
|
- lib/trifle/stats/designator/linear.rb
|
|
227
227
|
- lib/trifle/stats/driver/README.md
|
|
228
|
+
- lib/trifle/stats/driver/api.rb
|
|
228
229
|
- lib/trifle/stats/driver/mongo.rb
|
|
229
230
|
- lib/trifle/stats/driver/mysql.rb
|
|
230
231
|
- lib/trifle/stats/driver/postgres.rb
|