Feature flag parece assunto de plataforma grande, mas a ideia é simples: separar deploy de ativação. Você coloca o código novo no binário, mas decide em outro lugar se ele deve rodar para todos, para ninguém, para uma porcentagem pequena ou apenas para um grupo controlado. Em Zig, isso combina muito bem com binários pequenos, configuração explícita e serviços que precisam ser previsíveis em produção.
O erro comum é ir para extremos. De um lado, fazer deploy direto e torcer para o rollback ser rápido. Do outro, criar uma plataforma interna de flags antes de ter volume, métricas ou equipe para operá-la. A abordagem mais saudável para muitos projetos Zig é começar com um conjunto pequeno de flags bem tipadas, carregadas na inicialização, observadas em logs e removidas depois que a mudança estabiliza.
Este guia mostra como pensar em feature flags em Zig sem peso desnecessário: tipos, configuração, rollout gradual, canary, kill switch, testes, métricas e limpeza. Ele complementa os textos sobre configuração segura com variáveis de ambiente, serviço Zig com systemd, Docker para Zig em produção, observabilidade em Zig e supply chain de dependências e releases.
Quando uma flag vale a pena
Nem toda mudança precisa de feature flag. Um ajuste de texto, uma correção local sem risco operacional ou uma melhoria interna com teste forte pode seguir o fluxo normal de deploy. Flags valem o custo quando a mudança altera comportamento em produção e você quer uma saída rápida sem recompilar.
Use flag quando a mudança envolve:
- novo parser ou algoritmo em caminho crítico;
- rota HTTP nova que pode receber tráfego real;
- integração com serviço externo instável;
- cache, fila, worker ou job recorrente;
- migração gradual de formato de dados;
- mudança de performance que precisa de comparação;
- funcionalidade que deve abrir primeiro para usuários internos.
Evite flag para esconder código inacabado por meses. Feature flag é ferramenta de entrega segura, não depósito permanente de decisões adiadas. Toda flag deveria nascer com dono, motivo, critério de remoção e plano de rollback.
Tipos de flags em um projeto Zig
O primeiro desenho é classificar a flag. Isso ajuda a decidir onde ela vive e como será testada.
| Tipo | Exemplo | Onde costuma viver |
|---|---|---|
| Release flag | ativar novo endpoint | configuração de ambiente |
| Ops flag | desligar worker pesado | arquivo local, env ou painel simples |
| Experiment flag | comparar algoritmo A/B | config + métrica de exposição |
| Permission flag | liberar cliente específico | banco, lista allowlist ou serviço externo |
| Build flag | incluir backend opcional | build.zig e comptime |
Em Zig, build flags são tentadoras porque o comptime é poderoso. Mas não confunda compile-time com runtime. Se você precisa mudar comportamento sem novo binário, a flag precisa estar em configuração carregada pelo processo, não em build.zig.
Use comptime para incluir ou remover capacidades inteiras do artefato, como backend experimental, suporte a tracing detalhado ou integração opcional. Use runtime config para ativação operacional.
Um modelo simples e tipado
Comece com uma struct pequena. Ela torna a configuração auditável e evita espalhar std.process.getEnvVarOwned pelo código inteiro.
const std = @import("std");
pub const FeatureFlags = struct {
novo_parser_csv: bool = false,
cache_respostas: bool = true,
rollout_percentual: u8 = 0,
pub fn fromEnv(allocator: std.mem.Allocator) !FeatureFlags {
_ = allocator;
return .{
.novo_parser_csv = envBool("ZIGBR_NOVO_PARSER_CSV", false),
.cache_respostas = envBool("ZIGBR_CACHE_RESPOSTAS", true),
.rollout_percentual = envPercent("ZIGBR_ROLLOUT_PERCENTUAL", 0),
};
}
};
fn envBool(name: []const u8, default: bool) bool {
const value = std.process.getEnvVarOwned(std.heap.page_allocator, name) catch return default;
defer std.heap.page_allocator.free(value);
return std.mem.eql(u8, value, "1") or
std.ascii.eqlIgnoreCase(value, "true") or
std.ascii.eqlIgnoreCase(value, "yes");
}
fn envPercent(name: []const u8, default: u8) u8 {
const value = std.process.getEnvVarOwned(std.heap.page_allocator, name) catch return default;
defer std.heap.page_allocator.free(value);
const parsed = std.fmt.parseInt(u8, value, 10) catch return default;
return @min(parsed, 100);
}
O exemplo acima é intencionalmente simples. Em um serviço maior, você provavelmente passaria o allocator corretamente para helpers e retornaria erros de configuração inválida em vez de usar default silencioso. Para ferramenta interna, default seguro pode ser suficiente. Para serviço crítico, falhe no boot quando uma variável está malformada.
O ponto principal é ter um objeto FeatureFlags explícito. Ele entra no contexto da aplicação junto com logger, banco, cache e dependências. Assim, code review consegue ver onde decisões de ativação acontecem.
Não coloque segredo dentro de flag
Feature flag não é secret manager. A flag pode decidir se uma integração está ativa, mas token, senha, chave privada e DSN sensível continuam em canal protegido. A diferença importa porque flags costumam aparecer em log de boot, endpoint de status, página interna, dump de configuração ou print de debug.
Uma configuração saudável separa:
ENABLE_BILLING_SYNC=true, que pode aparecer em log;BILLING_API_TOKEN, que nunca deve aparecer;BILLING_BASE_URL, que normalmente é pública para a operação;BILLING_TIMEOUT_MS, que é configuração técnica.
Essa fronteira conversa diretamente com o guia de configuração segura em Zig. Se a equipe ainda não consegue garantir que logs não vazam segredo, não adicione painel de flags com dump completo de config.
Rollout percentual sem estado global mágico
Rollout gradual precisa ser determinístico. Se 10% dos usuários entram no novo caminho, a mesma chave deveria continuar no mesmo grupo entre requisições. Sortear a cada chamada cria comportamento instável, difícil de debugar e ruim para métricas.
Um desenho simples é usar uma chave estável, como tenant_id, user_id, project_id ou nome do arquivo, calcular hash e comparar com o percentual.
pub fn enabledForKey(percent: u8, key: []const u8) bool {
if (percent == 0) return false;
if (percent >= 100) return true;
const hash = std.hash.Wyhash.hash(0, key);
const bucket: u8 = @intCast(hash % 100);
return bucket < percent;
}
Use uma chave que represente o risco. Para cache por cliente, tenant_id costuma ser melhor que request_id, porque mantém experiência consistente. Para processamento de arquivo, o caminho ou identificador do lote pode bastar. Para uma CLI local, talvez a flag seja por projeto, usando o diretório raiz como chave.
Documente a escolha. O próximo incidente será mais fácil se todo mundo souber que rollout_percentual=5 significa 5% de tenants, não 5% de requisições.
Canary, allowlist e kill switch
Três padrões resolvem a maior parte dos casos antes de qualquer plataforma sofisticada.
Canary ativa a mudança para um grupo pequeno e conhecido. Pode ser uma lista de tenants internos, um host específico ou uma região de tráfego controlada. O objetivo é ver comportamento real com impacto limitado.
Allowlist ativa por identidade. Em vez de porcentagem, você diz exatamente quem entra. Para produto B2B, isso ajuda quando um cliente pediu a funcionalidade e aceita validar primeiro. Para ferramenta interna, pode ser a equipe de infraestrutura.
Kill switch desliga rápido. Ele deve ser mais simples que o sistema que está protegendo. Se o cache novo derruba latência, ENABLE_NEW_CACHE=false precisa funcionar sem deploy complexo. Se o worker consome CPU demais, uma flag operacional deve impedir novos jobs enquanto os processos em andamento terminam.
O kill switch deve ter caminho de validação. Depois de desligar, confira log, métrica e uma requisição real. Em um serviço systemd, isso pode ser systemctl restart, journalctl -u app -f e uma chamada ao health check. Em container, pode ser troca de variável, restart controlado e verificação no endpoint vivo.
Observabilidade mínima para flags
Uma flag sem observabilidade vira superstição. Você acha que ativou 10%, mas não sabe se o caminho novo realmente executou, se erro aumentou ou se latência mudou.
O mínimo saudável:
- logar no boot quais flags públicas estão ativas;
- registrar contador de execução por caminho antigo e novo;
- medir erro e duração por caminho;
- incluir versão do binário e commit nos logs;
- ter endpoint ou comando administrativo que mostre flags não sensíveis;
- registrar evento quando a flag muda, se houver painel ou arquivo dinâmico.
Não use valor de usuário como label de métrica. Cardinalidade alta derruba observabilidade. Prefira labels pequenas, como feature="novo_parser_csv" e variant="on". Para detalhes por tenant, log amostrado e consulta pontual costumam ser melhores.
Se o projeto já usa OpenTelemetry, conecte a decisão de flag ao span ou atributo controlado. Se ainda não usa, um log estruturado por evento já ajuda muito. O artigo de OpenTelemetry em Zig aprofunda esse caminho.
Testes: matriz pequena e explícita
Flags duplicam caminhos. Se você não limitar, a matriz explode. A solução é escolher combinações relevantes:
- todos os defaults seguros;
- flag principal desligada;
- flag principal ligada;
- rollout 0%, 1%, 50% e 100%;
- allowlist com chave presente e ausente;
- kill switch ativo;
- configuração inválida.
Para função pura, teste diretamente. Para serviço, injete FeatureFlags no contexto em vez de ler variável de ambiente dentro da regra de negócio. Isso deixa teste simples:
test "novo parser só roda quando flag está ativa" {
const flags_off = FeatureFlags{ .novo_parser_csv = false };
const flags_on = FeatureFlags{ .novo_parser_csv = true };
try std.testing.expect(!deveUsarNovoParser(flags_off, "cliente-a"));
try std.testing.expect(deveUsarNovoParser(flags_on, "cliente-a"));
}
Para rollout percentual, teste determinismo: a mesma chave deve retornar o mesmo resultado enquanto o percentual não muda. Também teste limites. Bugs em 0 e 100 são pequenos, mas irritantes em produção.
Flags em CLI, worker e servidor
O formato muda conforme o tipo de aplicação.
Em CLI, prefira flag de linha de comando quando a decisão pertence à execução atual: --new-parser, --dry-run, --no-cache. Use env ou arquivo de configuração quando a decisão é persistente para o projeto.
Em worker, use configuração de ambiente para comportamento global e uma allowlist por fila, cliente ou tipo de job quando o rollout precisa ser gradual. Um worker deve logar a decisão antes de processar o job, especialmente se a flag muda o formato de saída.
Em servidor HTTP, não leia env a cada request. Carregue no boot ou use um mecanismo explícito de reload. Se precisar de mudança dinâmica, trate como subsistema real: atualização atômica, validação, log de mudança e fallback.
O modelo de Zig favorece essa clareza porque dependências são passadas explicitamente. Se uma função precisa saber de flags, isso aparece na assinatura ou no contexto da aplicação. Esse atrito é bom: impede que flags virem estado global invisível.
Rollback não substitui limpeza
Uma flag antiga é dívida. Depois que o caminho novo virou padrão, remova o caminho antigo, a variável, os testes duplicados, a documentação temporária e a métrica de comparação. Caso contrário, seis meses depois ninguém sabe se ENABLE_FAST_PATH ainda faz algo ou se pode quebrar cliente antigo.
Uma regra prática:
- flag de release deve viver dias ou poucas semanas;
- flag operacional pode viver mais, mas precisa de documentação;
- experimento deve ter data de decisão;
- kill switch deve ser exercitado ocasionalmente;
- flag sem dono deve ser removida ou assumida.
Inclua remoção no próprio plano de entrega. O pull request que adiciona a flag pode criar uma issue ou comentário com critério de fim: “remover quando novo parser estiver 100% por 7 dias sem aumento de erro”.
Comparação com ecossistemas mais maduros
Linguagens como Go têm bibliotecas, exemplos e cultura operacional mais consolidados para serviços web, rollout e plataformas internas. O Golang Brasil é uma boa referência para comparar práticas de backend, observabilidade e deploy. Isso não torna Zig inadequado; apenas muda o ponto de partida.
Em Zig, você provavelmente escreverá mais código de infraestrutura no começo, mas ganha controle sobre alocação, binário, dependências e custo de runtime. Para produto pequeno, ferramenta interna, serviço de borda ou worker de alta performance, esse controle pode valer mais que adotar uma plataforma pesada cedo demais.
Checklist prático
Antes de colocar uma feature flag Zig em produção, confira:
- o default é seguro;
- a flag tem nome claro e dono;
- configuração inválida falha de forma previsível;
- segredo não aparece em log, status ou métrica;
- rollout percentual usa chave estável;
- kill switch foi testado;
- caminho antigo e novo têm testes;
- métricas ou logs distinguem variantes;
- rollback está documentado;
- critério de remoção está escrito.
Feature flag boa reduz medo de deploy. Feature flag ruim aumenta a quantidade de estados que ninguém entende. Em Zig, a diferença está em manter o desenho explícito: uma struct de configuração, decisões pequenas, observabilidade suficiente e disciplina para remover o que já cumpriu seu papel.
Comece simples. Uma variável bem nomeada, um objeto FeatureFlags, testes para 0 e 100, logs de boot e um kill switch claro já resolvem muita coisa. Se o produto crescer, você terá base para evoluir para arquivo dinâmico, banco, painel interno ou serviço de configuração. Se não crescer, você não terá criado uma plataforma inteira para ligar e desligar três ifs.