• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tarantool / crud / 21364862659

26 Jan 2026 04:13PM UTC coverage: 73.492% (-15.0%) from 88.463%
21364862659

push

github

web-flow
Merge f981517ee into a84e19f3e

4253 of 5787 relevant lines covered (73.49%)

55.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

33.12
/crud/stats/init.lua
1
---- CRUD statistics module.
2
-- @module crud.stats
3
--
4

5
local clock = require('clock')
19✔
6
local checks = require('checks')
19✔
7
local errors = require('errors')
19✔
8
local fiber = require('fiber')
19✔
9
local fun = require('fun')
19✔
10

11
local dev_checks = require('crud.common.dev_checks')
19✔
12
local stash = require('crud.common.stash')
19✔
13
local op_module = require('crud.stats.operation')
19✔
14

15
local StatsError = errors.new_class('StatsError', {capture_stack = false})
19✔
16

17
local stats = {}
19✔
18
local internal = stash.get(stash.name.stats_internal)
19✔
19

20
local local_registry = require('crud.stats.local_registry')
19✔
21
local metrics_registry = require('crud.stats.metrics_registry')
19✔
22

23
local drivers = {
19✔
24
    ['local'] = local_registry,
19✔
25
}
26
if metrics_registry.is_supported() then
38✔
27
    drivers['metrics'] = metrics_registry
19✔
28
end
29

30
function internal:get_registry()
19✔
31
    if self.driver == nil then
×
32
        return nil
×
33
    end
34
    return drivers[self.driver]
×
35
end
36

37
--- Check if statistics module was enabled.
38
--
39
-- @function is_enabled
40
--
41
-- @treturn boolean Returns `true` or `false`.
42
--
43
function stats.is_enabled()
19✔
44
    return internal.driver ~= nil
424✔
45
end
46

47
--- Get default statistics driver name.
48
--
49
-- @function get_default_driver
50
--
51
-- @treturn string `metrics` if supported, `local` if unsupported.
52
--
53
function stats.get_default_driver()
19✔
54
    if drivers.metrics ~= nil then
17✔
55
        return 'metrics'
17✔
56
    else
57
        return 'local'
×
58
    end
59
end
60

61
--- Check if provided driver is supported.
62
--
63
-- @function is_driver_supported
64
--
65
-- @string opts.driver
66
--
67
-- @treturn boolean Returns `true` or `false`.
68
--
69
function stats.is_driver_supported(driver)
19✔
70
    return drivers[driver] ~= nil
×
71
end
72

73
--- Initializes statistics registry, enables callbacks and wrappers.
74
--
75
--  If already enabled, do nothing.
76
--
77
-- @function enable
78
--
79
-- @tab[opt] opts
80
--
81
-- @string[opt] opts.driver
82
--  `'local'` or `'metrics'`.
83
--  If `'local'`, stores statistics in local registry (some Lua tables)
84
--  and computes latency as overall average. `'metrics'` requires
85
--  `metrics >= 0.9.0` installed and stores statistics in
86
--  global metrics registry (integrated with exporters).
87
--  `'metrics'` driver supports computing latency as 0.99 quantile with aging.
88
--  If `'metrics'` driver is available, it is used by default,
89
--  otherwise `'local'` is used.
90
--
91
-- @bool[opt=false] opts.quantiles
92
--  If `'metrics'` driver used, you can enable
93
--  computing requests latency as 0.99 quantile with aging.
94
--  Performance overhead for enabling is near 10%.
95
--
96
-- @number[opt=1e-3] opts.quantile_tolerated_error
97
--  See tarantool/metrics summary API for details:
98
--  https://www.tarantool.io/ru/doc/latest/book/monitoring/api_reference/#summary
99
--  If quantile value is -Inf, try to decrease quantile tolerated error.
100
--  See https://github.com/tarantool/metrics/issues/189 for issue details.
101
--
102
-- @number[opt=2] opts.quantile_age_buckets_count
103
--  Count of summary quantile buckets.
104
--  See tarantool/metrics summary API for details:
105
--  https://www.tarantool.io/ru/doc/latest/book/monitoring/api_reference/#summary
106
--  Increasing the value smoothes time window move,
107
--  but consumes additional memory and CPU.
108
--
109
-- @number[opt=60] opts.quantile_max_age_time
110
--  Duration of each bucket’s lifetime in seconds.
111
--  See tarantool/metrics summary API for details:
112
--  https://www.tarantool.io/ru/doc/latest/book/monitoring/api_reference/#summary
113
--  Smaller bucket lifetime results in smaller time window for quantiles,
114
--  but more CPU is spent on bucket rotation. If your application has low request
115
--  frequency, increase the value to reduce the amount of `-nan` gaps in quantile values.
116
--
117
-- @treturn boolean Returns `true`.
118
--
119
function stats.enable(opts)
19✔
120
    checks({
×
121
        driver = '?string',
122
        quantiles = '?boolean',
123
        quantile_tolerated_error = '?number',
124
        quantile_age_buckets_count = '?number',
125
        quantile_max_age_time = '?number',
126
    })
