github.com/prebid/prebid-server/v2@v2.18.0/prebid_cache_client/client.go (about)

     1  package prebid_cache_client
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/prebid/prebid-server/v2/config"
    16  	"github.com/prebid/prebid-server/v2/metrics"
    17  
    18  	"github.com/buger/jsonparser"
    19  	"github.com/golang/glog"
    20  	"golang.org/x/net/context/ctxhttp"
    21  )
    22  
    23  // Client stores values in Prebid Cache. For more info, see https://github.com/prebid/prebid-cache
    24  type Client interface {
    25  	// PutJson stores JSON values for the given openrtb2.Bids in the cache. Null values will be
    26  	//
    27  	// The returned string slice will always have the same number of elements as the values argument. If a
    28  	// value could not be saved, the element will be an empty string. Implementations are responsible for
    29  	// logging any relevant errors to the app logs
    30  	PutJson(ctx context.Context, values []Cacheable) ([]string, []error)
    31  
    32  	// GetExtCacheData gets the scheme, host, and path of the externally accessible cache url.
    33  	GetExtCacheData() (scheme string, host string, path string)
    34  }
    35  
    36  type PayloadType string
    37  
    38  const (
    39  	TypeJSON PayloadType = "json"
    40  	TypeXML  PayloadType = "xml"
    41  )
    42  
    43  type Cacheable struct {
    44  	Type       PayloadType     `json:"type,omitempty"`
    45  	Data       json.RawMessage `json:"value,omitempty"`
    46  	TTLSeconds int64           `json:"ttlseconds,omitempty"`
    47  	Key        string          `json:"key,omitempty"`
    48  
    49  	BidID     string `json:"bidid,omitempty"`     // this is "/vtrack" specific
    50  	Bidder    string `json:"bidder,omitempty"`    // this is "/vtrack" specific
    51  	Timestamp int64  `json:"timestamp,omitempty"` // this is "/vtrack" specific
    52  }
    53  
    54  func NewClient(httpClient *http.Client, conf *config.Cache, extCache *config.ExternalCache, metrics metrics.MetricsEngine) Client {
    55  	return &clientImpl{
    56  		httpClient:          httpClient,
    57  		putUrl:              conf.GetBaseURL() + "/cache",
    58  		externalCacheScheme: extCache.Scheme,
    59  		externalCacheHost:   extCache.Host,
    60  		externalCachePath:   extCache.Path,
    61  		metrics:             metrics,
    62  	}
    63  }
    64  
    65  type clientImpl struct {
    66  	httpClient          *http.Client
    67  	putUrl              string
    68  	externalCacheScheme string
    69  	externalCacheHost   string
    70  	externalCachePath   string
    71  	metrics             metrics.MetricsEngine
    72  }
    73  
    74  func (c *clientImpl) GetExtCacheData() (string, string, string) {
    75  	path := c.externalCachePath
    76  	if path == "/" {
    77  		// Only the slash for the path, remove it to empty
    78  		path = ""
    79  	} else if len(path) > 0 && !strings.HasPrefix(path, "/") {
    80  		// Path defined but does not start with "/", prepend it
    81  		path = "/" + path
    82  	}
    83  
    84  	return c.externalCacheScheme, c.externalCacheHost, path
    85  }
    86  
    87  func (c *clientImpl) PutJson(ctx context.Context, values []Cacheable) (uuids []string, errs []error) {
    88  	errs = make([]error, 0, 1)
    89  	if len(values) < 1 {
    90  		return nil, errs
    91  	}
    92  
    93  	uuidsToReturn := make([]string, len(values))
    94  
    95  	postBody, err := encodeValues(values)
    96  	if err != nil {
    97  		logError(&errs, "Error creating JSON for prebid cache: %v", err)
    98  		return uuidsToReturn, errs
    99  	}
   100  
   101  	httpReq, err := http.NewRequest("POST", c.putUrl, bytes.NewReader(postBody))
   102  	if err != nil {
   103  		logError(&errs, "Error creating POST request to prebid cache: %v", err)
   104  		return uuidsToReturn, errs
   105  	}
   106  
   107  	httpReq.Header.Add("Content-Type", "application/json;charset=utf-8")
   108  	httpReq.Header.Add("Accept", "application/json")
   109  
   110  	startTime := time.Now()
   111  	anResp, err := ctxhttp.Do(ctx, c.httpClient, httpReq)
   112  	elapsedTime := time.Since(startTime)
   113  	if err != nil {
   114  		c.metrics.RecordPrebidCacheRequestTime(false, elapsedTime)
   115  		logError(&errs, "Error sending the request to Prebid Cache: %v; Duration=%v, Items=%v, Payload Size=%v", err, elapsedTime, len(values), len(postBody))
   116  		return uuidsToReturn, errs
   117  	}
   118  	defer anResp.Body.Close()
   119  	c.metrics.RecordPrebidCacheRequestTime(true, elapsedTime)
   120  
   121  	responseBody, err := io.ReadAll(anResp.Body)
   122  	if anResp.StatusCode != 200 {
   123  		logError(&errs, "Prebid Cache call to %s returned %d: %s", c.putUrl, anResp.StatusCode, responseBody)
   124  		return uuidsToReturn, errs
   125  	}
   126  
   127  	currentIndex := 0
   128  	processResponse := func(uuidObj []byte, _ jsonparser.ValueType, _ int, err error) {
   129  		if uuid, valueType, _, err := jsonparser.Get(uuidObj, "uuid"); err != nil {
   130  			logError(&errs, "Prebid Cache returned a bad value at index %d. Error was: %v. Response body was: %s", currentIndex, err, string(responseBody))
   131  		} else if valueType != jsonparser.String {
   132  			logError(&errs, "Prebid Cache returned a %v at index %d in: %v", valueType, currentIndex, string(responseBody))
   133  		} else {
   134  			if uuidsToReturn[currentIndex], err = jsonparser.ParseString(uuid); err != nil {
   135  				logError(&errs, "Prebid Cache response index %d could not be parsed as string: %v", currentIndex, err)
   136  				uuidsToReturn[currentIndex] = ""
   137  			}
   138  		}
   139  		currentIndex++
   140  	}
   141  
   142  	if _, err := jsonparser.ArrayEach(responseBody, processResponse, "responses"); err != nil {
   143  		logError(&errs, "Error interpreting Prebid Cache response: %v\nResponse was: %s", err, string(responseBody))
   144  		return uuidsToReturn, errs
   145  	}
   146  
   147  	return uuidsToReturn, errs
   148  }
   149  
   150  func logError(errs *[]error, format string, a ...interface{}) {
   151  	msg := fmt.Sprintf(format, a...)
   152  	glog.Error(msg)
   153  	*errs = append(*errs, errors.New(msg))
   154  }
   155  
   156  func encodeValues(values []Cacheable) ([]byte, error) {
   157  	var buf bytes.Buffer
   158  	buf.WriteString(`{"puts":[`)
   159  	for i := 0; i < len(values); i++ {
   160  		if err := encodeValueToBuffer(values[i], i != 0, &buf); err != nil {
   161  			return nil, err
   162  		}
   163  	}
   164  	buf.WriteString("]}")
   165  	return buf.Bytes(), nil
   166  }
   167  
   168  func encodeValueToBuffer(value Cacheable, leadingComma bool, buffer *bytes.Buffer) error {
   169  	if leadingComma {
   170  		buffer.WriteByte(',')
   171  	}
   172  
   173  	buffer.WriteString(`{"type":"`)
   174  	buffer.WriteString(string(value.Type))
   175  	if value.TTLSeconds > 0 {
   176  		buffer.WriteString(`","ttlseconds":`)
   177  		buffer.WriteString(strconv.FormatInt(value.TTLSeconds, 10))
   178  		buffer.WriteString(`,"value":`)
   179  	} else {
   180  		buffer.WriteString(`","value":`)
   181  	}
   182  	buffer.Write(value.Data)
   183  	if len(value.Key) > 0 {
   184  		buffer.WriteString(`,"key":"`)
   185  		buffer.WriteString(string(value.Key))
   186  		buffer.WriteString(`"`)
   187  	}
   188  
   189  	//vtrack specific
   190  	if len(value.BidID) > 0 {
   191  		buffer.WriteString(`,"bidid":"`)
   192  		buffer.WriteString(string(value.BidID))
   193  		buffer.WriteString(`"`)
   194  	}
   195  
   196  	if len(value.Bidder) > 0 {
   197  		buffer.WriteString(`,"bidder":"`)
   198  		buffer.WriteString(string(value.Bidder))
   199  		buffer.WriteString(`"`)
   200  	}
   201  
   202  	if value.Timestamp > 0 {
   203  		buffer.WriteString(`,"timestamp":`)
   204  		buffer.WriteString(strconv.FormatInt(value.Timestamp, 10))
   205  	}
   206  
   207  	buffer.WriteByte('}')
   208  	return nil
   209  }