Skip to content
academia.sh

Lesson 17 / 21

Tagging and Cost Allocation

Ownership information comes from outside the bill, and it does not come in full: in a fictional inventory of 148 resources, visible tag coverage is 77.7% while valid coverage is 75.0%, and 25.1% of the monthly 4,214.3 fictional units stays unallocatable, entering no rule at all. When the shared 1,584.9 units is split across three rules, the same team's bill swings between 8.7% and 18.6%. A tag mandate zeroes the unallocatable share but rejects 37 resources and stalls 14 legitimate jobs worth 511.9 units; even once tags reach 95.9% coverage by month's end, that month's 568.6 units (13.5%) never correct retroactively.

Published Updated

In this lesson

  • Measures tag coverage as two separate numbers: resources that show up tagged in the coverage report and resources counted valid in allocation
  • Distributes cost from the tag to teams and counts the direct, shared, and unallocatable shares separately
  • Counts the cost of a tag mandate: rejected resources, stalled legitimate work, and the tag's failure to reach the past bill
Contents

The previous lesson measured that the bill gives fourteen splits while the network requires ninety units of separation. That gap does not close from the bill; ownership information has to come from somewhere else. On the cloud side, that place is the resource itself: a tag is attached to a resource, and cost is distributed from that tag.

This lesson’s question is not how the distribution should be done, it is where it does not hold. Team-based accounting and the unsplittable item were measured in M22/K08; what is measured here is the tag, its input.

Tagged Resource Inventory

The example is the fictional regional measurement network: five teams, three environments, a rented machine, network, store, function, and edge layer. Resources, tags, and amounts are fictional; the currency is fictional too and is called unit, corresponding to no real price list.

CC7: the inventory holds 148 resources; eight types, three of which are used by more than one team and carry a shared tag. CC8: whether a resource has an owner tag depends on how it was created; there are four creation paths. CC9: a misspelled owner tag shows up tagged in the coverage report but has no counterpart in the team roster; it is invalid in allocation. CC10: a shared resource’s real driver is unknown — there is no true share to compare against in this lesson, what is measured is not deviation but rule-driven swing. CC11: a resource can carry at most ten tag keys; the network uses seven. CC12: a tag is completed on some day; bill lines cut before that day are untagged and never correct.

The inventory is a process-internal model derived from a generator; the seed is visible.

// cost/inventory.mjs — tagged resource inventory model: no real inventory. Resources are
// derived with a generator, the seed is visible, and the run repeats. The currency is fictional
// and is called `unit`. Whether a tag *exists* is decided by how the resource was created;
// a misspelled tag is a separate case: tagged in the coverage report, invalid in allocation.

export const SEED = 6108;
export const COUNT = 148;
export const TEAM = ["meter-reading", "validation", "billing", "field", "reporting"];
export const ENV = ["production", "staging", "development"];
export const TAG_LIMIT = 10;                       // tag keys per resource, at most
export const USED_KEY =
  ["owner", "environment", "service", "version", "temporary", "responsible", "cost-center"];

// type name, monthly unit lo-hi, shared (used by more than one team)
export const TYPE = [
  ["rented machine", 18, 96, false], ["block storage", 4, 34, false],
  ["object store", 6, 52, false], ["function", 1, 12, false],
  ["edge node", 3, 21, false], ["network gateway", 12, 40, true],
  ["shared message bus", 20, 64, true], ["monitoring collector", 9, 30, true],
];

// creation path: name, share, probability of an owner tag, probability the tag is spelled right
export const PATH = [
  ["template", 0.40, 1.00, 1.00], ["auto-scaling", 0.26, 0.71, 0.94],
  ["manual", 0.22, 0.55, 0.78], ["legacy migration", 0.12, 0.08, 0.60],
];

const pick = (table, u) => { let a = 0; for (const s of table) { a += s[1]; if (u < a) return s; }
  return table[table.length - 1]; };

