go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/artifactcontent/metrics.go (about)

     1  // Copyright 2021 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package artifactcontent
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"time"
    21  
    22  	"go.chromium.org/luci/common/clock"
    23  	"go.chromium.org/luci/common/tsmon/field"
    24  	"go.chromium.org/luci/common/tsmon/metric"
    25  	"go.chromium.org/luci/common/tsmon/types"
    26  	"go.chromium.org/luci/server/router"
    27  )
    28  
    29  const firstBucket = 1024
    30  
    31  var (
    32  	// artifactTransferDurations tracks the duration of artifacts transfers
    33  	// between client and storage, e.g. RBE-CAS.
    34  	artifactTransferDurations = metric.NewCumulativeDistribution(
    35  		"resultdb/artifacts/durations",
    36  		"Durations of artifact transfers between client and storage",
    37  		&types.MetricMetadata{Units: types.Milliseconds},
    38  		nil,
    39  		field.Int("size_bucket"),
    40  		field.String("op"), // "upload" or "download".
    41  	)
    42  	// artifactTransferStatus tracks the response statuses of artifact
    43  	// transfers between client and storage, e.g. RBE-CAS.
    44  	artifactTransferStatus = metric.NewCounter(
    45  		"resultdb/artifacts/response_status",
    46  		"Response statuses sent to clients requesting artifact transfer",
    47  		&types.MetricMetadata{Units: "operations"},
    48  		field.Int("http_status"),
    49  		field.Int("size_bucket"),
    50  		field.String("op"), // "upload" or "download".
    51  	)
    52  )
    53  
    54  // NewMetricsWriter creates a MetricsWriter to be used after the transfer is
    55  // complete to time the operation and write the metrics.
    56  // It also replaces the given context's writer with a wrapped writer that keeps
    57  // track of the response status.
    58  func NewMetricsWriter(c *router.Context) *MetricsWriter {
    59  	if _, ok := c.Writer.(*statusTrackingWriter); !ok {
    60  		c.Writer = &statusTrackingWriter{ResponseWriter: c.Writer}
    61  	}
    62  	return &MetricsWriter{
    63  		statusTracker: c.Writer.(*statusTrackingWriter),
    64  		startTime:     clock.Now(c.Request.Context()),
    65  	}
    66  }
    67  
    68  // MetricsWriter can be used to record artifact transfer metrics, by creating
    69  // one out of the router context before the transfer, and calling its .Download
    70  // or .Upload methods afterwards.
    71  type MetricsWriter struct {
    72  	statusTracker *statusTrackingWriter
    73  	startTime     time.Time
    74  }
    75  
    76  // Upload writes upload metrics.
    77  func (mw *MetricsWriter) Upload(ctx context.Context, size int64) {
    78  	mw.writeRequestMetrics(ctx, size, "upload")
    79  }
    80  
    81  // Download writes download metrics.
    82  func (mw *MetricsWriter) Download(ctx context.Context, size int64) {
    83  	mw.writeRequestMetrics(ctx, size, "download")
    84  }
    85  
    86  func (mw *MetricsWriter) writeRequestMetrics(ctx context.Context, size int64, op string) {
    87  	duration := clock.Since(ctx, mw.startTime)
    88  	sizeB := sizeBucket(size)
    89  	artifactTransferDurations.Add(ctx, duration.Seconds()*1000, sizeB, op)
    90  	artifactTransferStatus.Add(ctx, 1, mw.statusTracker.status, sizeB, op)
    91  }
    92  
    93  type statusTrackingWriter struct {
    94  	http.ResponseWriter
    95  	status int
    96  }
    97  
    98  func (w *statusTrackingWriter) WriteHeader(status int) {
    99  	w.status = status
   100  	w.ResponseWriter.WriteHeader(status)
   101  }
   102  
   103  // sizeBucket returns the high bound of the bucket that the given size falls
   104  // into.
   105  // I.e. the smallest power of 4 that is greater or equal to the given size,
   106  // starting at firstBucket.
   107  // E.g. 1024, 4096, 16Ki, 64Ki, 256Ki, 1Mi, ...
   108  func sizeBucket(s int64) (b int64) {
   109  	b = firstBucket
   110  	// Shift b left 2 bits at a time, until it's greater than or equal to s, or
   111  	// until shifting it further would cause an overflow.
   112  	for s > b && b<<2 > 0 {
   113  		b <<= 2
   114  	}
   115  	return b
   116  }