github.com/blend/go-sdk@v1.20220411.3/r2/event.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package r2
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"io"
    14  	"net/http"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/blend/go-sdk/logger"
    19  	"github.com/blend/go-sdk/timeutil"
    20  	"github.com/blend/go-sdk/webutil"
    21  )
    22  
    23  const (
    24  	// Flag is a logger event flag.
    25  	Flag = "http.client.request"
    26  	// FlagResponse is a logger event flag.
    27  	FlagResponse = "http.client.response"
    28  )
    29  
    30  // NewEvent returns a new event.
    31  func NewEvent(flag string, options ...EventOption) Event {
    32  	e := Event{
    33  		Flag: flag,
    34  	}
    35  	for _, option := range options {
    36  		option(&e)
    37  	}
    38  	return e
    39  }
    40  
    41  // NewEventListener returns a new r2 event listener.
    42  func NewEventListener(listener func(context.Context, Event)) logger.Listener {
    43  	return func(ctx context.Context, e logger.Event) {
    44  		if typed, isTyped := e.(Event); isTyped {
    45  			listener(ctx, typed)
    46  		}
    47  	}
    48  }
    49  
    50  // NewEventFilter returns a new r2 event filter.
    51  func NewEventFilter(filter func(context.Context, Event) (Event, bool)) logger.Filter {
    52  	return func(ctx context.Context, e logger.Event) (logger.Event, bool) {
    53  		if typed, isTyped := e.(Event); isTyped {
    54  			return filter(ctx, typed)
    55  		}
    56  		return e, false
    57  	}
    58  }
    59  
    60  var (
    61  	_ logger.Event        = (*Event)(nil)
    62  	_ logger.TextWritable = (*Event)(nil)
    63  	_ logger.JSONWritable = (*Event)(nil)
    64  )
    65  
    66  // Event is a response to outgoing requests.
    67  type Event struct {
    68  	Flag string
    69  	// The request metadata.
    70  	Request *http.Request
    71  	// The response metadata (excluding the body).
    72  	Response *http.Response
    73  	// The response body.
    74  	Body []byte
    75  	// Elapsed is the time elapsed.
    76  	Elapsed time.Duration
    77  }
    78  
    79  // GetFlag implements logger.Event.
    80  func (e Event) GetFlag() string { return e.Flag }
    81  
    82  // WriteText writes the event to a text writer.
    83  func (e Event) WriteText(tf logger.TextFormatter, wr io.Writer) {
    84  	if e.Request != nil && e.Response != nil {
    85  		fmt.Fprintf(wr, "%s %s %s (%v)", e.Request.Method, e.Request.URL.String(), webutil.ColorizeStatusCodeWithFormatter(tf, e.Response.StatusCode), e.Elapsed)
    86  	} else if e.Request != nil {
    87  		fmt.Fprintf(wr, "%s %s", e.Request.Method, e.Request.URL.String())
    88  	}
    89  	if e.Body != nil {
    90  		fmt.Fprint(wr, logger.Newline)
    91  		fmt.Fprint(wr, string(e.Body))
    92  	}
    93  }
    94  
    95  // Decompose implements logger.JSONWritable.
    96  func (e Event) Decompose() map[string]interface{} {
    97  	output := make(map[string]interface{})
    98  	if e.Request != nil {
    99  		var url string
   100  		if e.Request.URL != nil {
   101  			url = e.Request.URL.String()
   102  		}
   103  		output["req"] = map[string]interface{}{
   104  			"method":  e.Request.Method,
   105  			"url":     url,
   106  			"headers": e.Request.Header,
   107  		}
   108  	}
   109  	if e.Response != nil {
   110  		output["res"] = map[string]interface{}{
   111  			"statusCode":      e.Response.StatusCode,
   112  			"contentLength":   e.Response.ContentLength,
   113  			"contentType":     tryHeader(e.Response.Header, "Content-Type", "content-type"),
   114  			"contentEncoding": tryHeader(e.Response.Header, "Content-Encoding", "content-encoding"),
   115  			"headers":         e.Response.Header,
   116  			"cert":            webutil.ParseCertInfo(e.Response),
   117  			"elapsed":         timeutil.Milliseconds(e.Elapsed),
   118  		}
   119  	}
   120  	if e.Body != nil {
   121  		output["body"] = string(e.Body)
   122  	}
   123  
   124  	return output
   125  }
   126  
   127  // EventJSONSchema is the json schema of the logger event.
   128  type EventJSONSchema struct {
   129  	Req struct {
   130  		StartTime time.Time           `json:"startTime"`
   131  		Method    string              `json:"method"`
   132  		URL       string              `json:"url"`
   133  		Headers   map[string][]string `json:"headers"`
   134  	} `json:"req"`
   135  	Res struct {
   136  		CompleteTime  time.Time           `json:"completeTime"`
   137  		StatusCode    int                 `json:"statusCode"`
   138  		ContentLength int                 `json:"contentLength"`
   139  		Headers       map[string][]string `json:"headers"`
   140  	} `json:"res"`
   141  	Body string `json:"body"`
   142  }
   143  
   144  func tryHeader(headers http.Header, keys ...string) string {
   145  	for _, key := range keys {
   146  		if values, hasValues := headers[key]; hasValues {
   147  			return strings.Join(values, ";")
   148  		}
   149  	}
   150  	return ""
   151  }