github.com/alwitt/goutils@v0.6.4/rest.go (about) 1 package goutils 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "time" 9 10 "github.com/apex/log" 11 "github.com/go-resty/resty/v2" 12 "github.com/google/uuid" 13 "github.com/urfave/negroni" 14 ) 15 16 // ============================================================================== 17 // Base HTTP Request Handling 18 19 // HTTPRequestLogLevel HTTP request log level data type 20 type HTTPRequestLogLevel string 21 22 // HTTP request log levels 23 const ( 24 HTTPLogLevelWARN HTTPRequestLogLevel = "warn" 25 HTTPLogLevelINFO HTTPRequestLogLevel = "info" 26 HTTPLogLevelDEBUG HTTPRequestLogLevel = "debug" 27 ) 28 29 // RestAPIHandler base REST API handler 30 type RestAPIHandler struct { 31 Component 32 // CallRequestIDHeaderField the HTTP header containing the request ID provided by the caller 33 CallRequestIDHeaderField *string 34 // DoNotLogHeaders marks the set of HTTP headers to not log 35 DoNotLogHeaders map[string]bool 36 // LogLevel configure the request logging level 37 LogLevel HTTPRequestLogLevel 38 // MetricsHelper HTTP request metric collection agent 39 MetricsHelper HTTPRequestMetricHelper 40 } 41 42 // ErrorDetail is the response detail in case of error 43 type ErrorDetail struct { 44 // Code is the response code 45 Code int `json:"code" validate:"required"` 46 // Msg is an optional descriptive message 47 Msg string `json:"message,omitempty"` 48 // Detail is an optional descriptive message providing additional details on the error 49 Detail string `json:"detail,omitempty"` 50 } 51 52 // RestAPIBaseResponse standard REST API response 53 type RestAPIBaseResponse struct { 54 // Success indicates whether the request was successful 55 Success bool `json:"success" validate:"required"` 56 // RequestID gives the request ID to match against logs 57 RequestID string `json:"request_id" validate:"required"` 58 // Error are details in case of errors 59 Error *ErrorDetail `json:"error,omitempty"` 60 } 61 62 /* 63 LoggingMiddleware is a support middleware to be used with Mux to perform request logging 64 65 @param next http.HandlerFunc - the core request handler function 66 @return middleware http.HandlerFunc 67 */ 68 func (h RestAPIHandler) LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc { 69 return func(rw http.ResponseWriter, r *http.Request) { 70 // use provided request id from incoming request if any 71 reqID := uuid.New().String() 72 if h.CallRequestIDHeaderField != nil { 73 reqID = r.Header.Get(*h.CallRequestIDHeaderField) 74 if reqID == "" { 75 // or use some generated string 76 reqID = uuid.New().String() 77 } 78 } 79 // Construct the request param tracking structure 80 params := RestRequestParam{ 81 ID: reqID, 82 Host: r.Host, 83 URI: r.URL.String(), 84 Method: r.Method, 85 Referer: r.Referer(), 86 RemoteAddr: r.RemoteAddr, 87 Proto: r.Proto, 88 ProtoMajor: r.ProtoMajor, 89 ProtoMinor: r.ProtoMinor, 90 Timestamp: time.Now(), 91 RequestHeaders: make(http.Header), 92 } 93 // Fill in the request headers 94 userAgentString := "-" 95 for headerField, headerValues := range r.Header { 96 if _, present := h.DoNotLogHeaders[headerField]; !present { 97 params.RequestHeaders[headerField] = headerValues 98 if headerField == "User-Agent" && len(headerValues) > 0 { 99 userAgentString = fmt.Sprintf("\"%s\"", headerValues[0]) 100 } 101 } 102 } 103 requestReferer := "-" 104 if r.Referer() != "" { 105 requestReferer = fmt.Sprintf("\"%s\"", r.Referer()) 106 } 107 // Construct new context 108 ctxt := context.WithValue(r.Context(), RestRequestParamKey{}, params) 109 // Make the request 110 newRespWriter := negroni.NewResponseWriter(rw) 111 if h.CallRequestIDHeaderField != nil { 112 newRespWriter.Header().Add(*h.CallRequestIDHeaderField, reqID) 113 } 114 next(newRespWriter, r.WithContext(ctxt)) 115 newRespWriter.Flush() 116 respTimestamp := time.Now() 117 // Log result of request 118 logTags := h.GetLogTagsForContext(ctxt) 119 respLen := newRespWriter.Size() 120 respCode := newRespWriter.Status() 121 // Log level based on config 122 logHandle := log.WithFields(logTags). 123 WithField("response_code", respCode). 124 WithField("response_size", respLen). 125 WithField("response_timestamp", respTimestamp.UTC().Format(time.RFC3339Nano)) 126 switch h.LogLevel { 127 case HTTPLogLevelDEBUG: 128 logHandle.Debugf( 129 "%s - - [%s] \"%s %s %s\" %d %d %s %s", 130 params.RemoteAddr, 131 params.Timestamp.UTC().Format(time.RFC3339Nano), 132 params.Method, 133 params.URI, 134 params.Proto, 135 respCode, 136 respLen, 137 requestReferer, 138 userAgentString, 139 ) 140 141 case HTTPLogLevelINFO: 142 logHandle.Infof( 143 "%s - - [%s] \"%s %s %s\" %d %d %s %s", 144 params.RemoteAddr, 145 params.Timestamp.UTC().Format(time.RFC3339Nano), 146 params.Method, 147 params.URI, 148 params.Proto, 149 respCode, 150 respLen, 151 requestReferer, 152 userAgentString, 153 ) 154 155 default: 156 logHandle.Warnf( 157 "%s - - [%s] \"%s %s %s\" %d %d %s %s", 158 params.RemoteAddr, 159 params.Timestamp.UTC().Format(time.RFC3339Nano), 160 params.Method, 161 params.URI, 162 params.Proto, 163 respCode, 164 respLen, 165 requestReferer, 166 userAgentString, 167 ) 168 } 169 170 // Record metrics 171 if h.MetricsHelper != nil { 172 h.MetricsHelper.RecordRequest( 173 r.Method, respCode, respTimestamp.Sub(params.Timestamp), int64(respLen), 174 ) 175 } 176 } 177 } 178 179 /* 180 ReadRequestIDFromContext reads the request ID from the request context if available 181 182 @param ctxt context.Context - a request context 183 @return if available, the request ID 184 */ 185 func (h RestAPIHandler) ReadRequestIDFromContext(ctxt context.Context) string { 186 if ctxt.Value(RestRequestParamKey{}) != nil { 187 v, ok := ctxt.Value(RestRequestParamKey{}).(RestRequestParam) 188 if ok { 189 return v.ID 190 } 191 } 192 return "" 193 } 194 195 /* 196 GetStdRESTSuccessMsg defines a standard success message 197 198 @param ctxt context.Context - a request context 199 @return the standard REST response 200 */ 201 func (h RestAPIHandler) GetStdRESTSuccessMsg(ctxt context.Context) RestAPIBaseResponse { 202 return RestAPIBaseResponse{Success: true, RequestID: h.ReadRequestIDFromContext(ctxt)} 203 } 204 205 /* 206 GetStdRESTErrorMsg defines a standard error message 207 208 @param ctxt context.Context - a request context 209 @param respCode int - the request response code 210 @param errMsg string - the error message 211 @param errDetail string - the details on the error 212 @return the standard REST response 213 */ 214 func (h RestAPIHandler) GetStdRESTErrorMsg( 215 ctxt context.Context, respCode int, errMsg string, errDetail string, 216 ) RestAPIBaseResponse { 217 return RestAPIBaseResponse{ 218 Success: false, 219 RequestID: h.ReadRequestIDFromContext(ctxt), 220 Error: &ErrorDetail{Code: respCode, Msg: errMsg, Detail: errDetail}, 221 } 222 } 223 224 /* 225 WriteRESTResponse helper function to write out the REST API response 226 227 @param w http.ResponseWriter - response writer 228 @param respCode int - the response code 229 @param resp interface{} - the response body 230 @param headers map[string]string - the response header 231 @return whether write succeeded 232 */ 233 func (h RestAPIHandler) WriteRESTResponse( 234 w http.ResponseWriter, respCode int, resp interface{}, headers map[string]string, 235 ) error { 236 w.Header().Set("content-type", "application/json") 237 for name, value := range headers { 238 w.Header().Set(name, value) 239 } 240 w.WriteHeader(respCode) 241 t, err := json.Marshal(resp) 242 if err != nil { 243 w.WriteHeader(http.StatusInternalServerError) 244 return err 245 } 246 if _, err = w.Write(t); err != nil { 247 w.WriteHeader(http.StatusInternalServerError) 248 return err 249 } 250 return nil 251 } 252 253 // ============================================================================== 254 // HTTP Client Support 255 256 // HTTPClientAuthConfig HTTP client OAuth middleware configuration 257 // 258 // Currently only support client-credential OAuth flow configuration 259 type HTTPClientAuthConfig struct { 260 // IssuerURL OpenID provider issuer URL 261 IssuerURL string `json:"issuer"` 262 // ClientID OAuth client ID 263 ClientID string `json:"client_id"` 264 // ClientSecret OAuth client secret 265 ClientSecret string `json:"client_secret"` 266 // TargetAudience target audience `aud` to acquire a token for 267 TargetAudience string `json:"target_audience"` 268 // LogTags auth middleware log tags 269 LogTags log.Fields 270 } 271 272 // HTTPClientRetryConfig HTTP client config retry configuration 273 type HTTPClientRetryConfig struct { 274 // MaxAttempts max number of retry attempts 275 MaxAttempts int `json:"max_attempts"` 276 // InitWaitTime wait time before the first wait retry 277 InitWaitTime time.Duration `json:"initialWaitTimeInSec"` 278 // MaxWaitTime max wait time 279 MaxWaitTime time.Duration `json:"maxWaitTimeInSec"` 280 } 281 282 // setHTTPClientRetryParam helper function to install retry on HTTP client 283 func setHTTPClientRetryParam( 284 client *resty.Client, config HTTPClientRetryConfig, 285 ) *resty.Client { 286 return client. 287 SetRetryCount(config.MaxAttempts). 288 SetRetryWaitTime(config.InitWaitTime). 289 SetRetryMaxWaitTime(config.MaxWaitTime) 290 } 291 292 // installHTTPClientAuthMiddleware install OAuth middleware on HTTP client 293 func installHTTPClientAuthMiddleware( 294 parentCtxt context.Context, 295 parentClient *resty.Client, 296 config HTTPClientAuthConfig, 297 retryCfg HTTPClientRetryConfig, 298 ) error { 299 // define client specifically to be used by 300 oauthHTTPClient := setHTTPClientRetryParam(resty.New(), retryCfg) 301 302 // define OAuth token manager 303 oauthMgmt, err := GetNewClientCredOAuthTokenManager( 304 parentCtxt, oauthHTTPClient, ClientCredOAuthTokenManagerParam{ 305 IDPIssuerURL: config.IssuerURL, 306 ClientID: config.ClientID, 307 ClientSecret: config.ClientSecret, 308 TargetAudience: config.TargetAudience, 309 LogTags: config.LogTags, 310 }, 311 ) 312 if err != nil { 313 return err 314 } 315 316 // Build middleware for parent client 317 parentClient.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error { 318 // Fetch OAuth token for request 319 token, err := oauthMgmt.GetToken(parentCtxt, time.Now().UTC()) 320 if err != nil { 321 return err 322 } 323 324 // Apply the token 325 r.SetAuthToken(token) 326 327 return nil 328 }) 329 330 return nil 331 } 332 333 /* 334 DefineHTTPClient helper function to define a resty HTTP client 335 336 @param parentCtxt context.Context - caller context 337 @param retryConfig HTTPClientRetryConfig - HTTP client retry config 338 @param authConfig *HTTPClientAuthConfig - HTTP client auth config 339 @returns new resty client 340 */ 341 func DefineHTTPClient( 342 parentCtxt context.Context, 343 retryConfig HTTPClientRetryConfig, 344 authConfig *HTTPClientAuthConfig, 345 ) (*resty.Client, error) { 346 newClient := resty.New() 347 348 // Configure resty client retry setting 349 newClient = setHTTPClientRetryParam(newClient, retryConfig) 350 351 // Install OAuth middleware 352 if authConfig != nil { 353 err := installHTTPClientAuthMiddleware(parentCtxt, newClient, *authConfig, retryConfig) 354 if err != nil { 355 return nil, err 356 } 357 } 358 359 return newClient, nil 360 }