Documentation
¶
Overview ¶
Package store define o contrato de persistência do UpWatch.
A separação em dois contratos é deliberada. MetadataStore guarda definições — monitores, usuários, canais — e vive sempre num banco relacional. TimeseriesStore guarda o volume: batidas e rollups. Só o segundo precisa escalar, e é ele que poderá ser trocado por ClickHouse, InfluxDB ou remote-write sem tocar no resto do sistema.
Qualquer implementação nova precisa passar integralmente na suíte storetest.RunConformance — é o que impede o "plugável" de virar fachada.
Index ¶
- Constants
- Variables
- type AnnouncementFilter
- type AnnouncementRepo
- type BatchWriter
- type BatchWriterOptions
- type ChannelRepo
- type HeartbeatQuery
- type IncidentFilter
- type IncidentRepo
- type MetadataStore
- type MonitorFilter
- type MonitorRepo
- type Page
- type PageFilter
- type RollupQuery
- type SessionRepo
- type StateRepo
- type StatusPageRepo
- type Store
- type TimeRange
- type TimeseriesStore
- type TokenRepo
- type UserRepo
Constants ¶
const ( DefaultPageSize = 50 MaxPageSize = 500 )
Limites de paginação. Listagem sem teto é o que degrada quando a tabela cresce, então toda consulta passa por Normalize antes de virar SQL.
const ( // DefaultMaxBatch é o tamanho de lote que dispara gravação imediata. DefaultMaxBatch = 500 // DefaultFlushInterval garante que uma batida solitária não fique // represada esperando o lote encher. DefaultFlushInterval = time.Second // DefaultQueueSize dá folga para rajadas sem bloquear os workers. DefaultQueueSize = 4096 // DefaultMaxBacklog limita o que fica retido quando o banco falha, // para indisponibilidade prolongada não virar vazamento de memória. DefaultMaxBacklog = 50_000 )
Padrões do batch writer.
Variables ¶
var ( // ErrNotFound indica que o registro pedido não existe. ErrNotFound = errors.New("store: registro não encontrado") // ErrConflict indica violação de unicidade, como nome de monitor repetido. ErrConflict = errors.New("store: conflito de unicidade") )
Functions ¶
This section is empty.
Types ¶
type AnnouncementFilter ¶
type AnnouncementFilter struct {
Page PageFilter
// Since corta a janela. Sem ele, uma instalação de dois anos
// devolveria o histórico inteiro a cada visita anônima.
Since time.Time
// OnlyOpen restringe aos que ainda não foram resolvidos.
OnlyOpen bool
}
AnnouncementFilter seleciona relatos numa listagem.
type AnnouncementRepo ¶
type AnnouncementRepo interface {
Create(ctx context.Context, a *domain.Announcement) error
Get(ctx context.Context, id int64) (domain.Announcement, error)
// Update substitui os componentes em vez de acumulá-los: um relato
// que agora só afeta o console não pode continuar aparecendo na
// página que publica a API.
Update(ctx context.Context, a domain.Announcement) error
Delete(ctx context.Context, id int64) error
// List devolve do mais recente para o mais antigo, que é como
// "incidentes anteriores" se lê.
List(ctx context.Context, f AnnouncementFilter) (Page[domain.Announcement], error)
AddUpdate(ctx context.Context, u *domain.AnnouncementUpdate) error
// Updates devolve em ordem cronológica: a linha do tempo de um
// incidente se lê do começo para o fim.
Updates(ctx context.Context, announcementID int64) ([]domain.AnnouncementUpdate, error)
}
AnnouncementRepo guarda os relatos públicos e sua linha do tempo.
type BatchWriter ¶
type BatchWriter struct {
// contains filtered or unexported fields
}
BatchWriter agrupa batidas e as grava em lote.
É a decisão de desempenho central do UpWatch. Com mil monitores a cada 30 segundos são cerca de 33 escritas por segundo; gravadas uma a uma seriam 33 transações e 33 fsyncs por segundo, o que o SQLite não sustenta. Agrupadas, viram aproximadamente uma transação por segundo.
func NewBatchWriter ¶
func NewBatchWriter(ts TimeseriesStore, opts BatchWriterOptions) *BatchWriter
NewBatchWriter cria o writer. É preciso chamar Run para ele operar.
func (*BatchWriter) Dropped ¶
func (w *BatchWriter) Dropped() int64
Dropped conta as batidas descartadas por estouro do backlog.
func (*BatchWriter) Interval ¶
func (w *BatchWriter) Interval() time.Duration
Interval é o período do flush por tempo.
func (*BatchWriter) MaxBatch ¶
func (w *BatchWriter) MaxBatch() int
MaxBatch é o tamanho de lote que dispara gravação imediata.
func (*BatchWriter) Pending ¶
func (w *BatchWriter) Pending() int64
Pending é quanto está retido aguardando gravação.
func (*BatchWriter) Run ¶
func (w *BatchWriter) Run(ctx context.Context)
Run opera o writer até o contexto terminar, gravando o que restou antes de sair.
type BatchWriterOptions ¶
type BatchWriterOptions struct {
MaxBatch int
Interval time.Duration
QueueSize int
MaxBacklog int
Clock clock.Clock
}
BatchWriterOptions configura o writer. Campos zerados assumem o padrão.
type ChannelRepo ¶
type ChannelRepo interface {
Create(ctx context.Context, c *domain.Channel) error
Get(ctx context.Context, id int64) (domain.Channel, error)
Update(ctx context.Context, c domain.Channel) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context) ([]domain.Channel, error)
// Link e Unlink definem quais canais avisam sobre qual monitor.
Link(ctx context.Context, monitorID, channelID int64) error
Unlink(ctx context.Context, monitorID, channelID int64) error
// ForMonitor devolve apenas os canais habilitados do monitor: um canal
// desligado não deve receber nada, e filtrar aqui evita que cada
// chamador precise lembrar disso.
ForMonitor(ctx context.Context, monitorID int64) ([]domain.Channel, error)
}
ChannelRepo guarda os destinos de aviso e seus vínculos.
type HeartbeatQuery ¶
type HeartbeatQuery struct {
MonitorID int64
// ProbeID vazio traz todas as origens.
ProbeID string
Range TimeRange
Limit int
}
HeartbeatQuery seleciona batidas cruas.
func (HeartbeatQuery) Normalize ¶
func (q HeartbeatQuery) Normalize() HeartbeatQuery
Normalize aplica limites e normaliza a janela.
type IncidentFilter ¶
type IncidentFilter struct {
Page PageFilter
// MonitorID zero traz de todos os monitores.
MonitorID int64
// OnlyOpen restringe aos que ainda não terminaram.
OnlyOpen bool
}
IncidentFilter seleciona incidentes numa listagem.
type IncidentRepo ¶
type IncidentRepo interface {
// Open registra o começo de uma queda. Devolve ErrConflict se já
// houver uma aberta para o monitor.
Open(ctx context.Context, i *domain.Incident) error
// Resolve encerra a queda aberta do monitor. Silencioso quando não há
// nenhuma: encerrar o que já acabou não é erro.
Resolve(ctx context.Context, monitorID int64, at time.Time) error
// Current devolve a queda em curso, ou ErrNotFound.
Current(ctx context.Context, monitorID int64) (domain.Incident, error)
List(ctx context.Context, f IncidentFilter) (Page[domain.Incident], error)
}
IncidentRepo guarda as janelas de indisponibilidade.
type MetadataStore ¶
type MetadataStore interface {
Monitors() MonitorRepo
Users() UserRepo
Sessions() SessionRepo
Tokens() TokenRepo
States() StateRepo
Incidents() IncidentRepo
Channels() ChannelRepo
StatusPages() StatusPageRepo
Announcements() AnnouncementRepo
Close() error
}
MetadataStore guarda as definições do sistema.
type MonitorFilter ¶
type MonitorFilter struct {
Page PageFilter
// Enabled nulo traz habilitados e pausados.
Enabled *bool
Tag string
}
MonitorFilter seleciona monitores numa listagem.
type MonitorRepo ¶
type MonitorRepo interface {
// Create insere o monitor e preenche ID, CreatedAt e UpdatedAt.
// Devolve ErrConflict se o nome já existir.
Create(ctx context.Context, m *domain.Monitor) error
Get(ctx context.Context, id int64) (domain.Monitor, error)
Update(ctx context.Context, m domain.Monitor) error
// Delete remove o monitor e, em cascata, seu histórico.
Delete(ctx context.Context, id int64) error
List(ctx context.Context, f MonitorFilter) (Page[domain.Monitor], error)
}
MonitorRepo é o CRUD de definições de monitor.
type PageFilter ¶
type PageFilter struct {
// AfterID traz a página seguinte à do último ID visto. Zero começa do início.
AfterID int64
Limit int
}
PageFilter é a paginação por keyset. Keyset em vez de OFFSET porque páginas continuam estáveis quando linhas são inseridas ou removidas durante a navegação.
func (PageFilter) Normalize ¶
func (f PageFilter) Normalize() PageFilter
Normalize aplica o limite padrão e trava o teto.
type RollupQuery ¶
type RollupQuery struct {
MonitorID int64
ProbeID string
Resolution domain.Resolution
Range TimeRange
}
RollupQuery seleciona estatísticas agregadas.
func (RollupQuery) Normalize ¶
func (q RollupQuery) Normalize() RollupQuery
Normalize preenche a resolução padrão e normaliza a janela.
type SessionRepo ¶
type SessionRepo interface {
Create(ctx context.Context, s domain.Session) error
// Get busca pelo hash; o cookie carrega o segredo cru.
Get(ctx context.Context, hash string) (domain.Session, error)
Delete(ctx context.Context, hash string) error
// DeleteExpired é a limpeza periódica.
DeleteExpired(ctx context.Context, before time.Time) (int64, error)
// DeleteByUser encerra todas as sessões de uma conta, usado quando a
// senha muda: uma sessão anterior sobrevivendo à troca de senha
// anularia o motivo de trocá-la.
DeleteByUser(ctx context.Context, userID int64) error
}
SessionRepo guarda os logins ativos da interface.
type StateRepo ¶
type StateRepo interface {
// Get devolve o estado, ou o zero quando o monitor ainda não foi
// verificado.
Get(ctx context.Context, monitorID int64) (domain.MonitorState, error)
Save(ctx context.Context, monitorID int64, s domain.MonitorState) error
// All carrega tudo de uma vez, para o motor não fazer uma consulta por
// monitor no arranque.
All(ctx context.Context) (map[int64]domain.MonitorState, error)
}
StateRepo guarda o estado confirmado de cada monitor.
Persistir importa: sem isso um reinício zeraria a contagem de confirmação, e um alvo prestes a ser declarado fora do ar voltaria à estaca zero, atrasando a detecção em várias janelas.
type StatusPageRepo ¶
type StatusPageRepo interface {
// Create insere a página. Devolve ErrConflict se o slug já existir —
// dois slugs iguais fariam duas páginas responderem no mesmo
// endereço, e qual delas responde dependeria da ordem da varredura.
Create(ctx context.Context, p *domain.StatusPage) error
Get(ctx context.Context, id int64) (domain.StatusPage, error)
// GetBySlug é o caminho da requisição anônima.
GetBySlug(ctx context.Context, slug string) (domain.StatusPage, error)
// GetDefault devolve a página que responde em "/status", ou
// ErrNotFound quando nenhuma foi marcada.
GetDefault(ctx context.Context) (domain.StatusPage, error)
// SetDefault promove uma página a padrão e rebaixa a anterior, numa
// transação. Trocar por erro de unicidade obrigaria quem opera a
// desmarcar antes de marcar, o que é uma janela em que "/status" não
// responde.
SetDefault(ctx context.Context, id int64) error
Update(ctx context.Context, p domain.StatusPage) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context) ([]domain.StatusPage, error)
CreateGroup(ctx context.Context, g *domain.StatusPageGroup) error
UpdateGroup(ctx context.Context, g domain.StatusPageGroup) error
// DeleteGroup desfaz o agrupamento sem despublicar os componentes:
// quem tira um grupo espera reorganizar, não remover da página.
DeleteGroup(ctx context.Context, id int64) error
Groups(ctx context.Context, pageID int64) ([]domain.StatusPageGroup, error)
// SetComponent publica um monitor na página, ou atualiza o vínculo
// existente. Idempotente: a interface reenvia o conjunto inteiro em
// vez de calcular a diferença.
SetComponent(ctx context.Context, c domain.StatusPageComponent) error
RemoveComponent(ctx context.Context, pageID, monitorID int64) error
Components(ctx context.Context, pageID int64) ([]domain.StatusPageComponent, error)
}
StatusPageRepo guarda as páginas públicas, seus grupos e componentes.
Um contrato só, e não três, porque grupo e componente não existem fora de uma página: separá-los criaria três repositórios que só se usam juntos e nunca sozinhos.
type Store ¶
type Store interface {
MetadataStore
TimeseriesStore
}
Store reúne os dois contratos. Implementações SQL atendem aos dois com a mesma conexão; backends especializados podem atender só ao segundo.
type TimeRange ¶
TimeRange é uma janela semiaberta [From, To).
func (TimeRange) Contains ¶
Contains testa pertinência com início inclusivo e fim exclusivo, de modo que buckets adjacentes não contem a mesma amostra duas vezes.
type TimeseriesStore ¶
type TimeseriesStore interface {
// WriteHeartbeats grava um lote numa única transação. O batch writer
// agrupa resultados justamente para não pagar um fsync por batida.
WriteHeartbeats(ctx context.Context, hbs []domain.Heartbeat) error
QueryHeartbeats(ctx context.Context, q HeartbeatQuery) ([]domain.Heartbeat, error)
// StreamHeartbeats entrega todas as batidas da janela, em ordem
// cronológica e sem teto de paginação.
//
// É o caminho da agregação, deliberadamente separado do da API. Um
// bucket diário com check de um segundo tem 86.400 batidas; passar
// pelo limite de paginação truncaria a amostra em silêncio e
// produziria percentis que não descrevem período nenhum.
//
// Interromper devolvendo erro em fn aborta a varredura com esse erro.
StreamHeartbeats(ctx context.Context, monitorID int64, r TimeRange, fn func(domain.Heartbeat) error) error
// WriteRollups é idempotente: reprocessar um bucket sobrescreve em vez
// de duplicar, para que uma reexecução após falha seja segura.
WriteRollups(ctx context.Context, rs []domain.Rollup) error
QueryRollups(ctx context.Context, q RollupQuery) ([]domain.Rollup, error)
// PruneHeartbeats apaga batidas anteriores a before e devolve quantas
// saíram. Só pode ser chamado depois da agregação do período — a ordem
// inversa perderia o dado para sempre.
PruneHeartbeats(ctx context.Context, before time.Time) (int64, error)
PruneRollups(ctx context.Context, res domain.Resolution, before time.Time) (int64, error)
// RecordPush registra o sinal recebido de um monitor push.
//
// Vive fora das batidas de propósito: o checker de push grava uma
// batida a cada verificação, então ler a última batida seria ler a
// que ele próprio acabou de escrever, e o monitor pareceria
// eternamente saudável.
RecordPush(ctx context.Context, monitorID int64, at time.Time) error
// LastPush devolve o instante do último sinal, e se houve algum.
LastPush(ctx context.Context, monitorID int64) (time.Time, bool, error)
// LatestHeartbeats devolve a batida mais recente de cada monitor.
//
// Numa consulta só, e não uma por monitor: é o que a exposição de
// métricas lê a cada raspagem, e uma consulta por alvo faria a
// métrica virar a maior fonte de carga do banco que ela observa.
LatestHeartbeats(ctx context.Context) (map[int64]domain.Heartbeat, error)
// Compact devolve ao sistema o espaço liberado pela poda.
//
// Apagar linhas não encolhe o arquivo no SQLite: as páginas ficam
// livres para reuso, mas o arquivo mantém o pico histórico. Sem esta
// etapa o banco pararia de crescer sem nunca devolver espaço — é o
// motivo de o Uptime Kuma expor um botão manual de VACUUM.
//
// Backends com manutenção automática, como o PostgreSQL, podem não
// fazer nada aqui.
Compact(ctx context.Context) error
// OldestHeartbeat é o instante da batida mais antiga preservada, e se
// existe alguma.
//
// É por onde a agregação começa quando não há marca d'água. Assumir
// que nada mais velho que a janela de retenção sobreviveu seria errado
// justamente no primeiro ciclo: a poda só roda depois da agregação,
// então o dado antigo ainda está lá e precisa virar estatística antes
// de sumir.
OldestHeartbeat(ctx context.Context) (time.Time, bool, error)
// RollupWatermark é o último bucket já agregado numa resolução.
// Zero quando nada foi processado ainda.
RollupWatermark(ctx context.Context, res domain.Resolution) (time.Time, error)
SetRollupWatermark(ctx context.Context, res domain.Resolution, bucket time.Time) error
Close() error
}
TimeseriesStore guarda o histórico: batidas cruas e agregados.
É o contrato que backends alternativos precisam implementar.
type TokenRepo ¶
type TokenRepo interface {
Create(ctx context.Context, t *domain.APIToken) error
GetByHash(ctx context.Context, hash string) (domain.APIToken, error)
List(ctx context.Context, userID int64) ([]domain.APIToken, error)
Delete(ctx context.Context, id int64) error
// TouchLastUsed registra o uso, para o operador identificar tokens
// esquecidos antes de revogá-los.
TouchLastUsed(ctx context.Context, id int64, at time.Time) error
}
TokenRepo guarda as credenciais de acesso programático.
type UserRepo ¶
type UserRepo interface {
Create(ctx context.Context, u *domain.User) error
Get(ctx context.Context, id int64) (domain.User, error)
// GetByUsername é o caminho do login.
GetByUsername(ctx context.Context, username string) (domain.User, error)
Update(ctx context.Context, u domain.User) error
// Count decide se o assistente de primeiro acesso deve aparecer.
Count(ctx context.Context) (int, error)
List(ctx context.Context) ([]domain.User, error)
Delete(ctx context.Context, id int64) error
// CountByRole é o que impede a instalação de se trancar para fora:
// remover ou rebaixar o último administrador deixaria a interface
// sem ninguém capaz de criar outro.
CountByRole(ctx context.Context, role domain.Role) (int, error)
}
UserRepo é o CRUD de contas de acesso.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package migrations embarca o schema de cada dialeto no binário.
|
Package migrations embarca o schema de cada dialeto no binário. |
|
Package sqlstore implementa store.Store sobre um banco relacional.
|
Package sqlstore implementa store.Store sobre um banco relacional. |
|
Package storetest é a suíte de conformidade que toda implementação de store precisa passar.
|
Package storetest é a suíte de conformidade que toda implementação de store precisa passar. |