capturebody.go (135 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 apm // import "go.elastic.co/apm/v2"
import (
"bytes"
"io"
"net/http"
"net/url"
"sync"
"unicode/utf8"
"go.elastic.co/apm/v2/internal/apmstrings"
"go.elastic.co/apm/v2/model"
)
// CaptureBodyMode holds a value indicating how a tracer should capture
// HTTP request bodies: for transactions, for errors, for both, or neither.
type CaptureBodyMode int
const (
// CaptureBodyOff disables capturing of HTTP request bodies. This is
// the default mode.
CaptureBodyOff CaptureBodyMode = 0
// CaptureBodyErrors captures HTTP request bodies for only errors.
CaptureBodyErrors CaptureBodyMode = 1
// CaptureBodyTransactions captures HTTP request bodies for only
// transactions.
CaptureBodyTransactions CaptureBodyMode = 1 << 1
// CaptureBodyAll captures HTTP request bodies for both transactions
// and errors.
CaptureBodyAll CaptureBodyMode = CaptureBodyErrors | CaptureBodyTransactions
)
var bodyCapturerPool = sync.Pool{
New: func() interface{} {
return &BodyCapturer{}
},
}
// CaptureHTTPRequestBody replaces req.Body and returns a possibly nil
// BodyCapturer which can later be passed to Context.SetHTTPRequestBody
// for setting the request body in a transaction or error context. If the
// tracer is not configured to capture HTTP request bodies, then req.Body
// is left alone and nil is returned.
//
// This must be called before the request body is read. The BodyCapturer's
// Discard method should be called after it is no longer needed, in order
// to recycle its memory.
func (t *Tracer) CaptureHTTPRequestBody(req *http.Request) *BodyCapturer {
if req.Body == nil {
return nil
}
captureBody := t.instrumentationConfig().captureBody
if captureBody == CaptureBodyOff {
return nil
}
bc := bodyCapturerPool.Get().(*BodyCapturer)
bc.captureBody = captureBody
bc.request = req
bc.originalBody = req.Body
bc.buffer.Reset()
req.Body = bodyCapturerReadCloser{BodyCapturer: bc}
return bc
}
// bodyCapturerReadCloser implements io.ReadCloser using the embedded BodyCapturer.
type bodyCapturerReadCloser struct {
*BodyCapturer
}
// Close closes the original body.
func (bc bodyCapturerReadCloser) Close() error {
bc.mu.Lock()
defer bc.mu.Unlock()
return bc.originalBody.Close()
}
// Read reads from the original body, copying into bc.buffer.
func (bc bodyCapturerReadCloser) Read(p []byte) (int, error) {
bc.mu.Lock()
defer bc.mu.Unlock()
n, err := bc.originalBody.Read(p)
if n > 0 {
bc.buffer.Write(p[:n])
}
return n, err
}
// BodyCapturer is returned by Tracer.CaptureHTTPRequestBody to later be
// passed to Context.SetHTTPRequestBody.
//
// Calling Context.SetHTTPRequestBody will reset req.Body to its original
// value, and invalidates the BodyCapturer.
type BodyCapturer struct {
captureBody CaptureBodyMode
mu sync.RWMutex
readbuf [bytes.MinRead]byte
buffer limitedBuffer
request *http.Request
originalBody io.ReadCloser
}
// Discard discards the body capturer: the original request body is
// replaced, and the body capturer is returned to a pool for reuse.
// The BodyCapturer must not be used after calling this.
//
// Discard has no effect if bc is nil.
func (bc *BodyCapturer) Discard() {
if bc == nil {
return
}
bc.mu.Lock()
defer bc.mu.Unlock()
bc.request.Body = bc.originalBody
bodyCapturerPool.Put(bc)
}
func (bc *BodyCapturer) setContext(out *model.RequestBody, req *http.Request) bool {
if req != nil && req.PostForm != nil {
// We must copy the map in case we need to
// sanitize the values. Ideally we should only
// copy if sanitization is necessary, but body
// capture shouldn't typically be enabled so
// we don't currently optimize this.
postForm := make(url.Values, len(req.PostForm))
for k, v := range req.PostForm {
vcopy := make([]string, len(v))
for i := range vcopy {
vcopy[i] = truncateString(v[i])
}
postForm[k] = vcopy
}
out.Form = postForm
return true
}
body, n := bc.getBufferTruncated()
if n == stringLengthLimit {
// There is at least enough data in the buffer
// to hit the string length limit, so we don't
// need to read from bc.originalBody as well.
out.Raw = body
return true
}
bc.mu.Lock()
defer bc.mu.Unlock()
// Read the remaining body, limiting to the maximum number of bytes
// that could make up the truncation limit. We ignore any errors here,
// and just return whatever we can.
rem := utf8.UTFMax * (stringLengthLimit - n)
for {
buf := bc.readbuf[:]
if rem < bytes.MinRead {
buf = buf[:rem]
}
n, err := bc.originalBody.Read(buf)
if n > 0 {
bc.buffer.Write(buf[:n])
rem -= n
}
if rem == 0 || err != nil {
break
}
}
body, _ = bc.getBufferTruncatedLocked()
out.Raw = body
return body != ""
}
func (bc *BodyCapturer) getBufferTruncated() (string, int) {
bc.mu.RLock()
defer bc.mu.RUnlock()
return bc.getBufferTruncatedLocked()
}
func (bc *BodyCapturer) getBufferTruncatedLocked() (string, int) {
return apmstrings.Truncate(bc.buffer.String(), stringLengthLimit)
}
type limitedBuffer struct {
bytes.Buffer
}
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
rem := (stringLengthLimit * utf8.UTFMax) - b.Len()
n = len(p)
if n > rem {
p = p[:rem]
}
written, err := b.Buffer.Write(p)
if err != nil {
n = written
}
return n, err
}