Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

Metrics #384

Merged
merged 4 commits into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/adminapi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ func realMain(ctx context.Context) error {
r.Use(requireAPIKey)
r.Use(rateLimit)

issueapiController := issueapi.New(ctx, config, db, h)
issueapiController, err := issueapi.New(ctx, config, db, h)
mikehelmick marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("issueapi.New: %w", err)
}
r.Handle("/api/issue", issueapiController.HandleIssue()).Methods("POST")

codeStatusController := codestatus.NewAPI(ctx, config, db, h)
Expand Down
5 changes: 4 additions & 1 deletion cmd/apiserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ func realMain(ctx context.Context) error {
// POST /api/verify
verifyChaff := chaff.New()
defer verifyChaff.Close()
verifyapiController := verifyapi.New(ctx, config, db, h, tokenSigner)
verifyapiController, err := verifyapi.New(ctx, config, db, h, tokenSigner)
if err != nil {
return fmt.Errorf("failed to create verify api controller: %w", err)
}
r.Handle("/api/verify", handleChaff(verifyChaff, verifyapiController.HandleVerify())).Methods("POST")

// POST /api/certificate
Expand Down
5 changes: 4 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,10 @@ func realMain(ctx context.Context) error {
sub.Handle("", homeController.HandleHome()).Methods("GET")

// API for creating new verification codes. Called via AJAX.
issueapiController := issueapi.New(ctx, config, db, h)
issueapiController, err := issueapi.New(ctx, config, db, h)
if err != nil {
return fmt.Errorf("issueapi.New: %w", err)
}
sub.Handle("/issue", issueapiController.HandleIssue()).Methods("POST")
}

Expand Down
16 changes: 15 additions & 1 deletion pkg/controller/certapi/certapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/google/exposure-notifications-verification-server/pkg/database"
"github.com/google/exposure-notifications-verification-server/pkg/keyutils"
"github.com/google/exposure-notifications-verification-server/pkg/render"
"go.opencensus.io/stats"

verifyapi "github.com/google/exposure-notifications-server/pkg/api/v1"
"github.com/google/exposure-notifications-server/pkg/cache"
Expand All @@ -42,6 +43,8 @@ type Controller struct {
pubKeyCache *keyutils.PublicKeyCache // Cache of public keys for verification token verification.
signerCache *cache.Cache // Cache signers on a per-realm basis.
kms keys.KeyManager

metrics *Metrics
}

func New(ctx context.Context, config *config.APIServerConfig, db *database.Database, h *render.Renderer, kms keys.KeyManager) (*Controller, error) {
Expand All @@ -57,6 +60,11 @@ func New(ctx context.Context, config *config.APIServerConfig, db *database.Datab
return nil, fmt.Errorf("cannot create signer cache, likely invalid duration: %w", err)
}

metrics, err := registerMetrics()
if err != nil {
return nil, err
}

return &Controller{
config: config,
db: db,
Expand All @@ -65,12 +73,13 @@ func New(ctx context.Context, config *config.APIServerConfig, db *database.Datab
pubKeyCache: pubKeyCache,
signerCache: signerCache,
kms: kms,
metrics: metrics,
}, nil
}

// Parses and validates the token against the configured keyID and public key.
// If the token si valid the token id (`tid') and subject (`sub`) claims are returned.
func (c *Controller) validateToken(verToken string, publicKey crypto.PublicKey) (string, *database.Subject, error) {
func (c *Controller) validateToken(ctx context.Context, verToken string, publicKey crypto.PublicKey) (string, *database.Subject, error) {
// Parse and validate the verification token.
token, err := jwt.ParseWithClaims(verToken, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
kidHeader := token.Header[verifyapi.KeyIDHeader]
Expand All @@ -84,24 +93,29 @@ func (c *Controller) validateToken(verToken string, publicKey crypto.PublicKey)
return nil, fmt.Errorf("no public key for specified 'kid' not found: %v", kid)
})
if err != nil {
stats.Record(ctx, c.metrics.TokenInvalid.M(1), c.metrics.CertificateErrors.M(1))
c.logger.Errorf("invalid verification token: %v", err)
return "", nil, fmt.Errorf("invalid verification token")
}
tokenClaims, ok := token.Claims.(*jwt.StandardClaims)
if !ok {
stats.Record(ctx, c.metrics.TokenInvalid.M(1), c.metrics.CertificateErrors.M(1))
c.logger.Errorf("invalid claims in verification token")
return "", nil, fmt.Errorf("invalid verification token")
}
if err := tokenClaims.Valid(); err != nil {
stats.Record(ctx, c.metrics.TokenInvalid.M(1), c.metrics.CertificateErrors.M(1))
c.logger.Errorf("JWT is invalid: %v", err)
return "", nil, fmt.Errorf("verification token expired")
}
if !tokenClaims.VerifyIssuer(c.config.TokenSigning.TokenIssuer, true) || !tokenClaims.VerifyAudience(c.config.TokenSigning.TokenIssuer, true) {
stats.Record(ctx, c.metrics.TokenInvalid.M(1), c.metrics.CertificateErrors.M(1))
c.logger.Errorf("jwt contains invalid iss/aud: iss %v aud: %v", tokenClaims.Issuer, tokenClaims.Audience)
return "", nil, fmt.Errorf("verification token not valid")
}
subject, err := database.ParseSubject(tokenClaims.Subject)
if err != nil {
stats.Record(ctx, c.metrics.TokenInvalid.M(1), c.metrics.CertificateErrors.M(1))
return "", nil, fmt.Errorf("invalid subject: %w", err)
}
return tokenClaims.Id, subject, nil
Expand Down
31 changes: 30 additions & 1 deletion pkg/controller/certapi/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import (
"github.com/google/exposure-notifications-verification-server/pkg/controller"
"github.com/google/exposure-notifications-verification-server/pkg/database"
"github.com/google/exposure-notifications-verification-server/pkg/jwthelper"
"github.com/google/exposure-notifications-verification-server/pkg/observability"
"go.opencensus.io/stats"
"go.opencensus.io/tag"

verifyapi "github.com/google/exposure-notifications-server/pkg/api/v1"
)
Expand All @@ -35,41 +38,60 @@ func (c *Controller) HandleCertificate() http.Handler {

authApp := controller.AuthorizedAppFromContext(ctx)
if authApp == nil {
stats.Record(ctx, c.metrics.CertificateErrors.M(1))
c.logger.Errorf("missing authorized app")
controller.MissingAuthorizedApp(w, r, c.h)
return
}

// This is a non terminal error, as we're only using the realm for stats.
realm, err := authApp.Realm(c.db)
if err != nil {
c.logger.Errorf("unable to load realm", "error", err)
} else {
ctx, err = tag.New(ctx,
tag.Upsert(observability.RealmTagKey, realm.Name))
if err != nil {
c.logger.Errorw("unable to record metrics for realm", "realmID", realm.ID, "error", err)
}
}
stats.Record(ctx, c.metrics.Attempts.M(1))

// Get the public key for the token.
publicKey, err := c.pubKeyCache.GetPublicKey(ctx, c.config.TokenSigning.TokenSigningKey, c.kms)
if err != nil {
c.logger.Errorw("failed to get public key", "error", err)
stats.Record(ctx, c.metrics.CertificateErrors.M(1))
c.h.RenderJSON(w, http.StatusInternalServerError, api.InternalError())
return
}

var request api.VerificationCertificateRequest
if err := controller.BindJSON(w, r, &request); err != nil {
c.logger.Errorf("failed to parse json request", "error", err)
stats.Record(ctx, c.metrics.CertificateErrors.M(1))
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrTokenInvalid))
return
}

// Parse and validate the verification token.
tokenID, subject, err := c.validateToken(request.VerificationToken, publicKey)
tokenID, subject, err := c.validateToken(ctx, request.VerificationToken, publicKey)
if err != nil {
stats.Record(ctx, c.metrics.CertificateErrors.M(1))
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrTokenInvalid))
return
}

