blob: 2571b8a5e31868722bc126af1e40f253a2ffe7a1 [file] [log] [blame]
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/metrics2"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
"golang.org/x/oauth2"
)
const (
firestoreBackupOperationType = "type.googleapis.com/google.firestore.admin.v1.ExportDocumentsMetadata"
firestoreBackupUriPrefix = "gs://skia-firestore-backup/everything/"
)
// firestoreOperations decodes the JSON response from a Firestore list operations request.
type firestoreOperations struct {
Operations []firestoreOperation `json:"operations"`
NextPageToken string `json:"nextPageToken"`
}
// firestoreOperation partially decodes a single Firestore operation from JSON.
type firestoreOperation struct {
Name string `json:"name"`
Metadata firestoreOperationMetadata `json:"metadata"`
Done bool `json:"done"`
// other fields omitted
}
// firestoreOperationMetadata is a sub-struct of firestoreOperation.
type firestoreOperationMetadata struct {
Type string `json:"@type"`
EndTime string `json:"endTime"`
OperationState string `json:"operationState"`
OutputUriPrefix string `json:"outputUriPrefix"`
// other fields omitted
}
// Returns the most recent time that a Firestore ExportDocumentsMetadata operation exporting to
// firestoreBackupUriPrefix completed successfully.
func getFirestoreLastBackupCompleted(ctx context.Context, httpClient *http.Client) (time.Time, error) {
z := time.Time{}
latest := time.Time{}
nextPageToken := ""
for {
listURL := fmt.Sprintf("https://firestore.googleapis.com/v1/projects/skia-firestore/databases/(default)/operations?filter=%s", url.QueryEscape("metadata.operationState=SUCCESSFUL"))
if nextPageToken != "" {
listURL = listURL + "&pageToken=" + nextPageToken
}
sklog.Debugf("Sending Firestore list operations request: %q", listURL)
resp, err := httpClient.Get(listURL)
if err != nil {
return z, fmt.Errorf("Error performing Firestore list operations request: %s", err)
}
operations := firestoreOperations{}
if err := json.NewDecoder(resp.Body).Decode(&operations); err != nil {
return z, fmt.Errorf("Unable to decode Firestore list operations response as JSON: %s", err)
}
sklog.Debugf("Decoded Firestore list operations response: %+v", operations)
for _, o := range operations.Operations {
if o.Done &&
o.Metadata.Type == firestoreBackupOperationType &&
o.Metadata.OperationState == "SUCCESSFUL" &&
strings.HasPrefix(o.Metadata.OutputUriPrefix, firestoreBackupUriPrefix) {
if parsed, err := time.Parse(time.RFC3339Nano, o.Metadata.EndTime); err != nil {
return z, fmt.Errorf("Invalid time %q in Firestore list operations response for %q: %s", o.Metadata.EndTime, o.Name, err)
} else if parsed.After(latest) {
latest = parsed
}
}
}
if operations.NextPageToken == "" {
break
} else {
nextPageToken = operations.NextPageToken
}
}
if util.TimeIsZero(latest) {
return z, fmt.Errorf("Firestore list operations response contained no matching results.")
}
return latest, nil
}
// StartFirestoreBackupMetrics starts a goroutine to periodically update the
// last_successful_firestore_backup liveness metric.
func StartFirestoreBackupMetrics(ctx context.Context, tokenSource oauth2.TokenSource) error {
httpClient := httputils.DefaultClientConfig().WithTokenSource(tokenSource).With2xxOnly().Client()
lvMetrics := metrics2.NewLiveness("last_successful_firestore_backup_metrics_update")
lvBackup := metrics2.NewLiveness("last_successful_firestore_backup")
go util.RepeatCtx(ctx, 5*time.Minute, func(ctx context.Context) {
if lastBackupTime, err := getFirestoreLastBackupCompleted(ctx, httpClient); err != nil {
sklog.Errorf("Failed to update firestore backup metrics: %s", err)
} else {
lvBackup.ManualReset(lastBackupTime)
lvMetrics.Reset()
}
})
return nil
}