blob: 5098ea3dc5b27da3b1456bb4b26bc8a2049e5fb1 [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cloud
import (
"context"
"fmt"
"net/http"
"os"
"strconv"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/runtime/paniccatcher"
cloudLogging "cloud.google.com/go/logging"
)
// HandlerFunc is a callback to handle the HTTP request.
type HandlerFunc func(context.Context, http.ResponseWriter)
// ScopedRequestHandler offers a middleware functionality that adds
// AppEngine-like handling to individual requests.
//
// ScopedRequestHandler is not required to use the cloud implementation. If
// used, a ScopedRequestHandler's Handle method should be called during
// processing of individual HTTP requests to add pre- and post-request
// processing to these requests.
//
// AppEngine has the behavior of grouping logs emitted during the processing of
// an individual request underneath of a top-level log for that request,
// allowing a user to visually isolate messages associated with a request.
// AppEngine Flex does not offer the same functionality by default, requiring
// users to manually emit the appropriate log messages to achieve a similar
// per-request grouping effect.
//
// The construction used by ScopedRequestHandler works as follows: a "debug log"
// Logger is installed into the handler Context by Use, causing all logs
// generated by the Handler to be associated with the request's Trace ID. After
// the request is completed, ScopedRequestHandler emits a single "request log"
// representing the status of the request's handling and referencing the same
// Trace ID as the debug logs. A user viewing the request log stream would see
// one log entry per request; however, when that log entry is expanded, the
// viewer populates it with the debug logs that share its Trace ID, creating the
// effect of per-request logs.
type ScopedRequestHandler struct {
// CapturePanics, if true, instructs the ScopedRequestHandler to capture and
// handle any panics that occur during its request processing. IF false,
// panics will not be caught or logged by ScopedRequestHandler.
//
// If a panic is caught, it will be logged to the installed logger. Handle
// will return an error indicating that a panic was observed.
CapturePanics bool
}
// Handle is a middleware function that wraps an HTTP request. It sets up a
// preprocessing environment, invokes the supplied Handler, and operates on the
// output after the Handler has finished.
//
// If a panic occurs during Handle, and CapturePanics is true, the panic will
// be logged and consumed, and Handle will return an error. The panic's metadata
// will be available in the error by calling ScopedRequestPanic.
//
// Handle requires a Flex GAE environment to be installed in the supplied
// Context (see Use).
func (sr *ScopedRequestHandler) Handle(c context.Context, rw http.ResponseWriter, fn HandlerFunc) (err error) {
rs := currentRequestState(c)
if rs == nil {
panic("no request state is installed in the Context")
}
// Wrap the ResponseWriter so we can track as much of the response properties
// as we can.
crw := capturingResponseWriter{Inner: rw}
// Finalize on deferred so we can handle panic scenarios.
defer func() {
if derr := sr.finalize(c, rs, &crw); derr != nil {
logging.WithError(derr).Warningf(c, "Failed to finalize scoped request.")
if err != nil {
err = derr
}
}
}()
// If we are handling panics, handle them before we finalize.
if sr.CapturePanics {
defer paniccatcher.Catch(func(p *paniccatcher.Panic) {
uri := "UNKNOWN"
if rs.HTTPRequest != nil {
uri = rs.HTTPRequest.RequestURI
}
// Log the panic to STDERR (flex).
fmt.Fprintf(os.Stderr, "Caught panic during handling of %q: %s\n%s\n", uri, p.Reason, p.Stack)
// Log the reason before the stack in case the stack gets truncated
// due to size limitations.
logging.Fields{
"panic.error": p.Reason,
}.Errorf(c, "Caught panic during handling of %q: %s\n%s", uri, p.Reason, p.Stack)
// Escalate observed log level to Critical.
rs.SeverityTracker.Observe(cloudLogging.Critical)
// Note: it may be too late to send HTTP 500 if the ResponseWriter has
// already sent headers. But there's nothing else we can do at this point
// anyway.
http.Error(&crw, "Internal Server Error. See logs.", http.StatusInternalServerError)
// Record the panic as an error.
if err == nil {
panicTag := makeScopedRequestPanicTag(p)
err = errors.Reason("panic caught during handler: %v", p.Reason).Tag(panicTag).Err()
}
})
}
// Invoke the actual handler.
fn(c, &crw)
return nil
}
func (*ScopedRequestHandler) finalize(c context.Context, rs *requestState, crw *capturingResponseWriter) error {
// If a request log name is supplied, emit a request logging entry.
if rs.cfg.RequestLogger != nil {
now := clock.Now(c)
latency := now.Sub(rs.StartTime)
// Build a logging HTTPRequest.
httpRequest := cloudLogging.HTTPRequest{
Request: rs.HTTPRequest,
Status: crw.Status,
ResponseSize: crw.Bytes,
Latency: latency,
LocalIP: rs.LocalAddr,
}
if req := rs.HTTPRequest; req != nil {
httpRequest.RemoteIP = req.RemoteAddr
}
// The payload is copied from observed "nginx" payload logging in Flex
// enviornment.
payload := struct {
LatencySeconds string `json:"latencySeconds,omitempty"`
Trace string `json:"trace,omitempty"`
}{
LatencySeconds: strconv.FormatFloat(latency.Seconds(), 'f', 3, 64),
Trace: rs.TraceID,
}
labels := make(map[string]string, 2)
if rs.cfg.InstanceID != "" {
labels["appengine.googleapis.com/instance_name"] = rs.cfg.InstanceID
}
labels[TraceIDLogLabel] = rs.TraceID
rs.cfg.RequestLogger.Log(cloudLogging.Entry{
Timestamp: now,
Severity: rs.SeverityTracker.HighestSeverity(),
Payload: payload,
Labels: labels,
InsertID: rs.insertIDGenerator.Next(),
HTTPRequest: &httpRequest,
})
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// capturingResponseWriter
////////////////////////////////////////////////////////////////////////////////
// capturingResponseWriter wraps an http.ResponseWriter, capturing metadata
// related to the response.
type capturingResponseWriter struct {
// Inner is the inner ResponseWriter.
Inner http.ResponseWriter
// Bytes is the total number of response bytes written.
Bytes int64
// HasStatus is true if an HTTP status has been formally set.
HasStatus bool
// Status is the value of the set HTTP status. It is only valid if HasStatus
// is true.
Status int
}
func (crw *capturingResponseWriter) Header() http.Header { return crw.Inner.Header() }
func (crw *capturingResponseWriter) Write(p []byte) (int, error) {
crw.maybeRecordStatus(http.StatusOK)
amt, err := crw.Inner.Write(p)
crw.Bytes += int64(amt)
return amt, err
}
func (crw *capturingResponseWriter) WriteHeader(status int) {
crw.maybeRecordStatus(status)
crw.Inner.WriteHeader(status)
}
func (crw *capturingResponseWriter) maybeRecordStatus(s int) {
if !crw.HasStatus {
crw.Status, crw.HasStatus = s, true
}
}
// Flush implements http.Flusher by passing through the flush call if the underlying
// ResponseWriter implements it.
func (crw *capturingResponseWriter) Flush() {
if f, ok := crw.Inner.(http.Flusher); ok {
f.Flush()
}
}
////////////////////////////////////////////////////////////////////////////////
// scopedRequestPanicTag
////////////////////////////////////////////////////////////////////////////////
var scopedRequestPanicTagKey = errors.NewTagKey("the panic reason that was captured by a ScoepdRequest Handler")
type scopedRequestPanicTag struct {
p *paniccatcher.Panic
}
func makeScopedRequestPanicTag(p *paniccatcher.Panic) errors.TagValueGenerator {
return &scopedRequestPanicTag{p}
}
func (t *scopedRequestPanicTag) GenerateErrorTagValue() errors.TagValue {
return errors.TagValue{Key: scopedRequestPanicTagKey, Value: t.p}
}
// ScopedRequestPanic returns the panic that a ScopedRequestHandler error hasl
// been tagged with.
//
// If the error was not triggered by a captured Panic, ScopedRequestPanic will
// return nil.
func ScopedRequestPanic(err error) *paniccatcher.Panic {
if v, ok := errors.TagValueIn(scopedRequestPanicTagKey, err); ok {
return v.(*paniccatcher.Panic)
}
return nil
}