// misspelling shapes: tagged in the coverage report, no counterpart in the team roster
const VARIANT = [(e) => e.toUpperCase(), (e) => e.replace(/-/g, ""), (e) => e + "-team"];

export const inventory = () => {
  let s = SEED >>> 0;
  const r = () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; };
  const list = [];
  for (let i = 0; i < COUNT; i++) {
    const [typeName, lo, hi, shared] = TYPE[Math.floor(r() * TYPE.length)];
    const [pathName, , pOwned, pCorrect] = pick(PATH, r());
    const env = ENV[Math.floor(r() * ENV.length)];
    const unit = Math.round((lo + r() * (hi - lo)) * 10) / 10;
    const team = TEAM[Math.floor(r() * TEAM.length)];
    let owner = null, valid = false;
    if (shared) { owner = "shared"; valid = true; }
    else if (r() < pOwned) {
      if (r() < pCorrect) { owner = team; valid = true; }
      else owner = VARIANT[Math.floor(r() * VARIANT.length)](team);
    }
    const day = 1 + Math.floor(r() * 34);               // day the tag is completed; >30 = never
    const urgent = pathName === "manual" && r() < 0.18;  // urgent repair job
    list.push({ id: i, type: typeName, shared, path: pathName, env, unit, owner, valid,
      fixDay: owner && valid ? 0 : day, urgent });
  }
  return list;
};

export const sum = (v) => v.reduce((a, b) => a + b, 0);
export const sumUnits = (v) => sum(v.map((k) => k.unit));
// cost/allocate.mjs — tag coverage, cost allocation to teams (three rules), the resources a tag
// mandate rejects, and tagging's failure to reach back into the past. What is measured is not
// whether the tag is correct, it is *where the allocation does not hold*.
import { inventory, TEAM, TYPE, PATH, TAG_LIMIT, USED_KEY, SEED,
  sum, sumUnits } from "./inventory.mjs";

const write = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const t0 = process.hrtime.bigint();

const R = inventory(), T = sumUnits(R), share = (x) => `${(100 * x / T).toFixed(1)}%`;
console.log(`seed ${SEED}: ${R.length} resources, monthly ${T.toFixed(1)} units (fictional currency)`);

console.log("\n1. tag coverage by creation path");
const A = [-20, 11, 10, 10, 10, 11, 12];
write(A, "creation path", "resources", "tagged", "valid", "invalid", "untagged", "unit share");
for (const [name] of PATH) {
  const g = R.filter((k) => k.path === name), e = g.filter((k) => k.owner);
  write(A, name, g.length, e.length, g.filter((k) => k.valid).length,
    e.filter((k) => !k.valid).length, g.filter((k) => !k.owner).length,
    share(sumUnits(g)));
}
const tagged = R.filter((k) => k.owner), valid = R.filter((k) => k.valid);
const invalid = R.filter((k) => k.owner && !k.valid), blank = R.filter((k) => !k.owner);
write(A, "total", R.length, tagged.length, valid.length, invalid.length, blank.length, "100%");
console.log(`  visible coverage ${(100 * tagged.length / R.length).toFixed(1)}%, ` +
  `valid coverage ${(100 * valid.length / R.length).toFixed(1)}%`);
console.log(`  invalid tag ${invalid.length} resources ${sumUnits(invalid).toFixed(1)} units ` +
  `(${share(sumUnits(invalid))}): shows up tagged in the coverage report`);
console.log(`  tag key limit ${TAG_LIMIT}, used ` +
  `${USED_KEY.length}, remaining ${TAG_LIMIT - USED_KEY.length}`);