127

128
    StatsError:assert(
×
129
        rawget(_G, 'crud') ~= nil,
×
130
        'Can be enabled only on crud router'
131
    )
132

133
    opts = table.deepcopy(opts or {})
×
134
    if opts.driver == nil then
×
135
        opts.driver = stats.get_default_driver()
×
136
    end
137

138
    StatsError:assert(
×
139
        stats.is_driver_supported(opts.driver),
×
140
        'Unsupported driver: %s', opts.driver)
×
141

142
    if opts.quantiles == nil then
×
143
        opts.quantiles = false
×
144
    end
145

146
    if opts.quantile_tolerated_error == nil then
×
147
        opts.quantile_tolerated_error = stats.DEFAULT_QUANTILE_TOLERATED_ERROR
×
148
    end
149

150
    if opts.quantile_age_buckets_count == nil then
×
151
        opts.quantile_age_buckets_count = stats.DEFAULT_QUANTILE_AGE_BUCKET_COUNT
×
152
    end
153

154
    if opts.quantile_max_age_time == nil then
×
155
        opts.quantile_max_age_time = stats.DEFAULT_QUANTILE_MAX_AGE_TIME
×
156
    end
157

158
    -- Do not reinit if called with same options.
159
    if internal.driver == opts.driver
×
160
    and internal.quantiles == opts.quantiles
×
161
    and internal.quantile_tolerated_error == opts.quantile_tolerated_error
×
162
    and internal.quantile_age_buckets_count == opts.quantile_age_buckets_count
×
163
    and internal.quantile_max_age_time == opts.quantile_max_age_time then
×
164
        return true
×
165
    end
166

167
    -- Disable old driver registry, if another one was requested.
168
    stats.disable()
×
169

170
    internal.driver = opts.driver
×
171

172
    internal:get_registry().init{
×
173
        quantiles = opts.quantiles,
174
        quantile_tolerated_error = opts.quantile_tolerated_error,
175
        quantile_age_buckets_count = opts.quantile_age_buckets_count,
176
        quantile_max_age_time = opts.quantile_max_age_time,
177
    }
178

179
    internal.quantiles = opts.quantiles
×
180
    internal.quantile_tolerated_error = opts.quantile_tolerated_error
×
181
    internal.quantile_age_buckets_count = opts.quantile_age_buckets_count
×
182
    internal.quantile_max_age_time = opts.quantile_max_age_time
×
183

184
    return true
×
185
end
186

187
--- Resets statistics registry.
188
--
189
--  After reset collectors are the same as right
190
--  after initial `stats.enable()`.
191
--
192
-- @function reset
193
--
194
-- @treturn boolean Returns true.
195
--
196
function stats.reset()
19✔
197
    if not stats.is_enabled() then
×
198
        return true
×
199
    end
200

201
    internal:get_registry().destroy()
×
202
    internal:get_registry().init{
×
203
        quantiles = internal.quantiles,
204
        quantile_tolerated_error = internal.quantile_tolerated_error,
205
        quantile_age_buckets_count = internal.quantile_age_buckets_count,
206
        quantile_max_age_time = internal.quantile_max_age_time,
207
    }
208

209
    return true
×
210
end
211

212
--- Destroys statistics registry and disable callbacks.
213
--
214
--  If already disabled, do nothing.
215
--
216
-- @function disable
217
--
218
-- @treturn boolean Returns true.
219
--
220
function stats.disable()
19✔
221
    if not stats.is_enabled() then
×
222
        return true
×
223
    end
224

225
    internal:get_registry().destroy()
×
226
    internal.driver = nil
×
227
    internal.quantiles = nil
×
228
    internal.quantile_tolerated_error = nil
×
229
    internal.quantile_age_buckets_count = nil
×
230
    internal.quantile_max_age_time = nil
×
231

232
    return true
×
233
end
234

235
--- Get statistics on CRUD operations.
236
--
237
-- @function get
238
--
239
-- @string[opt] space_name
240
--  If specified, returns table with statistics
241
--  of operations on space, separated by operation type and
242
--  execution status. If there wasn't any requests of "op" type
243
--  for space, there won't be corresponding collectors.
244
--  If not specified, returns table with statistics
245
--  about all observed spaces.
246
--
247
-- @treturn table Statistics on CRUD operations.
248
--  If statistics disabled, returns `{}`.
249
--
250
function stats.get(space_name)
19✔
251
    checks('?string')
