mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* frontend: refactor metrics collection The main goal is to provide accurate inspected bytes metrics (and othes) in responses for a potential future fair-use policy. Until now, we were counting inspected bytes regardless of the cache. With this PR, the responses that come from cache do not count towards inspected bytes. - ensure that we collect response metrics in the same way for all endpoints. - add cache header to querier responses, to indicate whether the response came from the cache - set 0 to metrics from cached querier responses * Update changelog * Ignore gosec integer conversion rule in a few lines
176 lines
4.3 KiB
Go
176 lines
4.3 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/grafana/tempo/modules/frontend/combiner"
|
|
|
|
"github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
"github.com/grafana/tempo/pkg/api"
|
|
"github.com/grafana/tempo/pkg/cache"
|
|
)
|
|
|
|
func NewCachingWare(cacheProvider cache.Provider, role cache.Role, logger log.Logger) Middleware {
|
|
return MiddlewareFunc(func(next RoundTripper) RoundTripper {
|
|
return cachingWare{
|
|
next: next,
|
|
cache: newFrontendCache(cacheProvider, role, logger),
|
|
}
|
|
})
|
|
}
|
|
|
|
type cachingWare struct {
|
|
next RoundTripper
|
|
cache *frontendCache
|
|
}
|
|
|
|
// RoundTrip implements http.RoundTripper
|
|
func (c cachingWare) RoundTrip(req Request) (*http.Response, error) {
|
|
// short circuit everything if there cache is no cache
|
|
if c.cache == nil {
|
|
return c.next.RoundTrip(req)
|
|
}
|
|
|
|
// extract cache key
|
|
key := req.CacheKey()
|
|
if len(key) > 0 {
|
|
body := c.cache.fetchBytes(req.Context(), key)
|
|
if len(body) > 0 {
|
|
contentType := determineContentType(body)
|
|
|
|
resp := &http.Response{
|
|
Header: http.Header{api.HeaderContentType: []string{contentType}, combiner.TempoCacheHeader: []string{combiner.TempoCacheHit}},
|
|
StatusCode: http.StatusOK,
|
|
Status: http.StatusText(http.StatusOK),
|
|
Body: io.NopCloser(bytes.NewBuffer(body)),
|
|
ContentLength: int64(len(body)),
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
}
|
|
|
|
resp, err := c.next.RoundTrip(req)
|
|
|
|
// Add cache miss header for all responses that weren't from cache
|
|
if resp != nil {
|
|
if resp.Header == nil {
|
|
resp.Header = http.Header{}
|
|
}
|
|
resp.Header[combiner.TempoCacheHeader] = []string{combiner.TempoCacheMiss}
|
|
}
|
|
|
|
// do not cache if there was an error
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
|
|
// do not cache if response is not HTTP 2xx
|
|
if !shouldCache(resp.StatusCode) {
|
|
return resp, nil
|
|
}
|
|
|
|
if len(key) > 0 {
|
|
// don't bother caching if the response is too large
|
|
maxItemSize := c.cache.c.MaxItemSize()
|
|
if maxItemSize > 0 && resp.ContentLength > int64(maxItemSize) {
|
|
return resp, nil
|
|
}
|
|
|
|
buffer, err := api.ReadBodyToBuffer(resp)
|
|
if err != nil {
|
|
return resp, fmt.Errorf("failed to cache: %w", err)
|
|
}
|
|
|
|
// reset the body so the caller can read it
|
|
resp.Body = io.NopCloser(buffer)
|
|
|
|
// cache the response
|
|
// todo: currently this is blindly caching any 200 status codes. it would be a bug, but it's possible for a querier
|
|
// to return a 200 status code with a response that does not parse as the expected type in the combiner.
|
|
// technically this should never happen...
|
|
// long term we should migrate the sync part of the pipeline to use generics so we can do the parsing early in the pipeline
|
|
// and be confident it's cacheable.
|
|
c.cache.store(req.Context(), key, buffer.Bytes())
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func determineContentType(body []byte) string {
|
|
// TODO - Cache should capture all of the relevant parts of the
|
|
// original response including both content-type and content-length headers, possibly more.
|
|
// But upgrading the cache format requires migration/detection of previous format either way.
|
|
// It's tempting to use https://pkg.go.dev/net/http#DetectContentType but it doesn't detect
|
|
// json or proto.
|
|
if body[0] == '{' {
|
|
return api.HeaderAcceptJSON
|
|
}
|
|
return api.HeaderAcceptProtobuf
|
|
}
|
|
|
|
func shouldCache(statusCode int) bool {
|
|
return statusCode/100 == 2
|
|
}
|
|
|
|
type frontendCache struct {
|
|
c cache.Cache
|
|
}
|
|
|
|
func newFrontendCache(cacheProvider cache.Provider, role cache.Role, logger log.Logger) *frontendCache {
|
|
var c cache.Cache
|
|
if cacheProvider != nil {
|
|
c = cacheProvider.CacheFor(role)
|
|
}
|
|
|
|
level.Info(logger).Log("msg", "init frontend cache", "role", role, "enabled", c != nil)
|
|
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
|
|
return &frontendCache{
|
|
c: c,
|
|
}
|
|
}
|
|
|
|
// store stores the response body in the cache. the caller assumes the responsibility of closing the response body
|
|
func (c *frontendCache) store(ctx context.Context, key string, buffer []byte) {
|
|
if c.c == nil {
|
|
return
|
|
}
|
|
|
|
if key == "" {
|
|
return
|
|
}
|
|
|
|
if len(buffer) == 0 {
|
|
return
|
|
}
|
|
|
|
c.c.Store(ctx, []string{key}, [][]byte{buffer})
|
|
}
|
|
|
|
// fetch fetches the response body from the cache. the caller assumes the responsibility of closing the response body.
|
|
func (c *frontendCache) fetchBytes(ctx context.Context, key string) []byte {
|
|
if c.c == nil {
|
|
return nil
|
|
}
|
|
|
|
if len(key) == 0 {
|
|
return nil
|
|
}
|
|
|
|
buf, found := c.c.FetchKey(ctx, key)
|
|
if !found {
|
|
return nil
|
|
}
|
|
|
|
return buf
|
|
}
|