console.log("\n2. allocation: direct, shared, unallocated");
const direct = R.filter((k) => k.valid && k.owner !== "shared");
const shared = R.filter((k) => k.owner === "shared");
const unallocated = R.filter((k) => !k.valid);
const D = sumUnits(direct), P = sumUnits(shared), X = sumUnits(unallocated);
const B = [-14, 11, 15, 9];
write(B, "bucket", "resources", "monthly units", "share");
write(B, "direct", direct.length, D.toFixed(1), share(D));
write(B, "shared", shared.length, P.toFixed(1), share(P));
write(B, "unallocated", unallocated.length, X.toFixed(1), share(X));
const teamUnit = TEAM.map((e) => sumUnits(direct.filter((k) => k.owner === e)));
const teamCount = TEAM.map((e) => direct.filter((k) => k.owner === e).length);
const ratio = (v) => { const t = sum(v); return v.map((x) => x / t); };
const RULE = { equal: TEAM.map(() => 1 / TEAM.length), cost: ratio(teamUnit),
  "resource count": ratio(teamCount) };
console.log(`  shared ${P.toFixed(1)} units split by a rule;`);
console.log(`  unallocated ${X.toFixed(1)} units (${share(X)}) enters no rule`);

console.log("\n3. three split rules: how much the same team's bill swings by rule");
const C = [-16, ...TEAM.map(() => 15)];
write(C, "rule", ...TEAM);
const result = {};
for (const [name, p] of Object.entries(RULE))
  result[name] = TEAM.map((_, i) => teamUnit[i] + P * p[i]);
write(C, "direct", ...teamUnit.map((x) => x.toFixed(1)));
for (const [name, v] of Object.entries(result)) write(C, name, ...v.map((x) => x.toFixed(1)));
const swing = TEAM.map((_, i) => {
  const v = Object.values(result).map((s) => s[i]);
  return [Math.min(...v), Math.max(...v)];
});
write(C, "widest gap", ...swing.map(([a, b]) => (b - a).toFixed(1)));
write(C, "as % of bill", ...swing.map(([a, b]) => `${(100 * (b - a) / a).toFixed(1)}%`));
const en = swing.map(([a, b]) => (b - a) / a);
const i0 = en.indexOf(Math.max(...en));
console.log(`  no real share exists to compare against: what is measured is not deviation, it is rule-driven swing`);
console.log(`  most swing ${TEAM[i0]}: ${(100 * en[i0]).toFixed(1)}%; ` +
  `unallocated ${share(X)} is off the bill under all three rules`);

console.log("\n4. tag mandate: how many resources are rejected, how many are legitimate work");
const rejected = R.filter((k) => !k.valid);
const E = [-20, 11, 15, 12, 15];
write(E, "creation path", "rejected", "monthly units", "legitimate", "stalled units");
let legitCount = 0, legitUnit = 0;
for (const [name] of PATH) {
  const g = rejected.filter((k) => k.path === name);
  const m = g.filter((k) => k.path === "auto-scaling" || k.urgent);
  legitCount += m.length; legitUnit += sumUnits(m);
  write(E, name, g.length, sumUnits(g).toFixed(1), m.length, sumUnits(m).toFixed(1));
}
write(E, "total", rejected.length, sumUnits(rejected).toFixed(1), legitCount, legitUnit.toFixed(1));
console.log(`  ${rejected.length} resources rejected (${(100 * rejected.length / R.length).toFixed(1)}%); ` +
  `${legitCount} of these are capacity or urgent repair work`);
console.log(`  the mandate zeroes the unallocated ${share(X)} share, in exchange ` +
  `${legitUnit.toFixed(1)} units' worth of work, ${legitCount} items, stalls`);

console.log("\n5. tagging added later: the past bill does not correct itself");
const DAYS = 30, MILESTONE = [1, 5, 10, 15, 20, 25, 30];
const F = [6, 14, 10, 16, 19];
write(F, "day", "tagged", "coverage", "units/day", "accumulated loss");
const dailyLoss = (g) => sumUnits(R.filter((k) => !k.valid && k.fixDay > g)) / DAYS;
let accumulated = 0;
for (let g = 1; g <= DAYS; g++) {
  accumulated += dailyLoss(g);
  if (!MILESTONE.includes(g)) continue;
  const closed = R.filter((k) => k.valid || k.fixDay <= g);
  write(F, g, closed.length, `${(100 * closed.length / R.length).toFixed(1)}%`,
    (sumUnits(closed) / DAYS).toFixed(2), accumulated.toFixed(1));
}
const never = R.filter((k) => !k.valid && k.fixDay > DAYS);
console.log(`  end-of-month coverage ${(100 * (R.length - never.length) / R.length).toFixed(1)}%, ` +
  `but the month's unallocated share is ${accumulated.toFixed(1)} units (${(100 * accumulated / T).toFixed(1)}%)`);
