Metric labels
Requests counted into time series, one per combination of label values: which labels keep taking new values as the traffic grows, how many series each labelling makes, and what counters and histograms cost to keep.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
Requests counted into time series, one series for each combination of label values: method, status, path, endpoint (the path with its numbers replaced by :id) and user.
main.eml- the menu, the requests, the values each label takes, the series each labelling makes, what storing them costs, and a labelling of your ownlabels.eml- the request log, templating a path, a request's label values, and the counts of series and of distinct values
How each part works:
- The requests are generated the same every run; the users and the order numbers grow with the traffic.
- As in the corpus case
metric-cardinality-explosion, path was declared with 5 values, one per kind of path, and carries ids: 89 values at 200 requests and 402 at 1000. Which labels grow is measured on the data at two volumes, not read from the declaration. Templating the path back to its endpoint keeps it at 5. - At 1000 requests: method and status make 20 series, as declared; adding endpoint makes 98 of the 100 declared; adding path instead makes 627.
- When a labelling's series grow while none of its labels takes a new value, the new series are combinations of values already seen, filling in; they can never pass the product of those values.
- As in the corpus case
counter-vs-histogram, a counter is one number a series and a histogram of 8 buckets with its sum and count is ten, but only the histogram can answer a percentile later.
What is checked: menu choices 1 to 6; labels from method, status, path, endpoint and user, each named once. An empty answer cancels.
Sessions: sessions/basic.in gives: - the requests, the labels, the series and the storage; - method endpoint, which stays at 20, and status user, which grows.
sessions/bad-input.in gives: - menu choices 0 and x; - labels: a cancel, method twice, colour; - path alone, which grows, and method status, which only fills in.
Built on the verified corpus cases metric-cardinality-explosion (a label that looked bounded until a path held an id) and counter-vs-histogram (a summary chosen at write time decides which questions it can answer).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== Metric labels ==
Requests counted into time series, one series for each combination of label values.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 0
Pick a number from 1 to 6.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> x
Pick a number from 1 to 6.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 5
Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.
labels>
Cancelled.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 5
Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.
labels> method method
Name each label once.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 5
Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.
labels> method colour
Type labels from method, status, path, endpoint and user.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 5
Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.
labels> path
path: 89 series at 200 requests, 402 at 1000; declared 5.
path kept taking new values as the traffic grew: these series have no bound.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 5
Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.
labels> method status
method status: 19 series at 200 requests, 20 at 1000; declared 20.
No label here took a new value; the extra series are combinations of values already seen, filling in.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 6
Bye.
What was typed (13 lines)
0
x
5
5
method method
5
method colour
5
path
5
method status
6
basic
interpreter: byte-equal== Metric labels ==
Requests counted into time series, one series for each combination of label values.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 1
The first 8 requests (method, status, path, user):
1 POST 200 /health user 8
2 GET 500 /orders/1034 user 6
3 PUT 200 /orders/1033 user 6
4 GET 200 /search user 1
5 GET 200 /users/6/orders user 6
6 GET 201 /health user 4
7 POST 200 /orders/1056 user 10
8 PUT 404 /orders/1006 user 12
Every path is one of /users/<id>/orders, /users/<id>, /orders/<id>, /health and /search.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 2
Distinct values each label takes, as the traffic grows:
label declared 200 requests 1000 requests
method 4 4 4
status 5 5 5
path 5 89 402 grows with the traffic
endpoint 5 5 5
user - 47 204 grows with the traffic
path holds the ids written into it; endpoint is the path with every number replaced by :id.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 3
Series to store, one per combination of label values seen, at 1000 requests:
labels declared seen
method status 20 20
method status endpoint 100 98
method status path 100 627
method status endpoint user unbounded 944
Declared counts multiply every value with every other, so they are upper bounds when every value is bounded; a label carrying ids has no bound at all.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 4
Numbers kept per series: a counter is 1; a histogram of 8 buckets, with its sum and count, is 10.
labels series counters histograms
method status 20 20 200
method status endpoint 98 98 980
method status path 627 627 6270
method status endpoint user 944 944 9440
Only a histogram can answer a percentile later; it costs ten numbers a series, and every series is kept for the whole retention.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 5
Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.
labels> method endpoint
method endpoint: 20 series at 200 requests, 20 at 1000; declared 20.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 5
Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.
labels> status user
status user: 106 series at 200 requests, 501 at 1000; declared unbounded.
user kept taking new values as the traffic grew: these series have no bound.
1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit
choice> 6
Bye.
What was typed (9 lines)
1
2
3
4
5
method endpoint
5
status user
6
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P133 metric labels: the requests, the values each label takes as traffic
# grows, the series each labelling makes, what storing them costs, and a
# labelling of your own.
import labels
def plain(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def pad(s, n):
s => out
while len(out) < n:
out + " " => out
return out
def lpad(s, n):
s => out
while len(out) < n:
" " + out => out
return out
def declared(label):
# the values the label was declared with; 0 for none
if label == "method":
return 4
if label == "status":
return 5
if label == "path" or label == "endpoint":
return 5
return 0
def show_requests():
"The first 8 requests (method, status, path, user):" ^0
for i in [1:8]:
labels.request(i) => r
(" " + lpad(str(i), 2) + " " + pad(r[0], 7) + pad(str(r[1]), 5) + pad(r[2], 18) + "user " + str(r[3])) ^0
"Every path is one of /users/<id>/orders, /users/<id>, /orders/<id>, /health and /search." ^0
def show_labels():
"Distinct values each label takes, as the traffic grows:" ^0
" label declared 200 requests 1000 requests" ^0
for l in labels.labels():
labels.distinct(200, l) => a
labels.distinct(1000, l) => b
"-" => d
if declared(l) > 0:
str(declared(l)) => d
"" => note
if b > a:
" grows with the traffic" => note
(" " + pad(l, 9) + lpad(d, 10) + lpad(str(a), 15) + lpad(str(b), 16) + note) ^0
"path holds the ids written into it; endpoint is the path with every number replaced by :id." ^0
def labellings():
return [["method", "status"], ["method", "status", "endpoint"], ["method", "status", "path"], ["method", "status", "endpoint", "user"]]
def names(ls):
"" => out
for l in ls:
if out != "":
out + " " => out
out + l => out
return out
def predicted(ls):
1 => p
for l in ls:
if declared(l) == 0:
return 0 - 1
p * declared(l) => p
return p
def show_series():
"Series to store, one per combination of label values seen, at 1000 requests:" ^0
" labels declared seen" ^0
for ls in labellings():
predicted(ls) => p
"unbounded" => d
if p >= 0:
str(p) => d
(" " + pad(names(ls), 28) + lpad(d, 10) + lpad(str(labels.series(1000, ls)), 7)) ^0
"Declared counts multiply every value with every other, so they are upper bounds when every value is bounded; a label carrying ids has no bound at all." ^0
def show_storage():
"Numbers kept per series: a counter is 1; a histogram of 8 buckets, with its sum and count, is 10." ^0
" labels series counters histograms" ^0
for ls in labellings():
labels.series(1000, ls) => n
(" " + pad(names(ls), 28) + lpad(str(n), 8) + lpad(str(n), 11) + lpad(str(10 * n), 13)) ^0
"Only a histogram can answer a percentile later; it costs ten numbers a series, and every series is kept for the whole retention." ^0
def own():
"Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint." ^0
plain(input("labels> ")) => t
if t == "":
"Cancelled." ^0
return
[] => ls
"" => w
for c in t + " ":
if c == " ":
if w != "":
False => ok
for l in labels.labels():
if l == w:
True => ok
if not ok:
"Type labels from method, status, path, endpoint and user." ^0
return
for x in ls:
if x == w:
"Name each label once." ^0
return
ls + [w] => ls
"" => w
else:
w + c => w
labels.series(200, ls) => a
labels.series(1000, ls) => b
predicted(ls) => p
"unbounded" => d
if p >= 0:
str(p) => d
(names(ls) + ": " + str(a) + " series at 200 requests, " + str(b) + " at 1000; declared " + d + ".") ^0
[] => growing
for l in ls:
if labels.distinct(1000, l) > labels.distinct(200, l):
growing + [l] => growing
if len(growing) > 0:
(" " + names(growing) + " kept taking new values as the traffic grew: these series have no bound.") ^0
elif b > a:
" No label here took a new value; the extra series are combinations of values already seen, filling in." ^0
"== Metric labels ==" ^0
"Requests counted into time series, one series for each combination of label values." ^0
True => running
while running:
"" ^0
"1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit" ^0
plain(input("choice> ")) => choice
if choice == "1":
show_requests()
elif choice == "2":
show_labels()
elif choice == "3":
show_series()
elif choice == "4":
show_storage()
elif choice == "5":
own()
elif choice == "6":
False => running
else:
"Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import labels
def plain(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def pad(s, n):
out = s
while len(out) < n:
out = out + " "
return out
def lpad(s, n):
out = s
while len(out) < n:
out = " " + out
return out
def declared(label):
if label == "method":
return 4
if label == "status":
return 5
if label == "path" or label == "endpoint":
return 5
return 0
def show_requests():
print("The first 8 requests (method, status, path, user):")
for i in range(1, 9):
r = labels.request(i)
print(" " + lpad(str(i), 2) + " " + pad(r[0], 7) + pad(str(r[1]), 5) + pad(r[2], 18) + "user " + str(r[3]))
print("Every path is one of /users/<id>/orders, /users/<id>, /orders/<id>, /health and /search.")
def show_labels():
print("Distinct values each label takes, as the traffic grows:")
print(" label declared 200 requests 1000 requests")
for l in labels.labels():
a = labels.distinct(200, l)
b = labels.distinct(1000, l)
d = "-"
if declared(l) > 0:
d = str(declared(l))
note = ""
if b > a:
note = " grows with the traffic"
print(" " + pad(l, 9) + lpad(d, 10) + lpad(str(a), 15) + lpad(str(b), 16) + note)
print("path holds the ids written into it; endpoint is the path with every number replaced by :id.")
def labellings():
return [["method", "status"], ["method", "status", "endpoint"], ["method", "status", "path"], ["method", "status", "endpoint", "user"]]
def names(ls):
out = ""
for l in ls:
if out != "":
out = out + " "
out = out + l
return out
def predicted(ls):
p = 1
for l in ls:
if declared(l) == 0:
return 0 - 1
p = p * declared(l)
return p
def show_series():
print("Series to store, one per combination of label values seen, at 1000 requests:")
print(" labels declared seen")
for ls in labellings():
p = predicted(ls)
d = "unbounded"
if p >= 0:
d = str(p)
print(" " + pad(names(ls), 28) + lpad(d, 10) + lpad(str(labels.series(1000, ls)), 7))
print("Declared counts multiply every value with every other, so they are upper bounds when every value is bounded; a label carrying ids has no bound at all.")
def show_storage():
print("Numbers kept per series: a counter is 1; a histogram of 8 buckets, with its sum and count, is 10.")
print(" labels series counters histograms")
for ls in labellings():
n = labels.series(1000, ls)
print(" " + pad(names(ls), 28) + lpad(str(n), 8) + lpad(str(n), 11) + lpad(str(10 * n), 13))
print("Only a histogram can answer a percentile later; it costs ten numbers a series, and every series is kept for the whole retention.")
def own():
print("Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.")
t = plain(input("labels> "))
if t == "":
print("Cancelled.")
return
ls = []
w = ""
for c in t + " ":
if c == " ":
if w != "":
ok = False
for l in labels.labels():
if l == w:
ok = True
if not ok:
print("Type labels from method, status, path, endpoint and user.")
return
for x in ls:
if x == w:
print("Name each label once.")
return
ls = ls + [w]
w = ""
else:
w = w + c
a = labels.series(200, ls)
b = labels.series(1000, ls)
p = predicted(ls)
d = "unbounded"
if p >= 0:
d = str(p)
print(names(ls) + ": " + str(a) + " series at 200 requests, " + str(b) + " at 1000; declared " + d + ".")
growing = []
for l in ls:
if labels.distinct(1000, l) > labels.distinct(200, l):
growing = growing + [l]
if len(growing) > 0:
print(" " + names(growing) + " kept taking new values as the traffic grew: these series have no bound.")
elif b > a:
print(" No label here took a new value; the extra series are combinations of values already seen, filling in.")
print("== Metric labels ==")
print("Requests counted into time series, one series for each combination of label values.")
running = True
while running:
print("")
print("1) requests 2) labels 3) series 4) storage 5) your own labels 6) quit")
choice = plain(input("choice> "))
if choice == "1":
show_requests()
elif choice == "2":
show_labels()
elif choice == "3":
show_series()
elif choice == "4":
show_storage()
elif choice == "5":
own()
elif choice == "6":
running = False
else:
print("Pick a number from 1 to 6.")
print("Bye.")
labels.eml
eml# P133 metric labels - a request log counted into time series, one series
# per distinct combination of label values. As in the corpus case
# metric-cardinality-explosion, a label that looks bounded can carry an id:
# /users/17/orders and /users/99/orders are two series to the metrics
# system and one endpoint to a person. Which labels grow is measured on the
# data, at two volumes of traffic. As in counter-vs-histogram, each series
# costs a counter, or a whole histogram.
#
# A request is [method, status, path, user].
def mix(i, salt):
# a number from 0 to 999999, the same for the same request and salt;
# squaring modulo a prime, so nearby requests do not move together
i * 7919 + salt * 104729 + 12345 => x
for k in [1:3]:
(x * x + 7) % 1000000007 => x
return x % 1000000
def request(i):
["GET", "GET", "GET", "POST", "PUT", "DELETE"] => methods
[200, 200, 200, 200, 201, 404, 500, 503] => statuses
methods[mix(i, 1) % 6] => m
statuses[mix(i, 2) % 8] => s
# users grow with the traffic: request i comes from one of the first
# i / 4 + 10 users
1 + mix(i, 3) % (int(i / 4) + 10) => user
mix(i, 4) % 5 => k
if k == 0:
"/users/" + str(user) + "/orders" => p
elif k == 1:
"/users/" + str(user) => p
elif k == 2:
"/orders/" + str(1000 + mix(i, 5) % (i + 50)) => p
elif k == 3:
"/health" => p
else:
"/search" => p
return [m, s, p, user]
def template(path):
# every all-digit segment of the path replaced by :id
"" => out
"" => seg
for c in path + "/":
if c == "/":
if seg != "":
True => digits
for d in seg:
if d < "0" or d > "9":
False => digits
if digits:
out + "/:id" => out
else:
out + "/" + seg => out
"" => seg
else:
seg + c => seg
if out == "":
return "/"
return out
def value(r, label):
if label == "method":
return r[0]
if label == "status":
return str(r[1])
if label == "path":
return r[2]
if label == "endpoint":
return template(r[2])
return str(r[3])
def labels():
return ["method", "status", "path", "endpoint", "user"]
def series(n, chosen):
# the distinct combinations of the chosen labels over requests 1 to n
{} => seen
for i in [1:n]:
request(i) => r
"" => key
for l in chosen:
key + "|" + value(r, l) => key
1 => seen[key]
return len(seen)
def distinct(n, label):
{} => seen
for i in [1:n]:
1 => seen[value(request(i), label)]
return len(seen)
Python projection (labels.py)
def mix(i, salt):
x = i * 7919 + salt * 104729 + 12345
for k in range(1, 4):
x = (x * x + 7) % 1000000007
return x % 1000000
def request(i):
methods = ["GET", "GET", "GET", "POST", "PUT", "DELETE"]
statuses = [200, 200, 200, 200, 201, 404, 500, 503]
m = methods[mix(i, 1) % 6]
s = statuses[mix(i, 2) % 8]
user = 1 + mix(i, 3) % (int(i / 4) + 10)
k = mix(i, 4) % 5
if k == 0:
p = "/users/" + str(user) + "/orders"
elif k == 1:
p = "/users/" + str(user)
elif k == 2:
p = "/orders/" + str(1000 + mix(i, 5) % (i + 50))
elif k == 3:
p = "/health"
else:
p = "/search"
return [m, s, p, user]
def template(path):
out = ""
seg = ""
for c in path + "/":
if c == "/":
if seg != "":
digits = True
for d in seg:
if d < "0" or d > "9":
digits = False
if digits:
out = out + "/:id"
else:
out = out + "/" + seg
seg = ""
else:
seg = seg + c
if out == "":
return "/"
return out
def value(r, label):
if label == "method":
return r[0]
if label == "status":
return str(r[1])
if label == "path":
return r[2]
if label == "endpoint":
return template(r[2])
return str(r[3])
def labels():
return ["method", "status", "path", "endpoint", "user"]
def series(n, chosen):
seen = {}
for i in range(1, n+1):
r = request(i)
key = ""
for l in chosen:
key = key + "|" + value(r, l)
seen[key] = 1
return len(seen)
def distinct(n, label):
seen = {}
for i in range(1, n+1):
seen[value(request(i), label)] = 1
return len(seen)