// Validate the HMAC length. SHA 256 HMAC must be 32 bytes in length.
hmacBytes, err := base64util.DecodeString(request.ExposureKeyHMAC)
if err != nil {
stats.Record(ctx, c.metrics.CertificateErrors.M(1))
c.h.RenderJSON(w, http.StatusBadRequest,
api.Errorf("exposure key HMAC is not a valid base64: %v", err).WithCode(api.ErrHMACInvalid))
return
}
if len(hmacBytes) != 32 {
stats.Record(ctx, c.metrics.CertificateErrors.M(1))
c.h.RenderJSON(w, http.StatusBadRequest,
api.Errorf("exposure key HMAC is not the correct length, want: 32 got: %v", len(hmacBytes)).WithCode(api.ErrHMACInvalid))
return
Expand All @@ -78,6 +100,7 @@ func (c *Controller) HandleCertificate() http.Handler {
// determine the correct signing key to use.
signerInfo, err := c.getSignerForRealm(ctx, authApp)
if err != nil {
stats.Record(ctx, c.metrics.CertificateErrors.M(1))
c.logger.Errorw("failed to get signer", "error", err)
c.h.RenderJSON(w, http.StatusInternalServerError, api.InternalError())
return
Expand All @@ -103,6 +126,7 @@ func (c *Controller) HandleCertificate() http.Handler {
certToken.Header[verifyapi.KeyIDHeader] = signerInfo.KeyID
certificate, err := jwthelper.SignJWT(certToken, signerInfo.Signer)
if err != nil {
stats.Record(ctx, c.metrics.CertificateErrors.M(1))
c.logger.Errorw("failed to sign certificate", "error", err)
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrInternal))
return
Expand All @@ -114,17 +138,22 @@ func (c *Controller) HandleCertificate() http.Handler {
c.logger.Errorw("failed to claim token", "tokenID", tokenID, "error", err)
switch {
case errors.Is(err, database.ErrTokenExpired):
stats.Record(ctx, c.metrics.TokenExpired.M(1), c.metrics.CertificateErrors.M(1))
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err).WithCode(api.ErrTokenExpired))
case errors.Is(err, database.ErrTokenUsed):
stats.Record(ctx, c.metrics.TokenUsed.M(1), c.metrics.CertificateErrors.M(1))
c.h.RenderJSON(w, http.StatusBadRequest, api.Errorf("verification token invalid").WithCode(api.ErrTokenExpired))
case errors.Is(err, database.ErrTokenMetadataMismatch):
stats.Record(ctx, c.metrics.TokenInvalid.M(1), c.metrics.CertificateErrors.M(1))
c.h.RenderJSON(w, http.StatusBadRequest, api.Errorf("verification token invalid").WithCode(api.ErrTokenExpired))
default:
stats.Record(ctx, c.metrics.TokenInvalid.M(1), c.metrics.CertificateErrors.M(1))
c.h.RenderJSON(w, http.StatusBadRequest, api.Error(err))
}
return
}

stats.Record(ctx, c.metrics.CertificateIssued.M(1))
c.h.RenderJSON(w, http.StatusOK, &api.VerificationCertificateResponse{
Certificate: certificate,
})
Expand Down
110 changes: 110 additions & 0 deletions pkg/controller/certapi/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package certapi

import (
"fmt"

"github.com/google/exposure-notifications-verification-server/pkg/observability"

"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
)

var (
MetricPrefix = observability.MetricRoot + "/api/certificate"
)

type Metrics struct {
Attempts *stats.Int64Measure
TokenExpired *stats.Int64Measure
TokenUsed *stats.Int64Measure
TokenInvalid *stats.Int64Measure
CertificateIssued *stats.Int64Measure
CertificateErrors *stats.Int64Measure
}

func registerMetrics() (*Metrics, error) {
mAttempts := stats.Int64(MetricPrefix+"/attempts", "certificate issue attempts", stats.UnitDimensionless)
if err := view.Register(&view.View{
Name: MetricPrefix + "/attempt_count",
Measure: mAttempts,
Description: "The count of certificate issue attempts",
TagKeys: []tag.Key{observability.RealmTagKey},
Aggregation: view.Count(),
}); err != nil {
return nil, fmt.Errorf("stat view registration failure: %w", err)
}
mTokenExpired := stats.Int64(MetricPrefix+"/token_expired", "expired tokens on certificate issue", stats.UnitDimensionless)
if err := view.Register(&view.View{
Name: MetricPrefix + "/token_expired_count",
Measure: mTokenExpired,
Description: "The count of expired tokens on certificate issue",
TagKeys: []tag.Key{observability.RealmTagKey},
Aggregation: view.Count(),
}); err != nil {
return nil, fmt.Errorf("stat view registration failure: %w", err)
}
mTokenUsed := stats.Int64(MetricPrefix+"/token_used", "already used tokens on certificate issue", stats.UnitDimensionless)
if err := view.Register(&view.View{
Name: MetricPrefix + "/token_used_count",
Measure: mTokenUsed,
Description: "The count of already used tokens on certificate issue",
TagKeys: []tag.Key{observability.RealmTagKey},
Aggregation: view.Count(),
}); err != nil {
return nil, fmt.Errorf("stat view registration failure: %w", err)
}
mTokenInvalid := stats.Int64(MetricPrefix+"/invalid_token", "invalid tokens on certificate issue", stats.UnitDimensionless)
if err := view.Register(&view.View{
Name: MetricPrefix + "/invalid_token_count",
Measure: mTokenInvalid,
Description: "The count of invalid tokens on certificate issue",
TagKeys: []tag.Key{observability.RealmTagKey},
Aggregation: view.Count(),
}); err != nil {
return nil, fmt.Errorf("stat view registration failure: %w", err)
}
mCertificateIssued := stats.Int64(MetricPrefix+"/issue", "certificates issued", stats.UnitDimensionless)
if err := view.Register(&view.View{
Name: MetricPrefix + "/issue_count",
Measure: mCertificateIssued,
Description: "The count of certificates issued",
TagKeys: []tag.Key{observability.RealmTagKey},
Aggregation: view.Count(),
}); err != nil {
return nil, fmt.Errorf("stat view registration failure: %w", err)
}
mCertificateErrors := stats.Int64(MetricPrefix+"/errors", "certificate issue errors", stats.UnitDimensionless)
if err := view.Register(&view.View{
Name: MetricPrefix + "/error_count",
Measure: mCertificateErrors,
Description: "The count of certificate issue errors",
TagKeys: []tag.Key{observability.RealmTagKey},
Aggregation: view.Count(),
}); err != nil {
return nil, fmt.Errorf("stat view registration failure: %w", err)
}

return &Metrics{
Attempts: mAttempts,
TokenExpired: mTokenExpired,
TokenUsed: mTokenUsed,
TokenInvalid: mTokenInvalid,
CertificateIssued: mCertificateIssued,
CertificateErrors: mCertificateErrors,
}, nil
}
Loading