console.log(`  ${never.length} resources are never tagged: ${sumUnits(never).toFixed(1)} units/month ` +
  `stay unallocated every month`);

const ms = Number(process.hrtime.bigint() - t0) / 1e6;   // raw measurement is not printed, only the band is checked
console.log(`\nfive tables within the 2000 ms band: ${ms < 2000 ? "yes" : "no"}`);
seed 6108: 148 resources, monthly 4214.3 units (fictional currency)

1. tag coverage by creation path
creation path         resources    tagged     valid   invalid   untagged  unit share
template                     58        58        58         0          0       40.8%
auto-scaling                 40        31        29         2          9       30.6%
manual                       27        16        14         2         11       14.5%
legacy migration             23        10        10         0         13       14.1%
total                       148       115       111         4         33        100%
  visible coverage 77.7%, valid coverage 75.0%
  invalid tag 4 resources 123.6 units (2.9%): shows up tagged in the coverage report
  tag key limit 10, used 7, remaining 3

2. allocation: direct, shared, unallocated
bucket          resources  monthly units    share
direct                 58         1572.2    37.3%
shared                 53         1584.9    37.6%
unallocated            37         1057.2    25.1%
  shared 1584.9 units split by a rule;
  unallocated 1057.2 units (25.1%) enters no rule

3. three split rules: how much the same team's bill swings by rule
rule              meter-reading     validation        billing          field      reporting
direct                    286.6          250.3          267.8          309.8          457.7
equal                     603.6          567.3          584.8          626.8          774.7
cost                      575.5          502.6          537.8          622.1          919.1
resource count            641.8          550.9          568.4          555.7          840.3
widest gap                 66.3           64.7           47.0           71.0          144.4
as % of bill              11.5%          12.9%           8.7%          12.8%          18.6%
  no real share exists to compare against: what is measured is not deviation, it is rule-driven swing
  most swing reporting: 18.6%; unallocated 25.1% is off the bill under all three rules

4. tag mandate: how many resources are rejected, how many are legitimate work
creation path          rejected  monthly units  legitimate  stalled units
template                      0            0.0           0            0.0
auto-scaling                 11          444.3          11          444.3
manual                       13          222.8           3           67.6
legacy migration             13          390.1           0            0.0
total                        37         1057.2          14          511.9
  37 resources rejected (25.0%); 14 of these are capacity or urgent repair work
  the mandate zeroes the unallocated 25.1% share, in exchange 511.9 units' worth of work, 14 items, stalls

5. tagging added later: the past bill does not correct itself
   day        tagged  coverage       units/day   accumulated loss
     1           112     75.7%          106.85               33.6
     5           115     77.7%          108.90              161.3
    10           119     80.4%          112.15              306.2
    15           123     83.1%          115.44              434.3
    20           133     89.9%          129.34              513.4
    25           139     93.9%          136.04              552.4
    30           142     95.9%          137.50              568.6
  end-of-month coverage 95.9%, but the month's unallocated share is 568.6 units (13.5%)
  6 resources are never tagged: 89.3 units/month stay unallocated every month

five tables within the 2000 ms band: yes

The numbers belong to the measurement class; their inputs are the assumptions above.

Coverage Is Not One Number

The first table splits tag coverage by creation path and produces two different numbers. Visible coverage is 77.7%: 115 of 148 resources carry an owner tag. Valid coverage is 75.0%: on four of those, the tag value matches no team at all. Four resources, 123.6 units, 2.9% of the bill — clean in the coverage report, lost in allocation.