×
252

253
    if not stats.is_enabled() then
×
254
        return {}
×
255
    end
256

257
    return internal:get_registry().get(space_name)
×
258
end
259

260
-- Hack to set __gc for a table in Lua 5.1
261
-- See https://stackoverflow.com/questions/27426704/lua-5-1-workaround-for-gc-metamethod-for-tables
262
-- or https://habr.com/ru/post/346892/
263
local function setmt__gc(t, mt)
264
    local prox = newproxy(true)
×
265
    getmetatable(prox).__gc = function() mt.__gc(t) end
×
266
    t[prox] = true
×
267
    return setmetatable(t, mt)
×
268
end
269

270
-- If jit will be enabled here, gc_observer usage
271
-- may be optimized so our __gc hack will not work.
272
local function keep_observer_alive(gc_observer) --luacheck: ignore
273
end
274
jit.off(keep_observer_alive)
19✔
275

276
local function wrap_pairs_gen(build_latency, space_name, op, gen, param, state)
277
    local total_latency = build_latency
×
278

279
    local registry = internal:get_registry()
×
280

281
    -- If pairs() cycle will be interrupted with break,
282
    -- we'll never get a proper obervation.
283
    -- We create an object with the same lifespan as gen()
284
    -- function so if someone break pairs cycle,
285
    -- it still will be observed.
286
    local observed = false
×
287

288
    local gc_observer = setmt__gc({}, {
×
289
        __gc = function()
290
            if observed == false then
×
291
                -- Do not call observe directly because metrics
292
                -- collectors may yield, for example
293
                -- https://github.com/tarantool/metrics/blob/a23f8d49779205dd45bd211e21a1d34f26010382/metrics/collectors/shared.lua#L85
294
                -- Calling fiber.yield is prohibited in gc.
295
                fiber.new(registry.observe, total_latency, space_name, op, 'ok')
×
296
                observed = true
×
297
            end
298
        end
299
    })
300

301
    local wrapped_gen = function(param, state)
302
        -- Mess with gc_observer so its lifespan will
303
        -- be the same as wrapped_gen() function.
304
        keep_observer_alive(gc_observer)
×
305

306
        local start_time = clock.monotonic()
×
307

308
        local status, next_state, var = pcall(gen, param, state)
×
309

310
        local finish_time = clock.monotonic()
×
311

312
        total_latency = total_latency + (finish_time - start_time)
×
313

314
        if status == false then
×
315
            registry.observe(total_latency, space_name, op, 'error')
×
316
            observed = true
×
317
            error(next_state, 2)
×
318
        end
319

320
        -- Observe stats in the end of pairs cycle
321
        if var == nil then
×
322
            registry.observe(total_latency, space_name, op, 'ok')
×
323
            observed = true
×
324
            return nil
×
325
        end
326

327
        return next_state, var
×
328
    end
329

330
    return fun.wrap(wrapped_gen, param, state)
×
331
end
332

333
local function wrap_tail(space_name, op, pairs, start_time, call_status, ...)
334
    dev_checks('string', 'string', 'boolean', 'number', 'boolean')
×
335

336
    local finish_time = clock.monotonic()
×
337
    local latency = finish_time - start_time
×
338

339
    local registry = internal:get_registry()
×
340

341
    if call_status == false then
×
342
        registry.observe(latency, space_name, op, 'error')
×
343
        error((...), 2)
×
344
    end
345

346
    if pairs == false then
×
347
        if select(2, ...) ~= nil then
×
348
            -- If not `pairs` call, return values `nil, err`
349
            -- treated as error case.
350
            registry.observe(latency, space_name, op, 'error')
×
351
            return ...
×
352
        else
353
            registry.observe(latency, space_name, op, 'ok')
×
354
            return ...
×
355
        end
356
    else
357
        return wrap_pairs_gen(latency, space_name, op, ...)
×
358
    end
359
end
360

