Medir o percurso completo de uma alteração
Num projeto cloud, a equipa pode trabalhar depressa e entregar devagar. Para descobrir a diferença, acompanha um pedido desde a aceitação até à entrega e regista execução, espera, retrabalho e responsáveis. No exemplo desta aula, quatro horas de trabalho estão dentro de um percurso de quarenta horas. Reduzir a execução para duas horas deixa trinta e oito horas no total, porque as trinta e seis horas de espera continuam. A melhoria é real, mas o relatório deve mostrar o seu efeito de cinco por cento no prazo completo. Faz este mapa com desenvolvimento, QA, segurança, plataforma e APS, porque uma única equipa pode não ver as filas que existem antes e depois da sua intervenção. Escolhe uma espera observável, identifica quem pode alterá-la e define como medir o resultado. Evita começar pela compra de uma ferramenta quando o bloqueio é uma regra, uma dependência ou a falta de um responsável.
Desbloquear o trabalho antes de aumentar a fila
Uma coluna de teste com quatro itens bloqueados não ganha capacidade quando recebe mais seis. Mantém o trabalho inacabado visível e orienta a colaboração para recuperar o ambiente, a informação ou a decisão em falta. Um limite de trabalho em curso ajuda a tornar a restrição explícita; não deve ser contornado fechando tickets incompletos. Para alterações repetitivas de baixo risco, uma revisão do processo pode reduzir esperas desnecessárias. Prepara um piloto autorizado com revisão por pares, evidência dos testes, rastreabilidade e critérios de escalamento para mudanças de maior risco. Define duração, âmbito, responsável e condições de suspensão do piloto. Depois compara os tempos e a estabilidade com uma referência adequada. O resultado pretendido é concluir alterações úteis com controlo verificável. Um aumento do número de itens iniciados, ou a remoção de uma aprovação sem autorização, não demonstra essa melhoria perante o negócio nem perante RUN.
Escolher a unidade certa para cada métrica
Trinta deployments e três falhas que exigiram intervenção correspondem a uma taxa de dez por cento. Se cada falha gerar quatro alertas, continuam a existir três deployments afetados, não doze. Conserva a ligação entre os sinais e o evento que a métrica conta. Da mesma forma, a recuperação de deployments falhados deve ser distinguida da recuperação de incidentes sem relação com uma alteração. Ambas importam, mas respondem a perguntas diferentes. Compara resultados ao longo do tempo dentro do contexto de cada serviço, evitando transformar uma frequência isolada num ranking que incentiva mudanças artificiais. A documentação também precisa de uma medida útil. No ensaio do runbook, observa se uma pessoa de RUN consegue encontrar a sequência, cumprir os pré-requisitos e executar a tarefa sem depender do autor. A contagem de páginas descreve volume; o desempenho nessa tarefa dá evidência sobre a autonomia que o handover pretende entregar.
Definir o âmbito antes de somar custos
Um relatório de utilização e um relatório de fatura podem distribuir a mesma linha por meses diferentes. Decide primeiro qual pergunta o relatório responde. Para reconciliar a fatura, conserva invoice.month; para investigar consumo, conserva o instante ou período de utilização. Não dupliques a cobrança para representar as duas perspetivas. Mantém também a moeda na chave de agrupamento. Cem EUR e cento e vinte USD não formam duzentos e vinte de uma moeda escolhida pela ordem das linhas. Qualquer conversão precisa de uma regra explícita e da referência usada. No exercício local, os campos invoice_month e usage_month são rótulos de entrada simplificados, e o ID é criado para o exercício. Não são uma promessa de um identificador único ou desse esquema na exportação real. Antes de automatizar um relatório de produção, confirma a granularidade e as regras de identificação dos dados disponíveis, com FINOPS e os responsáveis pela fonte.
Exercício: preservar uma linha antes de expandir os créditos
O conjunto original contém L1 com custo 120 e créditos -12 e -8, e L2 com custo 80 sem créditos. A conta correta é 120 - 12 - 8 + 80 = 180. Um CROSS JOIN que expande os créditos produz duas cópias do custo de L1 e nenhuma linha para L2, chegando a 220. Antes de executar o laboratório, escreve as linhas intermédias e explica cada parcela. Corre python3 content/labs/pca-billing-grain/run.py para comparar a agregação por linha com duas transformações incorretas. A implementação usa SQLite em memória e inteiros em milionésimos, dentro de limites explícitos; não executa GoogleSQL nem contacta Cloud Billing. O código valida o esquema sintético, os IDs, os rótulos de mês e a precisão das quantias. Os resultados demonstram a aritmética e a cardinalidade desse modelo. Não demonstram que uma exportação está completa, que uma fatura está liquidada ou que as regras de impostos foram aplicadas.
Investigar um total certo obtido pelo caminho errado
No caso do comité, altera L2 para custo 120. A conta manual passa a 220 e a query errada também devolve 220. O custo repetido de L1 e o custo perdido de L2 têm agora o mesmo valor e compensam-se. Esta amostra não valida a transformação. Volta a usar L2=80, acrescenta linhas sem créditos e experimenta duas linhas legítimas com o mesmo custo. A correção SUM(DISTINCT cost) também falha: elimina valores iguais sem respeitar a identidade das linhas. Com dois custos de 100 e créditos totais de -20, devolve 80 em vez de 180. Uma reconciliação útil verifica contributos por linha, moedas, períodos e totais, incluindo conjuntos que possam revelar erros diferentes. Para avançar com o comité, pode ser usado temporariamente um relatório reconciliado com âmbito e data de corte claros, enquanto a transformação automática é corrigida e ensaiada novamente.
Relacionar a despesa com resultados e hipóteses
Quando o negócio pede custo por relatório aceite, usa relatórios distintos aceites como denominador. Num dia com custo 600, mil e duzentos pedidos HTTP e trezentos relatórios aceites, o KPI vale duas unidades por relatório. Dividir pelos pedidos daria outra medida e os retries poderiam melhorar artificialmente esse indicador. Define numerador, denominador, período e critérios de qualidade antes de comparar tendências. No orçamento da transição, separa custos recorrentes, sobreposição e pagamentos únicos. O exemplo de seis meses inclui 24000 da plataforma nova, 6000 da antiga nos primeiros dois meses e 10000 de migração: total 40000. São hipóteses fictícias, não preços de fornecedor nem prova de retorno. Se o encerramento antigo atrasar, atualiza o período de sobreposição e a previsão. Mantém visíveis as hipóteses que ainda dependem de validação, para que o sponsor consiga decidir sobre custo, calendário e benefício com o mesmo âmbito.
Apresentar decisões que podem ser verificadas
A escolha de um serviço gerido deve considerar também a saída futura. Um protocolo conhecido não prova compatibilidade de extensões nem recuperação noutro destino. Prepara um ensaio com as funcionalidades realmente usadas e uma estimativa de adaptações, transferência, operação e competências. Aplica o mesmo cuidado às afirmações de sustentabilidade. Para o mesmo mês, 120 kgCO2e location-based e 80 kgCO2e market-based são duas bases de cálculo; a diferença não prova uma redução causada pelo projeto. Compara períodos e âmbitos equivalentes, mantendo a metodologia e as revisões identificadas. No comité, uma mensagem útil é: “The sample total matches, but two transformation errors cancel out. We will use the reconciled report while correcting and retesting the automated calculation.” Resume cada decisão com requisito, observação, limitação, ação, responsável e data. Assim, FINOPS, APS e o sponsor podem verificar o progresso sem depender de uma métrica isolada ou de um total aparentemente favorável.
"""Original finite billing-grain exercise. SQLite in memory, never BigQuery.
Input IDs are synthetic exercise IDs, not an asserted Cloud Billing export key.
Amount strings have <=8 integer digits and <=6 fractional digits; no rounding.
At most 1000 rows and 10 credits per row keep integer SQL sums within int64.
This model excludes tax logic, FX, export completeness and invoice settlement.
"""
import copy
import itertools
import json
import re
import sqlite3
FIELDS = {'id', 'currency', 'invoice_month', 'usage_month', 'cost', 'credits'}
def micros(value):
if not isinstance(value, str) or not re.fullmatch(r'-?\d{1,8}(?:\.\d{1,6})?', value, flags=re.ASCII):
raise ValueError('amount must be a bounded decimal string')
sign = -1 if value.startswith('-') else 1
whole, _, fraction = value.lstrip('-').partition('.')
return sign * (int(whole) * 1000000 + int(fraction.ljust(6, '0') or '0'))
def money(value):
sign = '-' if value < 0 else ''
whole, fraction = divmod(abs(value), 1000000)
return f'{sign}{whole}.{fraction:06d}'
def validate(rows):
if not isinstance(rows, list) or len(rows) > 1000:
raise ValueError('expected at most 1000 rows')
seen = set()
for row in rows:
if not isinstance(row, dict) or set(row) != FIELDS:
raise ValueError('unexpected row fields')
if not isinstance(row['id'], str) or not row['id'] or row['id'] in seen:
raise ValueError('exercise IDs must be nonempty and unique')
seen.add(row['id'])
if not isinstance(row['currency'], str) or not re.fullmatch(r'[A-Z]{3}', row['currency']):
raise ValueError('currency requires three uppercase letters; no registry validation')
for field in ['invoice_month', 'usage_month']:
value = row[field]
if not isinstance(value, str) or not re.fullmatch(r'20\d{2}(0[1-9]|1[0-2])', value, flags=re.ASCII):
raise ValueError('exercise month must be YYYYMM within 2000–2099')
micros(row['cost'])
if not isinstance(row['credits'], list) or len(row['credits']) > 10:
raise ValueError('expected at most 10 credits per row')
for credit in row['credits']:
micros(credit)
def totals(rows, mode='per-row'):
validate(rows)
if mode not in {'per-row', 'incorrect-flat', 'incorrect-distinct'}:
raise ValueError('unknown demonstration mode')
db = sqlite3.connect(':memory:')
try:
db.executescript('''
CREATE TABLE lines(id TEXT PRIMARY KEY, currency TEXT, invoice_month TEXT,
usage_month TEXT, cost INTEGER);
CREATE TABLE credits(line_id TEXT, amount INTEGER);
''')
for r in rows:
db.execute('INSERT INTO lines VALUES (?,?,?,?,?)',
(r['id'], r['currency'], r['invoice_month'], r['usage_month'], micros(r['cost'])))
db.executemany('INSERT INTO credits VALUES (?,?)', [(r['id'], micros(c)) for c in r['credits']])
if mode == 'per-row':
sql = '''SELECT l.currency, l.invoice_month,
SUM(l.cost + COALESCE((SELECT SUM(c.amount)
FROM credits c WHERE c.line_id=l.id),0))
FROM lines l GROUP BY l.currency,l.invoice_month
ORDER BY l.currency,l.invoice_month'''
else:
cost = 'SUM(l.cost)' if mode == 'incorrect-flat' else 'SUM(DISTINCT l.cost)'
sql = f'''SELECT l.currency,l.invoice_month,{cost}+SUM(c.amount)
FROM lines l JOIN credits c ON c.line_id=l.id
GROUP BY l.currency,l.invoice_month ORDER BY l.currency,l.invoice_month'''
return [dict(currency=c, invoice_month=m, net=money(n)) for c, m, n in db.execute(sql)]
finally:
db.close()
def row(identity, cost='120', credits=None, currency='EUR', invoice_month='202610', usage_month='202609'):
return dict(id=identity, currency=currency, invoice_month=invoice_month,
usage_month=usage_month, cost=cost, credits=[] if credits is None else credits)
def demo():
checks = []
def check(name, condition):
if not condition:
raise AssertionError(name)
checks.append(name)
def reject(name, rows):
try:
totals(rows)
except ValueError:
checks.append(name)
else:
raise AssertionError(name)
def value(rows, mode='per-row'):
return totals(rows, mode)[0]['net']
original = [row('A', credits=['-12', '-8']), row('B', '80')]
check('per-row total preserves 180', value(original) == '180.000000')
check('flattening yields wrong 220', value(original, 'incorrect-flat') == '220.000000')
cancel = [row('A', credits=['-12', '-8']), row('B', '120')]
check('offsetting errors hide behind 220', value(cancel) == value(cancel, 'incorrect-flat') == '220.000000')
equal = [row('A', '100', ['-6', '-4']), row('B', '100', ['-10'])]
check('equal legitimate costs both retained', value(equal) == '180.000000')
check('distinct-value repair loses a cost', value(equal, 'incorrect-distinct') == '80.000000')
check('row without credits retained', value([row('A', '80')]) == '80.000000')
check('inner join loses credit-free population', totals([row('A', '80')], 'incorrect-flat') == [])
separate = totals([row('A', '100'), row('B', '120', currency='USD')])
check('currencies stay separate', separate == [dict(currency='EUR', invoice_month='202610', net='100.000000'), dict(currency='USD', invoice_month='202610', net='120.000000')])
check('invoice axis retained despite usage month', totals([row('A')])[0]['invoice_month'] == '202610')
check('positive credit adjustment is not discarded', value([row('A', '90', ['10'])]) == '100.000000')
check('negative cost adjustment retained', value([row('A', '-15')]) == '-15.000000')
check('empty scope produces no totals', totals([]) == [])
check('micro-unit precision retained', value([row('A', '1.000001', ['-0.000001'])]) == '1.000000')
check('signed zero normalized', value([row('A', '-0')]) == '0.000000')
check('all input orders reconcile identically', all(totals(list(p)) == totals(original) for p in itertools.permutations(original)))
reject('non-list scope rejected', {})
bad = row('A');bad['extra'] = 1;reject('unknown fields rejected', [bad])
for name, amount in [('boolean amount rejected', True), ('float amount rejected', 1.2), ('scientific notation rejected', '1e2'), ('nonfinite amount rejected', 'NaN'), ('excess precision rejected', '1.0000001'), ('overbound amount rejected', '100000000')]:
reject(name, [row('A', amount)])
reject('duplicate synthetic ID rejected', [row('A'), row('A')])
reject('invalid currency syntax rejected', [row('A', currency='eur')])
reject('invalid invoice month rejected', [row('A', invoice_month='202613')])
reject('invalid usage month rejected', [row('A', usage_month='202600')])
reject('empty ID rejected', [row('')])
bad = row('A');bad['credits'] = None;reject('null credit collection rejected', [bad])
bad = row('A');bad['credits'] = '-10';reject('string credit collection rejected', [bad])
reject('overbound row count rejected', [row(str(i)) for i in range(1001)])
reject('overbound credit count rejected', [row('A', credits=['-1'] * 11)])
snapshot = copy.deepcopy(original);totals(original);check('input is not mutated', original == snapshot)
return dict(passed=len(checks), checks=checks, correct=totals(original), incorrect=totals(original, 'incorrect-flat'),
cancellation=totals(cancel), currencies=separate, network=False, vendorExecution=False, persistentWrites=False,
runtime='SQLite in-memory relational exercise; no GoogleSQL engine or live invoice validated')
if __name__ == '__main__':
print(json.dumps(demo(), indent=2))
Uma query duplica um custo de 120 e perde outro custo de 120: o total coincide com a referência, mas a transformação falha quando o segundo valor muda para 80.
Armadilhas comuns
Medir só tempo ativo, esconder trabalho bloqueado, contar alertas como deployments, somar moedas, usar DISTINCT para reparar a granularidade e interpretar bases de emissões como estados antes/depois.
Tópicos relacionados: Migração, custos e critérios de aceitação · Reconciliação de dados e fronteiras temporais · Governação de mudanças e autonomia de RUN
Define a unidade e o âmbito, preserva os contributos originais e testa exemplos que possam contrariar a conclusão antes de a levar ao comité.
Referência: Visibility of work in the value stream · Current linked standard guide; edition date unconfirmed (2026-09-30 inspection)