@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,442 @@
|
|
|
1
|
+
# ProjectName SDK cost feature
|
|
2
|
+
#
|
|
3
|
+
# Cost tracking and spend budget. Uses BOTH seams, which is the point of the
|
|
4
|
+
# feature: money is spent per HTTP ATTEMPT (a retried call is charged again,
|
|
5
|
+
# because the upstream API charges it again), but it is owed by an OPERATION.
|
|
6
|
+
# So the transport wrap prices each attempt, and PreDone attributes the
|
|
7
|
+
# running total to `<entity>.<op>` and to the caller (`ctrl.actor`, the same
|
|
8
|
+
# actor the audit feature records).
|
|
9
|
+
#
|
|
10
|
+
# The price of an attempt comes from the first source that answers: a
|
|
11
|
+
# 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 instead,
|
|
14
|
+
# from the already-parsed result. A body figure describes the whole call, so
|
|
15
|
+
# it REPLACES the per-attempt estimate rather than adding to it.
|
|
16
|
+
#
|
|
17
|
+
# `budget` caps total spend. With `onBudget` = "deny" a further operation is
|
|
18
|
+
# refused at PrePoint, before an endpoint is resolved and before anything
|
|
19
|
+
# reaches the network.
|
|
20
|
+
#
|
|
21
|
+
# ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
|
|
22
|
+
# cache is charged for money that was never spent. Aggregates live on
|
|
23
|
+
# `client._cost`; the per-operation accumulator lives on the ctx, the same
|
|
24
|
+
# place metrics keeps its start marker.
|
|
25
|
+
|
|
26
|
+
defmodule ProjectName.Feature.Cost do
|
|
27
|
+
alias Voxgig.Struct, as: S
|
|
28
|
+
alias ProjectName.Helpers, as: H
|
|
29
|
+
alias ProjectName.{Feature, Context}
|
|
30
|
+
|
|
31
|
+
def new do
|
|
32
|
+
f = Feature.base("cost")
|
|
33
|
+
Feature.install(f, "init", fn ctx, opts -> init(f, ctx, opts) end)
|
|
34
|
+
Feature.install(f, "PrePoint", fn ctx -> pre_point(f, ctx) end)
|
|
35
|
+
Feature.install(f, "PreDone", fn ctx -> finish(f, ctx, true) end)
|
|
36
|
+
Feature.install(f, "PreUnexpected", fn ctx -> finish(f, ctx, false) end)
|
|
37
|
+
f
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def init(f, ctx, options) do
|
|
41
|
+
active = Feature.init_common(f, ctx, options)
|
|
42
|
+
|
|
43
|
+
client = S.getprop(f, "client")
|
|
44
|
+
opts = Feature.opts(f)
|
|
45
|
+
lim = limit(opts)
|
|
46
|
+
|
|
47
|
+
Feature.track_node(
|
|
48
|
+
client,
|
|
49
|
+
"_cost",
|
|
50
|
+
S.jm([
|
|
51
|
+
"currency", H.or_(S.getprop(opts, "currency"), "USD"),
|
|
52
|
+
"total", S.jm(["calls", 0, "attempts", 0, "amount", 0, "reported", 0, "estimated", 0]),
|
|
53
|
+
"ops", S.jm([]),
|
|
54
|
+
"actors", S.jm([]),
|
|
55
|
+
"budget", S.jm(["limit", lim, "spent", 0, "remaining", lim, "exceeded", false]),
|
|
56
|
+
"last", nil
|
|
57
|
+
])
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
S.setprop(f, "seq", 0)
|
|
61
|
+
|
|
62
|
+
if active do
|
|
63
|
+
utility = S.getprop(ctx, "utility")
|
|
64
|
+
inner = S.getprop(utility, "fetcher")
|
|
65
|
+
S.setprop(utility, "fetcher", fn fctx, url, fd -> charge(f, fctx, url, fd, inner) end)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Budget gate. Runs before endpoint resolution, so a refused call costs
|
|
72
|
+
# nothing at all.
|
|
73
|
+
defp pre_point(f, ctx) do
|
|
74
|
+
if not Feature.active?(f) do
|
|
75
|
+
nil
|
|
76
|
+
else
|
|
77
|
+
# Mark the context as running through the pipeline, so charge knows a
|
|
78
|
+
# PreDone is coming and does not commit the spend itself.
|
|
79
|
+
pending = pending_for(ctx)
|
|
80
|
+
S.setprop(pending, "piped", true)
|
|
81
|
+
|
|
82
|
+
opts = Feature.opts(f)
|
|
83
|
+
lim = limit(opts)
|
|
84
|
+
cost = S.getprop(S.getprop(f, "client"), "_cost")
|
|
85
|
+
total = S.getprop(cost, "total")
|
|
86
|
+
|
|
87
|
+
cond do
|
|
88
|
+
lim <= 0 ->
|
|
89
|
+
nil
|
|
90
|
+
|
|
91
|
+
S.getprop(total, "amount") < lim ->
|
|
92
|
+
nil
|
|
93
|
+
|
|
94
|
+
true ->
|
|
95
|
+
S.setprop(S.getprop(cost, "budget"), "exceeded", true)
|
|
96
|
+
|
|
97
|
+
if S.getprop(opts, "onBudget") != "deny" do
|
|
98
|
+
nil
|
|
99
|
+
else
|
|
100
|
+
currency = S.getprop(cost, "currency")
|
|
101
|
+
|
|
102
|
+
err =
|
|
103
|
+
Context.make_error(
|
|
104
|
+
ctx,
|
|
105
|
+
"cost_budget",
|
|
106
|
+
"Cost budget of " <>
|
|
107
|
+
to_string(lim) <>
|
|
108
|
+
" " <>
|
|
109
|
+
to_string(currency) <>
|
|
110
|
+
" is spent (" <>
|
|
111
|
+
to_string(S.getprop(total, "amount")) <>
|
|
112
|
+
" " <> to_string(currency) <> " used)"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Short-circuit endpoint resolution; the pipeline surfaces this error.
|
|
116
|
+
S.setprop(S.getprop(ctx, "out"), "point", err)
|
|
117
|
+
err
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A failing transport still costs an attempt. Without this, a run of
|
|
124
|
+
# connection-level failures under `retry` (which retries on an error) would
|
|
125
|
+
# be charged nothing at all, and an onBudget = "deny" ceiling could never
|
|
126
|
+
# stop it.
|
|
127
|
+
defp charge(f, ctx, url, fetchdef, inner) do
|
|
128
|
+
{res, err, raised} =
|
|
129
|
+
try do
|
|
130
|
+
{r, e} = inner.(ctx, url, fetchdef)
|
|
131
|
+
{r, e, nil}
|
|
132
|
+
rescue
|
|
133
|
+
e -> {nil, nil, e}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
opts = Feature.opts(f)
|
|
137
|
+
{amount, source} = price(f, ctx, if(raised != nil, do: nil, else: res))
|
|
138
|
+
|
|
139
|
+
cost = S.getprop(S.getprop(f, "client"), "_cost")
|
|
140
|
+
total = S.getprop(cost, "total")
|
|
141
|
+
|
|
142
|
+
pending = pending_for(ctx)
|
|
143
|
+
|
|
144
|
+
# Accumulated here, committed once at PreDone. Adding each attempt to the
|
|
145
|
+
# running total and then subtracting it again when a body figure
|
|
146
|
+
# supersedes it loses precision to catastrophic cancellation
|
|
147
|
+
# (5 + (0.01 - 5) is not 0.01 in binary floating point).
|
|
148
|
+
#
|
|
149
|
+
# Reported and estimated are kept apart per ATTEMPT, not per operation: a
|
|
150
|
+
# 503 priced from the rate table followed by a 200 carrying the cost
|
|
151
|
+
# header is part estimate, part reported, and collapsing both into the
|
|
152
|
+
# final attempt's category would corrupt the split.
|
|
153
|
+
S.setprop(pending, "attempts", S.getprop(pending, "attempts") + 1)
|
|
154
|
+
S.setprop(pending, "amount", S.getprop(pending, "amount") + amount)
|
|
155
|
+
|
|
156
|
+
bucket = if source == "header" or source == "body", do: "reported", else: "estimated"
|
|
157
|
+
S.setprop(pending, bucket, S.getprop(pending, bucket) + amount)
|
|
158
|
+
S.setprop(pending, "source", source)
|
|
159
|
+
|
|
160
|
+
S.setprop(total, "attempts", S.getprop(total, "attempts") + 1)
|
|
161
|
+
|
|
162
|
+
# direct() and graphql() reach the transport without dispatching any
|
|
163
|
+
# pipeline hooks - no PrePoint to gate on, and no PreDone to commit. Their
|
|
164
|
+
# spend is committed here instead, or it would never be counted and could
|
|
165
|
+
# run past an onBudget = "deny" ceiling indefinitely. `piped` is set by
|
|
166
|
+
# PrePoint, so its absence is the signal.
|
|
167
|
+
if S.getprop(pending, "piped") != true do
|
|
168
|
+
commit(f, ctx, pending, "_", "direct", opts)
|
|
169
|
+
S.delprop(ctx, "_cost_pending")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if raised != nil, do: raise(raised), else: {res, err}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
defp pending_for(ctx) do
|
|
176
|
+
pending = S.getprop(ctx, "_cost_pending")
|
|
177
|
+
|
|
178
|
+
if pending == nil do
|
|
179
|
+
p =
|
|
180
|
+
S.jm([
|
|
181
|
+
"attempts", 0,
|
|
182
|
+
"amount", 0,
|
|
183
|
+
"reported", 0,
|
|
184
|
+
"estimated", 0,
|
|
185
|
+
"source", "none",
|
|
186
|
+
"piped", false
|
|
187
|
+
])
|
|
188
|
+
|
|
189
|
+
S.setprop(ctx, "_cost_pending", p)
|
|
190
|
+
p
|
|
191
|
+
else
|
|
192
|
+
pending
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# PreDone attributes the operation's spend; PreUnexpected commits a FAILED
|
|
197
|
+
# operation's, since PreDone never runs when the pipeline raises. Whichever
|
|
198
|
+
# fires first consumes the pending entry, so it commits exactly once.
|
|
199
|
+
defp finish(f, ctx, done) do
|
|
200
|
+
if not Feature.active?(f) do
|
|
201
|
+
nil
|
|
202
|
+
else
|
|
203
|
+
pending = S.getprop(ctx, "_cost_pending")
|
|
204
|
+
|
|
205
|
+
cond do
|
|
206
|
+
pending == nil ->
|
|
207
|
+
nil
|
|
208
|
+
|
|
209
|
+
# A FAILED operation that made no attempt never reached the network:
|
|
210
|
+
# PrePoint creates the pending entry to mark the context as piped, and
|
|
211
|
+
# then the budget gate refuses the call (rbac, or an unresolvable
|
|
212
|
+
# endpoint, short-circuits just as early). Committing it would count a
|
|
213
|
+
# call that never happened and file a zero-amount record as `last`.
|
|
214
|
+
#
|
|
215
|
+
# A SUCCEEDED operation that made no attempt is the opposite case: it
|
|
216
|
+
# was served from the cache. That is a real call, and the fact that it
|
|
217
|
+
# cost nothing is the whole point of ordering cost inside the cache.
|
|
218
|
+
not done and S.getprop(pending, "attempts") == 0 ->
|
|
219
|
+
S.delprop(ctx, "_cost_pending")
|
|
220
|
+
nil
|
|
221
|
+
|
|
222
|
+
true ->
|
|
223
|
+
S.delprop(ctx, "_cost_pending")
|
|
224
|
+
commit(f, ctx, pending, entity_of(ctx), opname_of(ctx), Feature.opts(f))
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Commit one operation's spend: totals, budget, per-op and per-actor
|
|
230
|
+
# attribution, and the record. Shared by finish and the raw-request path in
|
|
231
|
+
# charge, which has no PreDone to reach.
|
|
232
|
+
defp commit(f, ctx, pending, entity, opname, opts) do
|
|
233
|
+
cost = S.getprop(S.getprop(f, "client"), "_cost")
|
|
234
|
+
total = S.getprop(cost, "total")
|
|
235
|
+
|
|
236
|
+
# A body figure prices the whole call, so it replaces the per-attempt
|
|
237
|
+
# estimate rather than adding to it - and, being server-stated, the whole
|
|
238
|
+
# amount counts as reported.
|
|
239
|
+
body = body_amount(ctx, opts)
|
|
240
|
+
|
|
241
|
+
{amount, reported, estimated, source} =
|
|
242
|
+
if body == nil do
|
|
243
|
+
{S.getprop(pending, "amount"), S.getprop(pending, "reported"),
|
|
244
|
+
S.getprop(pending, "estimated"), S.getprop(pending, "source")}
|
|
245
|
+
else
|
|
246
|
+
{body, body, 0, "body"}
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
spend(cost, amount, reported, estimated)
|
|
250
|
+
|
|
251
|
+
ctrl = S.getprop(ctx, "ctrl")
|
|
252
|
+
actor = if ctrl != nil, do: S.getprop(ctrl, "actor"), else: nil
|
|
253
|
+
actor = if actor == nil, do: S.getprop(opts, "actor"), else: actor
|
|
254
|
+
actor = if actor == nil, do: "anonymous", else: actor
|
|
255
|
+
|
|
256
|
+
S.setprop(total, "calls", S.getprop(total, "calls") + 1)
|
|
257
|
+
bump(S.getprop(cost, "ops"), entity <> "." <> opname, amount)
|
|
258
|
+
bump(S.getprop(cost, "actors"), to_string(actor), amount)
|
|
259
|
+
|
|
260
|
+
seq = H.or_(S.getprop(f, "seq"), 0) + 1
|
|
261
|
+
S.setprop(f, "seq", seq)
|
|
262
|
+
|
|
263
|
+
record =
|
|
264
|
+
S.jm([
|
|
265
|
+
"seq", seq,
|
|
266
|
+
"entity", entity,
|
|
267
|
+
"op", opname,
|
|
268
|
+
"actor", actor,
|
|
269
|
+
"amount", amount,
|
|
270
|
+
"currency", S.getprop(cost, "currency"),
|
|
271
|
+
"source", source,
|
|
272
|
+
"attempts", S.getprop(pending, "attempts")
|
|
273
|
+
])
|
|
274
|
+
|
|
275
|
+
S.setprop(cost, "last", record)
|
|
276
|
+
|
|
277
|
+
sink = S.getprop(opts, "sink")
|
|
278
|
+
|
|
279
|
+
if S.isfunc(sink) do
|
|
280
|
+
# A sink must never break the call it is reporting on.
|
|
281
|
+
try do
|
|
282
|
+
sink.(record)
|
|
283
|
+
rescue
|
|
284
|
+
_ -> nil
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
nil
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Price one attempt: a reported header figure, else the rate table, else the
|
|
292
|
+
# flat unit.
|
|
293
|
+
defp price(f, ctx, res) do
|
|
294
|
+
opts = Feature.opts(f)
|
|
295
|
+
header = S.getprop(opts, "header")
|
|
296
|
+
|
|
297
|
+
hv =
|
|
298
|
+
if is_binary(header) and header != "" do
|
|
299
|
+
header_num(res, header)
|
|
300
|
+
else
|
|
301
|
+
nil
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
cond do
|
|
305
|
+
hv != nil ->
|
|
306
|
+
{hv * per_unit(opts), "header"}
|
|
307
|
+
|
|
308
|
+
true ->
|
|
309
|
+
rate = rate_for(ctx, opts)
|
|
310
|
+
unit = S.getprop(opts, "unit")
|
|
311
|
+
|
|
312
|
+
cond do
|
|
313
|
+
rate != nil -> {rate, "table"}
|
|
314
|
+
is_number(unit) and unit != 0 -> {unit, "unit"}
|
|
315
|
+
true -> {0, "none"}
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# The rate table uses the same lookup grammar as rbac's rules:
|
|
321
|
+
# `<entity>.<op>`, then `<op>`, then `*`.
|
|
322
|
+
defp rate_for(ctx, opts) do
|
|
323
|
+
rates = S.getprop(opts, "rates")
|
|
324
|
+
rates = if S.ismap(rates), do: rates, else: S.jm([])
|
|
325
|
+
|
|
326
|
+
entity = entity_of(ctx)
|
|
327
|
+
opname = opname_of(ctx)
|
|
328
|
+
|
|
329
|
+
Enum.find_value([entity <> "." <> opname, opname, "*"], fn key ->
|
|
330
|
+
v = S.getprop(rates, key)
|
|
331
|
+
if is_number(v) and not is_boolean(v), do: v
|
|
332
|
+
end)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# A usage figure from the parsed result body, priced by perUnit. Read here,
|
|
336
|
+
# not at the transport seam, because the body is consumed once.
|
|
337
|
+
defp body_amount(ctx, opts) do
|
|
338
|
+
path = S.getprop(opts, "path")
|
|
339
|
+
result = S.getprop(ctx, "result")
|
|
340
|
+
body = if result != nil, do: S.getprop(result, "body"), else: nil
|
|
341
|
+
|
|
342
|
+
if not is_binary(path) or path == "" or not S.ismap(body) do
|
|
343
|
+
nil
|
|
344
|
+
else
|
|
345
|
+
v = S.getpath(body, path)
|
|
346
|
+
if is_number(v) and not is_boolean(v), do: v * per_unit(opts), else: nil
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
defp spend(cost, amount, reported, estimated) do
|
|
351
|
+
total = S.getprop(cost, "total")
|
|
352
|
+
S.setprop(total, "amount", S.getprop(total, "amount") + amount)
|
|
353
|
+
S.setprop(total, "reported", S.getprop(total, "reported") + reported)
|
|
354
|
+
S.setprop(total, "estimated", S.getprop(total, "estimated") + estimated)
|
|
355
|
+
|
|
356
|
+
budget = S.getprop(cost, "budget")
|
|
357
|
+
lim = S.getprop(budget, "limit")
|
|
358
|
+
spent = S.getprop(total, "amount")
|
|
359
|
+
S.setprop(budget, "spent", spent)
|
|
360
|
+
|
|
361
|
+
if lim > 0 do
|
|
362
|
+
S.setprop(budget, "remaining", max(0, lim - spent))
|
|
363
|
+
if spent >= lim, do: S.setprop(budget, "exceeded", true)
|
|
364
|
+
else
|
|
365
|
+
S.setprop(budget, "remaining", 0)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
nil
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
defp bump(bucket, key, amount) do
|
|
372
|
+
b = S.getprop(bucket, key)
|
|
373
|
+
|
|
374
|
+
b =
|
|
375
|
+
if b == nil do
|
|
376
|
+
nb = S.jm(["calls", 0, "amount", 0])
|
|
377
|
+
S.setprop(bucket, key, nb)
|
|
378
|
+
nb
|
|
379
|
+
else
|
|
380
|
+
b
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
S.setprop(b, "calls", S.getprop(b, "calls") + 1)
|
|
384
|
+
S.setprop(b, "amount", S.getprop(b, "amount") + amount)
|
|
385
|
+
nil
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# HTTP header names are case-insensitive and a custom transport keeps
|
|
389
|
+
# conventional casing ("X-Request-Cost"), so header_get scans rather than
|
|
390
|
+
# indexes.
|
|
391
|
+
defp header_num(res, name) do
|
|
392
|
+
headers = if S.ismap(res), do: S.getprop(res, "headers"), else: nil
|
|
393
|
+
|
|
394
|
+
if not S.ismap(headers) do
|
|
395
|
+
nil
|
|
396
|
+
else
|
|
397
|
+
v = Feature.header_get(headers, name)
|
|
398
|
+
|
|
399
|
+
cond do
|
|
400
|
+
is_number(v) and not is_boolean(v) -> v
|
|
401
|
+
is_binary(v) -> parse_num(v)
|
|
402
|
+
true -> nil
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
defp parse_num(s) do
|
|
408
|
+
case Float.parse(String.trim(s)) do
|
|
409
|
+
{n, _} -> n
|
|
410
|
+
:error -> nil
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
defp entity_of(ctx) do
|
|
415
|
+
name = Context.entity_name(S.getprop(ctx, "entity"))
|
|
416
|
+
name = if name == "_", do: "", else: name
|
|
417
|
+
|
|
418
|
+
if name == "" do
|
|
419
|
+
op = S.getprop(ctx, "op")
|
|
420
|
+
oe = if op != nil, do: S.getprop(op, "entity"), else: nil
|
|
421
|
+
if is_binary(oe) and oe != "", do: oe, else: "_"
|
|
422
|
+
else
|
|
423
|
+
name
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
defp opname_of(ctx) do
|
|
428
|
+
op = S.getprop(ctx, "op")
|
|
429
|
+
nm = if op != nil, do: S.getprop(op, "name"), else: nil
|
|
430
|
+
if is_binary(nm) and nm != "", do: nm, else: "_"
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
defp per_unit(opts) do
|
|
434
|
+
p = S.getprop(opts, "perUnit")
|
|
435
|
+
if is_number(p) and not is_boolean(p), do: p, else: 0
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
defp limit(opts) do
|
|
439
|
+
b = S.getprop(opts, "budget")
|
|
440
|
+
if is_number(b) and not is_boolean(b), do: b, else: 0
|
|
441
|
+
end
|
|
442
|
+
end
|
|
File without changes
|