361
--- Wrap CRUD operation call to collect statistics.
362
--
363
--  Approach based on `box.atomic()`:
364
--  https://github.com/tarantool/tarantool/blob/b9f7204b5e0d10b443c6f198e9f7f04e0d16a867/src/box/lua/schema.lua#L369
365
--
366
-- @function wrap
367
--
368
-- @func func
369
--  Function to wrap. First argument is expected to
370
--  be a space name string. If statistics enabled,
371
--  errors are caught and thrown again.
372
--
373
-- @string op
374
--  Label of registry collectors.
375
--  Use `require('crud.stats').op` to pick one.
376
--
377
-- @tab[opt] opts
378
--
379
-- @bool[opt=false] opts.pairs
380
--  If false, wraps only function passed as argument.
381
--  Second return value of wrapped function is treated
382
--  as error (`nil, err` case).
383
--  If true, also wraps gen() function returned by
384
--  call. Statistics observed on cycle end (last
385
--  element was fetched or error was thrown). If pairs
386
--  cycle was interrupted with `break`, statistics will
387
--  be collected when pairs objects are cleaned up with
388
--  Lua garbage collector.
389
--
390
-- @return Wrapped function output.
391
--
392
function stats.wrap(func, op, opts)
19✔
393
    dev_checks('function', 'string', { pairs = '?boolean' })
418✔
394

395
    local pairs
396
    if type(opts) == 'table' and opts.pairs ~= nil then
418✔
397
        pairs = opts.pairs
19✔
398
    else
399
        pairs = false
399✔
400
    end
401

402
    return function(space_name, ...)
403
        if not stats.is_enabled() then
818✔
404
            return func(space_name, ...)
409✔
405
        end
406

407
        local start_time = clock.monotonic()
×
408

409
        return wrap_tail(
×
410
            space_name, op, pairs, start_time,
411
            pcall(func, space_name, ...)
×
412
        )
413
    end
414
end
415

416
local storage_stats_schema = { tuples_fetched = 'number', tuples_lookup = 'number' }
19✔
417
--- Callback to collect storage tuples stats (select/pairs).
418
--
419
-- @function update_fetch_stats
420
--
421
-- @tab storage_stats
422
--  Statistics from select storage call.
423
--
424
-- @number storage_stats.tuples_fetched
425
--  Count of tuples fetched during storage call.
426
--
427
-- @number storage_stats.tuples_lookup
428
--  Count of tuples looked up on storages while collecting response.
429
--
430
-- @string space_name
431
--  Name of space.
432
--
433
-- @treturn boolean Returns `true`.
434
--
435
function stats.update_fetch_stats(storage_stats, space_name)
19✔
436
    dev_checks(storage_stats_schema, 'string')
13✔
437

438
    if not stats.is_enabled() then
26✔
439
        return true
13✔
440
    end
441

442
    internal:get_registry().observe_fetch(
×
443
        storage_stats.tuples_fetched,
×
444
        storage_stats.tuples_lookup,
×
445
        space_name
446
    )
447

448
    return true
×
449
end
450

451
--- Callback to collect planned map reduces stats (select/pairs).
452
--
453
-- @function update_map_reduces
454
--
455
-- @string space_name
456
--  Name of space.
457
--
458
-- @treturn boolean Returns `true`.
459
--
460
function stats.update_map_reduces(space_name)
19✔
461
    dev_checks('string')
2✔
462

463
    if not stats.is_enabled() then
4✔
464
        return true
2✔
465
    end
466

467
    internal:get_registry().observe_map_reduces(1, space_name)
×
468

469
    return true
×
470
end
471

472
--- Table with CRUD operation lables.
473
--
474
-- @tfield string INSERT
475
--  Identifies both `insert` and `insert_object`.
476
--
477
-- @tfield string GET
478
--
479
-- @tfield string REPLACE
480
--  Identifies both `replace` and `replace_object`.
481
--
482
-- @tfield string UPDATE
483
--
484
-- @tfield string UPSERT
485
--  Identifies both `upsert` and `upsert_object`.
486
--
487
-- @tfield string DELETE
488
--
489
-- @tfield string SELECT
490
--  Identifies both `pairs` and `select`.
491
--
492
-- @tfield string TRUNCATE
493
--
494
-- @tfield string LEN
495
--
496
-- @tfield string COUNT
497
--
498
-- @tfield string BORDERS
499
--  Identifies both `min` and `max`.
500
--
501
stats.op = op_module
19✔
502

503
--- Stats module internal state (for debug/test).
504
--
505
-- @tfield[opt] string driver Current statistics registry driver (if nil, stats disabled).
506
--
507
-- @tfield[opt] boolean quantiles Is quantiles computed.
508
stats.internal = internal
19✔
509

510
--- Default metrics quantile precision.
511
stats.DEFAULT_QUANTILE_TOLERATED_ERROR = 1e-3
19✔
512

513
--- Default metrics quantile bucket count.
514
stats.DEFAULT_QUANTILE_AGE_BUCKET_COUNT = 2
19✔
515

516
--- Default metrics quantile bucket lifetime.
517
stats.DEFAULT_QUANTILE_MAX_AGE_TIME = 60
19✔
518

519
return stats
19✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc