@voxgig/sdkgen 4.2.6 → 4.2.8
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.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/helpers/naming.js +5 -4
- package/dist/helpers/naming.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/model/feature/cost.aon +43 -0
- package/project/.sdk/model/feature/feature-index.aon +1 -0
- package/project/.sdk/src/cmp/js/fragment/EntityCreateOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityListOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityLoadOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityRemoveOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityUpdateOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/py/ReadmeExamplesTest_py.ts +15 -5
- package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.ts +2 -1
- package/project/.sdk/tm/c/core/sdk.h +1 -0
- package/project/.sdk/tm/c/feature/cost.c +549 -0
- package/project/.sdk/tm/c/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/clojure/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/cpp/feature/cost.hpp +419 -0
- package/project/.sdk/tm/cpp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/csharp/feature/CostFeature.cs +518 -0
- package/project/.sdk/tm/csharp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/dart/lib/feature/cost/CostFeature.dart +446 -0
- package/project/.sdk/tm/dart/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/elixir/lib/projectname/feature/cost.ex +442 -0
- package/project/.sdk/tm/elixir/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go/feature/cost_feature.go +424 -0
- package/project/.sdk/tm/go/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go-cli/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go-mcp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/java/feature/CostFeature.java +431 -0
- package/project/.sdk/tm/java/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/js/src/feature/cost/CostFeature.js +413 -0
- package/project/.sdk/tm/kotlin/feature/CostFeature.kt +430 -0
- package/project/.sdk/tm/kotlin/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/lean/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/lua/feature/cost_feature.lua +457 -0
- package/project/.sdk/tm/lua/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/ocaml/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/perl/feature/cost_feature.pm +446 -0
- package/project/.sdk/tm/perl/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/php/feature/CostFeature.php +439 -0
- package/project/.sdk/tm/php/feature/TestFeature.php +31 -1
- package/project/.sdk/tm/php/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/py/pkg/feature/cost_feature.py +381 -0
- package/project/.sdk/tm/py/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/rb/feature/cost_feature.rb +386 -0
- package/project/.sdk/tm/rb/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/rust/feature/cost.rs +467 -0
- package/project/.sdk/tm/rust/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/scala/feature/CostFeature.scala +388 -0
- package/project/.sdk/tm/scala/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/CostFeature.swift +425 -0
- package/project/.sdk/tm/swift/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/ts/src/feature/cost/CostFeature.ts +415 -0
- package/project/.sdk/tm/zig/feature/cost.zig +457 -0
- package/project/.sdk/tm/zig/src/feature/cost/.gitkeep +0 -0
- package/project/sdkgen-package.json +2 -1
- package/src/helpers/naming.ts +5 -4
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
-- ProjectName SDK cost feature
|
|
2
|
+
--
|
|
3
|
+
-- Cost tracking and spend budget. Uses BOTH seams, which is the point of
|
|
4
|
+
-- the feature: money is spent per HTTP ATTEMPT (a retried call is charged
|
|
5
|
+
-- again, because the upstream API charges it again), but it is owed by an
|
|
6
|
+
-- OPERATION. So the transport wrap prices each attempt, and PreDone
|
|
7
|
+
-- attributes the running total to `<entity>.<op>` and to the caller
|
|
8
|
+
-- (`ctrl.actor`, the same actor the audit feature records).
|
|
9
|
+
--
|
|
10
|
+
-- The price of an attempt comes from the first source that answers:
|
|
11
|
+
-- a response header (`header` x `perUnit`), the rate table (`rates`, keyed
|
|
12
|
+
-- '<entity>.<op>' / '<op>' / '*'), then the flat `unit`. A body figure
|
|
13
|
+
-- (`path` x `perUnit`, e.g. "usage.total_tokens") is read at PreDone
|
|
14
|
+
-- instead, from the already-parsed result. A body figure describes the
|
|
15
|
+
-- whole call, so it REPLACES the per-attempt estimate rather than adding
|
|
16
|
+
-- to it.
|
|
17
|
+
--
|
|
18
|
+
-- `budget` caps total spend. With `onBudget` = "deny" a further operation
|
|
19
|
+
-- is refused at PrePoint, before an endpoint is resolved and before
|
|
20
|
+
-- anything reaches the network.
|
|
21
|
+
--
|
|
22
|
+
-- ORDER MATTERS. Cost must sit INSIDE the cache, or a response served
|
|
23
|
+
-- from cache is charged for money that was never spent. The default (map)
|
|
24
|
+
-- order puts cache innermost and cost outside it, so activate them in
|
|
25
|
+
-- array form with cost first.
|
|
26
|
+
|
|
27
|
+
local BaseFeature = require("feature.base_feature")
|
|
28
|
+
local vs = require("utility.struct.struct")
|
|
29
|
+
|
|
30
|
+
local CostFeature = {}
|
|
31
|
+
CostFeature.__index = CostFeature
|
|
32
|
+
setmetatable(CostFeature, { __index = BaseFeature })
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
-- Case-insensitive header lookup on a plain header table. HTTP header
|
|
36
|
+
-- names are case-insensitive and a custom transport keeps conventional
|
|
37
|
+
-- casing ("X-Request-Cost"), so scan rather than index.
|
|
38
|
+
local function header_get(headers, name)
|
|
39
|
+
if type(headers) ~= "table" then
|
|
40
|
+
return nil
|
|
41
|
+
end
|
|
42
|
+
local lower = string.lower(name)
|
|
43
|
+
for k, v in pairs(headers) do
|
|
44
|
+
if type(k) == "string" and string.lower(k) == lower then
|
|
45
|
+
return v
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
return nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
function CostFeature.new()
|
|
53
|
+
local self = setmetatable(BaseFeature.new(), CostFeature)
|
|
54
|
+
self.version = "0.0.1"
|
|
55
|
+
self.name = "cost"
|
|
56
|
+
self.active = true
|
|
57
|
+
self.client = nil
|
|
58
|
+
self.options = nil
|
|
59
|
+
-- Weak keys: an abandoned context must not leak its accumulator.
|
|
60
|
+
self.pending = setmetatable({}, { __mode = "k" })
|
|
61
|
+
self.seq = 0
|
|
62
|
+
return self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
function CostFeature:init(ctx, options)
|
|
67
|
+
self.client = ctx.client
|
|
68
|
+
self.options = options or {}
|
|
69
|
+
|
|
70
|
+
if options["active"] == true then
|
|
71
|
+
self.active = true
|
|
72
|
+
else
|
|
73
|
+
self.active = false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
self.pending = setmetatable({}, { __mode = "k" })
|
|
77
|
+
self.seq = 0
|
|
78
|
+
|
|
79
|
+
local limit = self:_limit()
|
|
80
|
+
local client = self.client
|
|
81
|
+
if client._cost == nil then
|
|
82
|
+
client._cost = {
|
|
83
|
+
currency = self.options["currency"] or "USD",
|
|
84
|
+
total = { calls = 0, attempts = 0, amount = 0, reported = 0, estimated = 0 },
|
|
85
|
+
ops = {},
|
|
86
|
+
actors = {},
|
|
87
|
+
budget = { limit = limit, spent = 0, remaining = limit, exceeded = false },
|
|
88
|
+
last = nil,
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if not self.active then
|
|
93
|
+
return
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
local cost_self = self
|
|
97
|
+
local utility = ctx.utility
|
|
98
|
+
local inner = utility.fetcher
|
|
99
|
+
|
|
100
|
+
utility.fetcher = function(fctx, fullurl, fetchdef)
|
|
101
|
+
return cost_self:_charge(fctx, fullurl, fetchdef, inner)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
-- Budget gate. Runs before endpoint resolution, so a refused call costs
|
|
107
|
+
-- nothing at all.
|
|
108
|
+
function CostFeature:PrePoint(ctx)
|
|
109
|
+
if not self.active then
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
-- Mark the context as running through the pipeline, so _charge knows a
|
|
114
|
+
-- PreDone is coming and does not commit the spend itself.
|
|
115
|
+
local pending = self.pending[ctx]
|
|
116
|
+
if pending == nil then
|
|
117
|
+
pending = self:_new_pending()
|
|
118
|
+
self.pending[ctx] = pending
|
|
119
|
+
end
|
|
120
|
+
pending.piped = true
|
|
121
|
+
|
|
122
|
+
local limit = self:_limit()
|
|
123
|
+
if limit <= 0 then
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
local cost = self.client._cost
|
|
128
|
+
if cost.total.amount < limit then
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
cost.budget.exceeded = true
|
|
133
|
+
|
|
134
|
+
if self.options["onBudget"] ~= "deny" then
|
|
135
|
+
return
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
local err = ctx:make_error("cost_budget",
|
|
139
|
+
"Cost budget of " .. tostring(limit) .. " " .. cost.currency ..
|
|
140
|
+
" is spent (" .. tostring(cost.total.amount) .. " " .. cost.currency .. " used)")
|
|
141
|
+
-- Short-circuit endpoint resolution; the pipeline surfaces this error.
|
|
142
|
+
ctx.out["point"] = err
|
|
143
|
+
return err
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
function CostFeature:_charge(ctx, fullurl, fetchdef, inner)
|
|
148
|
+
-- A failing transport still costs an attempt. Without this, a run of
|
|
149
|
+
-- connection-level failures under `retry` (which retries on an error)
|
|
150
|
+
-- would be charged nothing at all, and an onBudget = "deny" ceiling
|
|
151
|
+
-- could never stop it.
|
|
152
|
+
local res, err = inner(ctx, fullurl, fetchdef)
|
|
153
|
+
|
|
154
|
+
local amount, source = self:_price(ctx, res)
|
|
155
|
+
local cost = self.client._cost
|
|
156
|
+
|
|
157
|
+
local pending = self.pending[ctx]
|
|
158
|
+
if pending == nil then
|
|
159
|
+
pending = self:_new_pending()
|
|
160
|
+
self.pending[ctx] = pending
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
pending.attempts = pending.attempts + 1
|
|
164
|
+
|
|
165
|
+
-- Accumulated here, committed once at PreDone. Adding each attempt to
|
|
166
|
+
-- the running total and then subtracting it again when a body figure
|
|
167
|
+
-- supersedes it loses precision to catastrophic cancellation
|
|
168
|
+
-- (5 + (0.01 - 5) is not 0.01 in binary floating point).
|
|
169
|
+
--
|
|
170
|
+
-- Reported and estimated are kept apart per ATTEMPT, not per operation:
|
|
171
|
+
-- a 503 priced from the rate table followed by a 200 carrying the cost
|
|
172
|
+
-- header is part estimate, part reported, and collapsing both into the
|
|
173
|
+
-- final attempt's category would corrupt the split.
|
|
174
|
+
pending.amount = pending.amount + amount
|
|
175
|
+
if source == "header" or source == "body" then
|
|
176
|
+
pending.reported = pending.reported + amount
|
|
177
|
+
else
|
|
178
|
+
pending.estimated = pending.estimated + amount
|
|
179
|
+
end
|
|
180
|
+
pending.source = source
|
|
181
|
+
|
|
182
|
+
cost.total.attempts = cost.total.attempts + 1
|
|
183
|
+
|
|
184
|
+
-- direct() and graphql() reach the transport without dispatching any
|
|
185
|
+
-- pipeline hooks - no PrePoint to gate on, and no PreDone to commit.
|
|
186
|
+
-- Their spend is committed here instead, or it would never be counted
|
|
187
|
+
-- and could run past an onBudget = "deny" ceiling indefinitely. `piped`
|
|
188
|
+
-- is set by PrePoint, so its absence is the signal.
|
|
189
|
+
if not pending.piped then
|
|
190
|
+
self:_commit(ctx, pending, "_", "direct")
|
|
191
|
+
self.pending[ctx] = nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
return res, err
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
function CostFeature:_new_pending()
|
|
199
|
+
return {
|
|
200
|
+
attempts = 0,
|
|
201
|
+
amount = 0,
|
|
202
|
+
reported = 0,
|
|
203
|
+
estimated = 0,
|
|
204
|
+
source = "none",
|
|
205
|
+
piped = false,
|
|
206
|
+
}
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
-- Attribute the operation's spend once the call is finished.
|
|
211
|
+
function CostFeature:PreDone(ctx)
|
|
212
|
+
self:_finish(ctx, true)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
-- A failed operation still spent the money. When the pipeline errors,
|
|
217
|
+
-- PreDone never runs, so without this the attempts are counted and the
|
|
218
|
+
-- spend is not, and a budget could never see the cost of a failed call.
|
|
219
|
+
-- Whichever hook fires first consumes the pending entry, so it commits
|
|
220
|
+
-- exactly once.
|
|
221
|
+
function CostFeature:PreUnexpected(ctx)
|
|
222
|
+
self:_finish(ctx, false)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
function CostFeature:_finish(ctx, done)
|
|
227
|
+
if not self.active then
|
|
228
|
+
return
|
|
229
|
+
end
|
|
230
|
+
local pending = self.pending[ctx]
|
|
231
|
+
if pending == nil then
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
self.pending[ctx] = nil
|
|
235
|
+
|
|
236
|
+
-- A FAILED operation that made no attempt never reached the network:
|
|
237
|
+
-- PrePoint creates the pending entry to mark the context as piped, and
|
|
238
|
+
-- then the budget gate refuses the call (rbac, or an unresolvable
|
|
239
|
+
-- endpoint, short-circuits just as early). Committing it would count a
|
|
240
|
+
-- call that never happened and file a zero-amount record as `last`.
|
|
241
|
+
--
|
|
242
|
+
-- A SUCCEEDED operation that made no attempt is the opposite case: it
|
|
243
|
+
-- was served from the cache. That is a real call, and the fact that it
|
|
244
|
+
-- cost nothing is the whole point of ordering cost inside the cache.
|
|
245
|
+
if not done and pending.attempts == 0 then
|
|
246
|
+
return
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
local entity = "_"
|
|
250
|
+
local opname = "_"
|
|
251
|
+
if ctx.op ~= nil then
|
|
252
|
+
if type(ctx.op.entity) == "string" and ctx.op.entity ~= "" then
|
|
253
|
+
entity = ctx.op.entity
|
|
254
|
+
end
|
|
255
|
+
if type(ctx.op.name) == "string" and ctx.op.name ~= "" then
|
|
256
|
+
opname = ctx.op.name
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
self:_commit(ctx, pending, entity, opname)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
-- Commit one operation's spend: totals, budget, per-op and per-actor
|
|
265
|
+
-- attribution, and the record. Shared by _finish and the raw-request path
|
|
266
|
+
-- in _charge, which has no PreDone to reach.
|
|
267
|
+
function CostFeature:_commit(ctx, pending, entity, opname)
|
|
268
|
+
local cost = self.client._cost
|
|
269
|
+
|
|
270
|
+
local amount = pending.amount
|
|
271
|
+
local reported = pending.reported
|
|
272
|
+
local estimated = pending.estimated
|
|
273
|
+
local source = pending.source
|
|
274
|
+
|
|
275
|
+
-- A body figure prices the whole call, so it replaces the per-attempt
|
|
276
|
+
-- estimate rather than adding to it - and, being server-stated, the
|
|
277
|
+
-- whole amount counts as reported.
|
|
278
|
+
local body = self:_body(ctx)
|
|
279
|
+
if body ~= nil then
|
|
280
|
+
amount = body
|
|
281
|
+
reported = body
|
|
282
|
+
estimated = 0
|
|
283
|
+
source = "body"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
self:_spend(cost, amount, reported, estimated)
|
|
287
|
+
|
|
288
|
+
local actor = nil
|
|
289
|
+
if ctx.ctrl ~= nil and ctx.ctrl.actor ~= nil then
|
|
290
|
+
actor = ctx.ctrl.actor
|
|
291
|
+
end
|
|
292
|
+
if actor == nil then
|
|
293
|
+
actor = self.options["actor"]
|
|
294
|
+
end
|
|
295
|
+
if actor == nil then
|
|
296
|
+
actor = "anonymous"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
cost.total.calls = cost.total.calls + 1
|
|
300
|
+
self:_bump(cost.ops, entity .. "." .. opname, amount)
|
|
301
|
+
self:_bump(cost.actors, actor, amount)
|
|
302
|
+
|
|
303
|
+
self.seq = self.seq + 1
|
|
304
|
+
local record = {
|
|
305
|
+
seq = self.seq,
|
|
306
|
+
entity = entity,
|
|
307
|
+
op = opname,
|
|
308
|
+
actor = actor,
|
|
309
|
+
amount = amount,
|
|
310
|
+
currency = cost.currency,
|
|
311
|
+
source = source,
|
|
312
|
+
attempts = pending.attempts,
|
|
313
|
+
}
|
|
314
|
+
cost.last = record
|
|
315
|
+
|
|
316
|
+
local sink = self.options["sink"]
|
|
317
|
+
if type(sink) == "function" then
|
|
318
|
+
pcall(sink, record)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
-- Price one attempt: a reported header figure, else the rate table, else
|
|
324
|
+
-- the flat unit. Returns amount, source.
|
|
325
|
+
function CostFeature:_price(ctx, res)
|
|
326
|
+
local header = self.options["header"]
|
|
327
|
+
if type(header) == "string" and header ~= "" then
|
|
328
|
+
local v = self:_header(res, header)
|
|
329
|
+
if v ~= nil then
|
|
330
|
+
return v * self:_per_unit(), "header"
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
local rate = self:_rate(ctx)
|
|
335
|
+
if rate ~= nil then
|
|
336
|
+
return rate, "table"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
local unit = self.options["unit"]
|
|
340
|
+
if type(unit) == "number" and unit ~= 0 then
|
|
341
|
+
return unit, "unit"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
return 0, "none"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
-- The rate table uses the same lookup grammar as rbac's rules:
|
|
349
|
+
-- '<entity>.<op>', then '<op>', then '*'.
|
|
350
|
+
function CostFeature:_rate(ctx)
|
|
351
|
+
local rates = self.options["rates"] or {}
|
|
352
|
+
|
|
353
|
+
local entity = ""
|
|
354
|
+
if ctx.entity ~= nil then
|
|
355
|
+
if type(ctx.entity.get_name) == "function" then
|
|
356
|
+
entity = ctx.entity:get_name()
|
|
357
|
+
elseif type(ctx.entity.name) == "string" then
|
|
358
|
+
entity = ctx.entity.name
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
if entity == "" and ctx.op ~= nil and type(ctx.op.entity) == "string" then
|
|
362
|
+
entity = ctx.op.entity
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
local opname = ""
|
|
366
|
+
if ctx.op ~= nil and type(ctx.op.name) == "string" then
|
|
367
|
+
opname = ctx.op.name
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
if type(rates[entity .. "." .. opname]) == "number" then
|
|
371
|
+
return rates[entity .. "." .. opname]
|
|
372
|
+
end
|
|
373
|
+
if type(rates[opname]) == "number" then
|
|
374
|
+
return rates[opname]
|
|
375
|
+
end
|
|
376
|
+
if type(rates["*"]) == "number" then
|
|
377
|
+
return rates["*"]
|
|
378
|
+
end
|
|
379
|
+
return nil
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
-- A usage figure from the parsed result body, priced by perUnit. Read
|
|
384
|
+
-- here, not at the transport seam, because the body is consumed once.
|
|
385
|
+
function CostFeature:_body(ctx)
|
|
386
|
+
local path = self.options["path"]
|
|
387
|
+
if type(path) ~= "string" or path == "" then
|
|
388
|
+
return nil
|
|
389
|
+
end
|
|
390
|
+
if ctx.result == nil or type(ctx.result.body) ~= "table" then
|
|
391
|
+
return nil
|
|
392
|
+
end
|
|
393
|
+
local v = vs.getpath(ctx.result.body, path)
|
|
394
|
+
local n = tonumber(v)
|
|
395
|
+
if n == nil then
|
|
396
|
+
return nil
|
|
397
|
+
end
|
|
398
|
+
return n * self:_per_unit()
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
function CostFeature:_spend(cost, amount, reported, estimated)
|
|
403
|
+
cost.total.amount = cost.total.amount + amount
|
|
404
|
+
cost.total.reported = cost.total.reported + reported
|
|
405
|
+
cost.total.estimated = cost.total.estimated + estimated
|
|
406
|
+
|
|
407
|
+
local limit = cost.budget.limit
|
|
408
|
+
cost.budget.spent = cost.total.amount
|
|
409
|
+
if limit > 0 then
|
|
410
|
+
cost.budget.remaining = math.max(0, limit - cost.total.amount)
|
|
411
|
+
if cost.total.amount >= limit then
|
|
412
|
+
cost.budget.exceeded = true
|
|
413
|
+
end
|
|
414
|
+
else
|
|
415
|
+
cost.budget.remaining = 0
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
function CostFeature:_bump(bucket, key, amount)
|
|
421
|
+
local b = bucket[key]
|
|
422
|
+
if b == nil then
|
|
423
|
+
b = { calls = 0, amount = 0 }
|
|
424
|
+
bucket[key] = b
|
|
425
|
+
end
|
|
426
|
+
b.calls = b.calls + 1
|
|
427
|
+
b.amount = b.amount + amount
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
function CostFeature:_header(res, name)
|
|
432
|
+
if res == nil then
|
|
433
|
+
return nil
|
|
434
|
+
end
|
|
435
|
+
return tonumber(header_get(res["headers"], name))
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
function CostFeature:_per_unit()
|
|
440
|
+
local p = self.options["perUnit"]
|
|
441
|
+
if type(p) == "number" then
|
|
442
|
+
return p
|
|
443
|
+
end
|
|
444
|
+
return 0
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
function CostFeature:_limit()
|
|
449
|
+
local b = self.options["budget"]
|
|
450
|
+
if type(b) == "number" then
|
|
451
|
+
return b
|
|
452
|
+
end
|
|
453
|
+
return 0
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
return CostFeature
|
|
File without changes
|
|
File without changes
|