{
  "id": "P133",
  "slug": "metric-labels",
  "key": "P133-metric-labels",
  "title": "Metric labels",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P133 - Metric labels\n\nRequests counted into time series, one series for each combination of\nlabel values: method, status, path, endpoint (the path with its numbers\nreplaced by :id) and user.\n\n- `main.eml` - the menu, the requests, the values each label takes, the\n  series each labelling makes, what storing them costs, and a labelling\n  of your own\n- `labels.eml` - the request log, templating a path, a request's label\n  values, and the counts of series and of distinct values\n\nHow each part works:\n\n- The requests are generated the same every run; the users and the order\n  numbers grow with the traffic.\n- As in the corpus case `metric-cardinality-explosion`, path was declared\n  with 5 values, one per kind of path, and carries ids: 89 values at 200\n  requests and 402 at 1000. Which labels grow is measured on the data at\n  two volumes, not read from the declaration. Templating the path back to\n  its endpoint keeps it at 5.\n- At 1000 requests: method and status make 20 series, as declared; adding\n  endpoint makes 98 of the 100 declared; adding path instead makes 627.\n- When a labelling's series grow while none of its labels takes a new\n  value, the new series are combinations of values already seen, filling\n  in; they can never pass the product of those values.\n- As in the corpus case `counter-vs-histogram`, a counter is one number a\n  series and a histogram of 8 buckets with its sum and count is ten, but\n  only the histogram can answer a percentile later.\n\nWhat is checked: menu choices 1 to 6; labels from method, status, path,\nendpoint and user, each named once. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the requests, the labels, the series and the storage;\n- method endpoint, which stays at 20, and status user, which grows.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- labels: a cancel, method twice, colour;\n- path alone, which grows, and method status, which only fills in.\n\nBuilt on the verified corpus cases `metric-cardinality-explosion` (a label\nthat looked bounded until a path held an id) and `counter-vs-histogram`\n(a summary chosen at write time decides which questions it can answer).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P133 metric labels: the requests, the values each label takes as traffic\n# grows, the series each labelling makes, what storing them costs, and a\n# labelling of your own.\nimport labels\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef declared(label):\n    # the values the label was declared with; 0 for none\n    if label == \"method\":\n        return 4\n    if label == \"status\":\n        return 5\n    if label == \"path\" or label == \"endpoint\":\n        return 5\n    return 0\n\ndef show_requests():\n    \"The first 8 requests (method, status, path, user):\" ^0\n    for i in [1:8]:\n        labels.request(i) => r\n        (\"  \" + lpad(str(i), 2) + \"  \" + pad(r[0], 7) + pad(str(r[1]), 5) + pad(r[2], 18) + \"user \" + str(r[3])) ^0\n    \"Every path is one of /users/<id>/orders, /users/<id>, /orders/<id>, /health and /search.\" ^0\n\ndef show_labels():\n    \"Distinct values each label takes, as the traffic grows:\" ^0\n    \"  label      declared   200 requests   1000 requests\" ^0\n    for l in labels.labels():\n        labels.distinct(200, l) => a\n        labels.distinct(1000, l) => b\n        \"-\" => d\n        if declared(l) > 0:\n            str(declared(l)) => d\n        \"\" => note\n        if b > a:\n            \"   grows with the traffic\" => note\n        (\"  \" + pad(l, 9) + lpad(d, 10) + lpad(str(a), 15) + lpad(str(b), 16) + note) ^0\n    \"path holds the ids written into it; endpoint is the path with every number replaced by :id.\" ^0\n\ndef labellings():\n    return [[\"method\", \"status\"], [\"method\", \"status\", \"endpoint\"], [\"method\", \"status\", \"path\"], [\"method\", \"status\", \"endpoint\", \"user\"]]\n\ndef names(ls):\n    \"\" => out\n    for l in ls:\n        if out != \"\":\n            out + \" \" => out\n        out + l => out\n    return out\n\ndef predicted(ls):\n    1 => p\n    for l in ls:\n        if declared(l) == 0:\n            return 0 - 1\n        p * declared(l) => p\n    return p\n\ndef show_series():\n    \"Series to store, one per combination of label values seen, at 1000 requests:\" ^0\n    \"  labels                        declared   seen\" ^0\n    for ls in labellings():\n        predicted(ls) => p\n        \"unbounded\" => d\n        if p >= 0:\n            str(p) => d\n        (\"  \" + pad(names(ls), 28) + lpad(d, 10) + lpad(str(labels.series(1000, ls)), 7)) ^0\n    \"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\n\ndef show_storage():\n    \"Numbers kept per series: a counter is 1; a histogram of 8 buckets, with its sum and count, is 10.\" ^0\n    \"  labels                        series   counters   histograms\" ^0\n    for ls in labellings():\n        labels.series(1000, ls) => n\n        (\"  \" + pad(names(ls), 28) + lpad(str(n), 8) + lpad(str(n), 11) + lpad(str(10 * n), 13)) ^0\n    \"Only a histogram can answer a percentile later; it costs ten numbers a series, and every series is kept for the whole retention.\" ^0\n\ndef own():\n    \"Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.\" ^0\n    plain(input(\"labels> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    [] => ls\n    \"\" => w\n    for c in t + \" \":\n        if c == \" \":\n            if w != \"\":\n                False => ok\n                for l in labels.labels():\n                    if l == w:\n                        True => ok\n                if not ok:\n                    \"Type labels from method, status, path, endpoint and user.\" ^0\n                    return\n                for x in ls:\n                    if x == w:\n                        \"Name each label once.\" ^0\n                        return\n                ls + [w] => ls\n            \"\" => w\n        else:\n            w + c => w\n    labels.series(200, ls) => a\n    labels.series(1000, ls) => b\n    predicted(ls) => p\n    \"unbounded\" => d\n    if p >= 0:\n        str(p) => d\n    (names(ls) + \": \" + str(a) + \" series at 200 requests, \" + str(b) + \" at 1000; declared \" + d + \".\") ^0\n    [] => growing\n    for l in ls:\n        if labels.distinct(1000, l) > labels.distinct(200, l):\n            growing + [l] => growing\n    if len(growing) > 0:\n        (\"  \" + names(growing) + \" kept taking new values as the traffic grew: these series have no bound.\") ^0\n    elif b > a:\n        \"  No label here took a new value; the extra series are combinations of values already seen, filling in.\" ^0\n\n\"== Metric labels ==\" ^0\n\"Requests counted into time series, one series for each combination of label values.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_requests()\n    elif choice == \"2\":\n        show_labels()\n    elif choice == \"3\":\n        show_series()\n    elif choice == \"4\":\n        show_storage()\n    elif choice == \"5\":\n        own()\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import labels\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef declared(label):\n    if label == \"method\":\n        return 4\n    if label == \"status\":\n        return 5\n    if label == \"path\" or label == \"endpoint\":\n        return 5\n    return 0\n\ndef show_requests():\n    print(\"The first 8 requests (method, status, path, user):\")\n    for i in range(1, 9):\n        r = labels.request(i)\n        print(\"  \" + lpad(str(i), 2) + \"  \" + pad(r[0], 7) + pad(str(r[1]), 5) + pad(r[2], 18) + \"user \" + str(r[3]))\n    print(\"Every path is one of /users/<id>/orders, /users/<id>, /orders/<id>, /health and /search.\")\n\ndef show_labels():\n    print(\"Distinct values each label takes, as the traffic grows:\")\n    print(\"  label      declared   200 requests   1000 requests\")\n    for l in labels.labels():\n        a = labels.distinct(200, l)\n        b = labels.distinct(1000, l)\n        d = \"-\"\n        if declared(l) > 0:\n            d = str(declared(l))\n        note = \"\"\n        if b > a:\n            note = \"   grows with the traffic\"\n        print(\"  \" + pad(l, 9) + lpad(d, 10) + lpad(str(a), 15) + lpad(str(b), 16) + note)\n    print(\"path holds the ids written into it; endpoint is the path with every number replaced by :id.\")\n\ndef labellings():\n    return [[\"method\", \"status\"], [\"method\", \"status\", \"endpoint\"], [\"method\", \"status\", \"path\"], [\"method\", \"status\", \"endpoint\", \"user\"]]\n\ndef names(ls):\n    out = \"\"\n    for l in ls:\n        if out != \"\":\n            out = out + \" \"\n        out = out + l\n    return out\n\ndef predicted(ls):\n    p = 1\n    for l in ls:\n        if declared(l) == 0:\n            return 0 - 1\n        p = p * declared(l)\n    return p\n\ndef show_series():\n    print(\"Series to store, one per combination of label values seen, at 1000 requests:\")\n    print(\"  labels                        declared   seen\")\n    for ls in labellings():\n        p = predicted(ls)\n        d = \"unbounded\"\n        if p >= 0:\n            d = str(p)\n        print(\"  \" + pad(names(ls), 28) + lpad(d, 10) + lpad(str(labels.series(1000, ls)), 7))\n    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.\")\n\ndef show_storage():\n    print(\"Numbers kept per series: a counter is 1; a histogram of 8 buckets, with its sum and count, is 10.\")\n    print(\"  labels                        series   counters   histograms\")\n    for ls in labellings():\n        n = labels.series(1000, ls)\n        print(\"  \" + pad(names(ls), 28) + lpad(str(n), 8) + lpad(str(n), 11) + lpad(str(10 * n), 13))\n    print(\"Only a histogram can answer a percentile later; it costs ten numbers a series, and every series is kept for the whole retention.\")\n\ndef own():\n    print(\"Labels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.\")\n    t = plain(input(\"labels> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    ls = []\n    w = \"\"\n    for c in t + \" \":\n        if c == \" \":\n            if w != \"\":\n                ok = False\n                for l in labels.labels():\n                    if l == w:\n                        ok = True\n                if not ok:\n                    print(\"Type labels from method, status, path, endpoint and user.\")\n                    return\n                for x in ls:\n                    if x == w:\n                        print(\"Name each label once.\")\n                        return\n                ls = ls + [w]\n            w = \"\"\n        else:\n            w = w + c\n    a = labels.series(200, ls)\n    b = labels.series(1000, ls)\n    p = predicted(ls)\n    d = \"unbounded\"\n    if p >= 0:\n        d = str(p)\n    print(names(ls) + \": \" + str(a) + \" series at 200 requests, \" + str(b) + \" at 1000; declared \" + d + \".\")\n    growing = []\n    for l in ls:\n        if labels.distinct(1000, l) > labels.distinct(200, l):\n            growing = growing + [l]\n    if len(growing) > 0:\n        print(\"  \" + names(growing) + \" kept taking new values as the traffic grew: these series have no bound.\")\n    elif b > a:\n        print(\"  No label here took a new value; the extra series are combinations of values already seen, filling in.\")\n\nprint(\"== Metric labels ==\")\nprint(\"Requests counted into time series, one series for each combination of label values.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_requests()\n    elif choice == \"2\":\n        show_labels()\n    elif choice == \"3\":\n        show_series()\n    elif choice == \"4\":\n        show_storage()\n    elif choice == \"5\":\n        own()\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "labels.eml",
      "eml": "# P133 metric labels - a request log counted into time series, one series\n# per distinct combination of label values. As in the corpus case\n# metric-cardinality-explosion, a label that looks bounded can carry an id:\n# /users/17/orders and /users/99/orders are two series to the metrics\n# system and one endpoint to a person. Which labels grow is measured on the\n# data, at two volumes of traffic. As in counter-vs-histogram, each series\n# costs a counter, or a whole histogram.\n#\n# A request is [method, status, path, user].\n\ndef mix(i, salt):\n    # a number from 0 to 999999, the same for the same request and salt;\n    # squaring modulo a prime, so nearby requests do not move together\n    i * 7919 + salt * 104729 + 12345 => x\n    for k in [1:3]:\n        (x * x + 7) % 1000000007 => x\n    return x % 1000000\n\ndef request(i):\n    [\"GET\", \"GET\", \"GET\", \"POST\", \"PUT\", \"DELETE\"] => methods\n    [200, 200, 200, 200, 201, 404, 500, 503] => statuses\n    methods[mix(i, 1) % 6] => m\n    statuses[mix(i, 2) % 8] => s\n    # users grow with the traffic: request i comes from one of the first\n    # i / 4 + 10 users\n    1 + mix(i, 3) % (int(i / 4) + 10) => user\n    mix(i, 4) % 5 => k\n    if k == 0:\n        \"/users/\" + str(user) + \"/orders\" => p\n    elif k == 1:\n        \"/users/\" + str(user) => p\n    elif k == 2:\n        \"/orders/\" + str(1000 + mix(i, 5) % (i + 50)) => p\n    elif k == 3:\n        \"/health\" => p\n    else:\n        \"/search\" => p\n    return [m, s, p, user]\n\ndef template(path):\n    # every all-digit segment of the path replaced by :id\n    \"\" => out\n    \"\" => seg\n    for c in path + \"/\":\n        if c == \"/\":\n            if seg != \"\":\n                True => digits\n                for d in seg:\n                    if d < \"0\" or d > \"9\":\n                        False => digits\n                if digits:\n                    out + \"/:id\" => out\n                else:\n                    out + \"/\" + seg => out\n            \"\" => seg\n        else:\n            seg + c => seg\n    if out == \"\":\n        return \"/\"\n    return out\n\ndef value(r, label):\n    if label == \"method\":\n        return r[0]\n    if label == \"status\":\n        return str(r[1])\n    if label == \"path\":\n        return r[2]\n    if label == \"endpoint\":\n        return template(r[2])\n    return str(r[3])\n\ndef labels():\n    return [\"method\", \"status\", \"path\", \"endpoint\", \"user\"]\n\ndef series(n, chosen):\n    # the distinct combinations of the chosen labels over requests 1 to n\n    {} => seen\n    for i in [1:n]:\n        request(i) => r\n        \"\" => key\n        for l in chosen:\n            key + \"|\" + value(r, l) => key\n        1 => seen[key]\n    return len(seen)\n\ndef distinct(n, label):\n    {} => seen\n    for i in [1:n]:\n        1 => seen[value(request(i), label)]\n    return len(seen)\n",
      "python": "def mix(i, salt):\n    x = i * 7919 + salt * 104729 + 12345\n    for k in range(1, 4):\n        x = (x * x + 7) % 1000000007\n    return x % 1000000\n\ndef request(i):\n    methods = [\"GET\", \"GET\", \"GET\", \"POST\", \"PUT\", \"DELETE\"]\n    statuses = [200, 200, 200, 200, 201, 404, 500, 503]\n    m = methods[mix(i, 1) % 6]\n    s = statuses[mix(i, 2) % 8]\n    user = 1 + mix(i, 3) % (int(i / 4) + 10)\n    k = mix(i, 4) % 5\n    if k == 0:\n        p = \"/users/\" + str(user) + \"/orders\"\n    elif k == 1:\n        p = \"/users/\" + str(user)\n    elif k == 2:\n        p = \"/orders/\" + str(1000 + mix(i, 5) % (i + 50))\n    elif k == 3:\n        p = \"/health\"\n    else:\n        p = \"/search\"\n    return [m, s, p, user]\n\ndef template(path):\n    out = \"\"\n    seg = \"\"\n    for c in path + \"/\":\n        if c == \"/\":\n            if seg != \"\":\n                digits = True\n                for d in seg:\n                    if d < \"0\" or d > \"9\":\n                        digits = False\n                if digits:\n                    out = out + \"/:id\"\n                else:\n                    out = out + \"/\" + seg\n            seg = \"\"\n        else:\n            seg = seg + c\n    if out == \"\":\n        return \"/\"\n    return out\n\ndef value(r, label):\n    if label == \"method\":\n        return r[0]\n    if label == \"status\":\n        return str(r[1])\n    if label == \"path\":\n        return r[2]\n    if label == \"endpoint\":\n        return template(r[2])\n    return str(r[3])\n\ndef labels():\n    return [\"method\", \"status\", \"path\", \"endpoint\", \"user\"]\n\ndef series(n, chosen):\n    seen = {}\n    for i in range(1, n+1):\n        r = request(i)\n        key = \"\"\n        for l in chosen:\n            key = key + \"|\" + value(r, l)\n        seen[key] = 1\n    return len(seen)\n\ndef distinct(n, label):\n    seen = {}\n    for i in range(1, n+1):\n        seen[value(request(i), label)] = 1\n    return len(seen)\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n5\n\n5\nmethod method\n5\nmethod colour\n5\npath\n5\nmethod status\n6\n",
      "screen": "== Metric labels ==\nRequests counted into time series, one series for each combination of label values.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> x\nPick a number from 1 to 6.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 5\nLabels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.\nlabels> \nCancelled.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 5\nLabels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.\nlabels> method method\nName each label once.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 5\nLabels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.\nlabels> method colour\nType labels from method, status, path, endpoint and user.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 5\nLabels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.\nlabels> path\npath: 89 series at 200 requests, 402 at 1000; declared 5.\n  path kept taking new values as the traffic grew: these series have no bound.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 5\nLabels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.\nlabels> method status\nmethod status: 19 series at 200 requests, 20 at 1000; declared 20.\n  No label here took a new value; the extra series are combinations of values already seen, filling in.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\n5\nmethod endpoint\n5\nstatus user\n6\n",
      "screen": "== Metric labels ==\nRequests counted into time series, one series for each combination of label values.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 1\nThe first 8 requests (method, status, path, user):\n   1  POST   200  /health           user 8\n   2  GET    500  /orders/1034      user 6\n   3  PUT    200  /orders/1033      user 6\n   4  GET    200  /search           user 1\n   5  GET    200  /users/6/orders   user 6\n   6  GET    201  /health           user 4\n   7  POST   200  /orders/1056      user 10\n   8  PUT    404  /orders/1006      user 12\nEvery path is one of /users/<id>/orders, /users/<id>, /orders/<id>, /health and /search.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 2\nDistinct values each label takes, as the traffic grows:\n  label      declared   200 requests   1000 requests\n  method            4              4               4\n  status            5              5               5\n  path              5             89             402   grows with the traffic\n  endpoint          5              5               5\n  user              -             47             204   grows with the traffic\npath holds the ids written into it; endpoint is the path with every number replaced by :id.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 3\nSeries to store, one per combination of label values seen, at 1000 requests:\n  labels                        declared   seen\n  method status                       20     20\n  method status endpoint             100     98\n  method status path                 100    627\n  method status endpoint user  unbounded    944\nDeclared 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.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 4\nNumbers kept per series: a counter is 1; a histogram of 8 buckets, with its sum and count, is 10.\n  labels                        series   counters   histograms\n  method status                     20         20          200\n  method status endpoint            98         98          980\n  method status path               627        627         6270\n  method status endpoint user      944        944         9440\nOnly a histogram can answer a percentile later; it costs ten numbers a series, and every series is kept for the whole retention.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 5\nLabels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.\nlabels> method endpoint\nmethod endpoint: 20 series at 200 requests, 20 at 1000; declared 20.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 5\nLabels from method, status, path, endpoint and user, separated by spaces, such as method endpoint.\nlabels> status user\nstatus user: 106 series at 200 requests, 501 at 1000; declared unbounded.\n  user kept taking new values as the traffic grew: these series have no bound.\n\n1) requests  2) labels  3) series  4) storage  5) your own labels  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "metric-cardinality-explosion",
      "caseId": "241-metric-cardinality-explosion",
      "title": "A label that looked like it had five values"
    },
    {
      "slug": "counter-vs-histogram",
      "caseId": "235-counter-vs-histogram",
      "title": "The percentile you threw away at write time"
    }
  ],
  "updated": "2026-10-17"
}