Where coverage comes from is written into the path itself. All 58 resources created from a template are tagged correctly; the tag is written by the template, not a person. Thirteen of the 23 resources from legacy migration carry no tag at all, and 27 manually created resources show both a missing tag (11) and a misspelling (2). Nine of the 40 resources from auto-scaling carry no tag: scaling that does not inherit the resource does not inherit the tag either.

The limit is also counted: a resource can carry ten tag keys, the network uses seven, three remain; adding one more split spends one of those three slots.

Three Rules, Three Different Bills

The second table splits allocation three ways: 1,572.2 units directly assigned (37.3%), 1,584.9 units shared (37.6%), and 1,057.2 units unallocated (25.1%). The unallocated share is not a splitting problem; the input is missing, and no rule accepts it.

The third table splits the shared amount three ways: equal share, the direct-cost ratio, and the resource-count ratio. All three read only from the tag. Unlike M22/K08, there is no real share here; what is measured is not deviation, it is the same team’s bill swinging by rule.

Reporting swings the most: 774.7 at equal share, 840.3 at resource count, 919.1 at cost ratio — a gap of 144.4 units, 18.6% of its bill. Billing swings least (8.7%). A team can watch its share swing by a sixth without changing anything about its own resources, purely because one rule changed centrally. The unallocated 25.1% shows up in none of the three rules; the teams’ totals never add up to the total bill.

The Work the Mandate Stalls

The fourth table applies the obvious fix: a resource with no valid owner tag cannot be created. The mandate zeroes the unallocated 25.1%, because those resources never exist.

Its cost sits in the same table. 37 resources are rejected, and 14 of them are legitimate work: 11 resources opened by auto-scaling are capacity, three of the manually opened ones are urgent repairs. The stalled work is 511.9 units. All of auto-scaling’s rejections are legitimate — the rule bites hardest exactly where no one is at a keyboard. The 13 legacy-migration rejections (390.1 units) stall no legitimate work at all: those resources already exist, and the mandate only reaches new ones.

Tags Do Not Reach Backward

The fifth table completes tags over the course of the month. Coverage climbs from 75.7% on day 1 to 95.9% on day 30; by month’s end the inventory report reads nearly clean. The month’s bill is not clean: the accumulated unallocated amount is 568.6 units, 13.5% of the month.

The reason is one sentence: bill lines are cut day by day, and a cut line carries whatever tag the resource wore that day. A resource tagged on day 20 leaves its first nineteen days unallocated. Some providers let a tag the resource already wore be activated retroactively for cost allocation; none writes a tag that was never attached back into the past. Coverage is instantaneous, the bill is continuous; putting both in the same report produces the picture of a month that has already been fixed. Six resources are never tagged at all: 89.3 units stay unallocated again every month.

Summary

  • In the fictional inventory of 148 resources, visible coverage is 77.7%, valid coverage is 75.0%; the four-resource gap between them (123.6 units, 2.9%) shows up tagged in the report and gets lost in allocation. Coverage depends on the creation path: all 58 template resources are tagged correctly, 13 of 23 legacy-migration resources carry no tag.
  • Of the monthly 4,214.3 units, 37.3% is directly assigned, 37.6% is shared, and 25.1% enters no rule at all; the teams’ totals never add up to the total bill.
  • Split across three rules, the shared share makes the same team’s bill swing between 8.7% and 18.6%; reporting’s bill sits in a 144.4-unit band, between 774.7 and 919.1.
  • A tag mandate zeroes the unallocated share but rejects 37 resources; 14 of them are legitimate work, and 511.9 units’ worth stalls. All 11 of auto-scaling’s rejections are capacity work.
  • Even once tags reach 95.9% coverage by month’s end, that month’s 568.6 units (13.5%) stay unallocated: coverage is instantaneous, the bill is continuous.

Next Step

Items were split and one attempt at ownership was made; both read the bill after it arrives. The next lesson moves the question onto the time axis: two alert types run over a spending curve, the day each is noticed is counted, and the bill’s own delay enters the table as a constraint — by the time an alert fires, how much of the spending has already happened.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close