Skip to content

Commit 502ef91

Browse files
Marcel LudwigAlex Schneider
Marcel Ludwig
and
Alex Schneider
authored
Add missing bytes field to backend response log (#494)
* Add missing bytes field to backend response log * (docs) remove unknown utf8 chars and reformat tables * (docs) add response.bytes field to backend log documentation * Add content-length fallback * rm obsolete key * more checks in test * handle timeout cancel select case * fmt * Fix missing token request within custom log context * Fix possible timer deadlock golang/go#27169 * cancel while reading results * fire collected backend logStack at the end endpoint handler has to many exits; even with panic recovers * Add changelog entry Co-authored-by: Alex Schneider <[email protected]>
1 parent 27bce19 commit 502ef91

File tree

15 files changed

+398
-115
lines changed

15 files changed

+398
-115
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
1212
* Log malformed duration settings ([#487](https://github.com/avenga/couper/pull/487))
1313
* `url` attribute could make use of our wildcard pattern `/**` and relative urls in combination with a backend reference ([#480](https://github.com/avenga/couper/pull/480))
1414
* Error handling for `backend`, `backend_openapi_validation` and `backend_timeout` error types ([#490](https://github.com/avenga/couper/pull/490))
15+
* `response.bytes` log-field to backend logs if read from body, fallback is the `Content-Length` header ([#494](https://github.com/avenga/couper/pull/494))
1516

1617
* **Changed**
1718
* Permission handling: ([#477](https://github.com/avenga/couper/pull/477))

config/request/context_key.go

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const (
66
ContextType ContextKey = iota
77
APIName
88
AccessControls
9+
BackendBytes
910
BackendName
1011
BackendParams
1112
BackendTokenRequest

docs/LOGS.md

+88-87
Large diffs are not rendered by default.

eval/sync.go

+15-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import (
55
"sync"
66

77
"github.com/zclconf/go-cty/cty"
8+
9+
"github.com/avenga/couper/config/request"
810
)
911

12+
const TokenRequestPrefix = "_tr_"
13+
1014
type SyncedVariables struct {
1115
items sync.Map
1216
}
@@ -20,11 +24,18 @@ func NewSyncedVariables() *SyncedVariables {
2024
type syncPair struct {
2125
name string
2226
bereq, beresp cty.Value
27+
tokenRequest bool
2328
}
2429

2530
// Set finalized cty req/resp pair.
2631
func (sv *SyncedVariables) Set(beresp *http.Response) {
27-
name, bereqV, berespV := newBerespValues(beresp.Request.Context(), true, beresp)
32+
ctx := beresp.Request.Context()
33+
name, bereqV, berespV := newBerespValues(ctx, true, beresp)
34+
35+
if tr, ok := ctx.Value(request.TokenRequest).(string); ok && tr != "" {
36+
name = TokenRequestPrefix + name
37+
}
38+
2839
sv.items.Store(name, &syncPair{
2940
name: name,
3041
bereq: bereqV,
@@ -46,12 +57,13 @@ func (sv *SyncedVariables) Sync(variables map[string]cty.Value) {
4657
if bereqs == nil {
4758
bereqs = make(map[string]cty.Value)
4859
}
49-
bereqs[p.name] = p.bereq
60+
name := key.(string)
61+
bereqs[name] = p.bereq
5062

5163
if beresps == nil {
5264
beresps = make(map[string]cty.Value)
5365
}
54-
beresps[p.name] = p.beresp
66+
beresps[name] = p.beresp
5567

5668
return true
5769
})

handler/endpoint.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func (e *Endpoint) produce(req *http.Request) (producer.ResultMap, error) {
225225
close(tripCh)
226226

227227
for resultCh := range tripCh {
228-
e.readResults(resultCh, results)
228+
e.readResults(req.Context(), resultCh, results)
229229
}
230230

231231
var err error // TODO: prefer default resp err
@@ -240,10 +240,12 @@ func (e *Endpoint) produce(req *http.Request) (producer.ResultMap, error) {
240240
return results, err
241241
}
242242

243-
func (e *Endpoint) readResults(requestResults producer.Results, beresps producer.ResultMap) {
243+
func (e *Endpoint) readResults(ctx context.Context, requestResults producer.Results, beresps producer.ResultMap) {
244244
i := 0
245245
for {
246246
select {
247+
case <-ctx.Done():
248+
return
247249
case r, more := <-requestResults:
248250
if !more {
249251
return

handler/transport/backend.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ func (b *Backend) RoundTrip(req *http.Request) (*http.Response, error) {
187187
}
188188

189189
if !eval.IsUpgradeResponse(req, beresp) {
190+
beresp.Body = logging.NewBytesCountReader(beresp)
190191
if err = setGzipReader(beresp); err != nil {
191192
b.upstreamLog.LogEntry().WithContext(req.Context()).WithError(err).Error()
192193
}
@@ -373,18 +374,20 @@ func (b *Backend) withTimeout(req *http.Request, conf *Config) <-chan error {
373374
return
374375
}
375376

376-
go func(done <-chan struct{}) {
377+
go func(c context.Context, timeoutCh chan time.Time) {
377378
ttfbTimer.Reset(conf.TTFBTimeout)
378379
select {
379-
case <-done:
380-
if !ttfbTimer.Stop() {
381-
<-ttfbTimer.C
380+
case <-c.Done():
381+
ttfbTimer.Stop()
382+
select {
383+
case <-ttfbTimer.C:
384+
default:
382385
}
383-
return
384-
case ttfbTimeout <- <-ttfbTimer.C:
386+
case t := <-ttfbTimer.C:
387+
// buffered, no select done required
388+
timeoutCh <- t
385389
}
386-
387-
}(req.Context().Done())
390+
}(ctx, ttfbTimeout)
388391
},
389392
GotFirstResponseByte: func() {
390393
if downstreamTrace != nil && downstreamTrace.GotFirstResponseByte != nil {
@@ -396,7 +399,7 @@ func (b *Backend) withTimeout(req *http.Request, conf *Config) <-chan error {
396399

397400
*req = *req.WithContext(httptrace.WithClientTrace(ctx, ctxTrace))
398401

399-
go func(cancelFn func(), c context.Context, ec chan error) {
402+
go func(c context.Context, cancelFn func(), ec chan error) {
400403
defer cancelFn()
401404
deadline := make(<-chan time.Time)
402405
if timeout > 0 {
@@ -415,7 +418,7 @@ func (b *Backend) withTimeout(req *http.Request, conf *Config) <-chan error {
415418
case <-c.Done():
416419
return
417420
}
418-
}(cancel, ctx, errCh)
421+
}(ctx, cancel, errCh)
419422
return errCh
420423
}
421424

logging/bytes_reader.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package logging
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"sync/atomic"
8+
9+
"github.com/avenga/couper/config/request"
10+
)
11+
12+
var _ io.ReadCloser = &BytesCountReader{}
13+
14+
type BytesCountReader struct {
15+
c context.Context
16+
n int64
17+
r io.ReadCloser
18+
}
19+
20+
// NewBytesCountReader just counts the raw read bytes from given response body for logging purposes.
21+
func NewBytesCountReader(beresp *http.Response) io.ReadCloser {
22+
return &BytesCountReader{
23+
c: beresp.Request.Context(),
24+
r: beresp.Body,
25+
}
26+
}
27+
28+
func (b *BytesCountReader) Read(p []byte) (n int, err error) {
29+
n, err = b.r.Read(p)
30+
b.n += int64(n)
31+
return n, err
32+
}
33+
34+
func (b *BytesCountReader) Close() error {
35+
bytesPtr, ok := b.c.Value(request.BackendBytes).(*int64)
36+
if ok {
37+
atomic.StoreInt64(bytesPtr, b.n)
38+
}
39+
return b.r.Close()
40+
}

logging/hooks/context.go

+14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package hooks
22

33
import (
4+
"sync/atomic"
5+
46
"github.com/sirupsen/logrus"
57

68
"github.com/avenga/couper/config/request"
9+
"github.com/avenga/couper/logging"
710
)
811

912
var _ logrus.Hook = &Context{}
@@ -21,5 +24,16 @@ func (c *Context) Fire(entry *logrus.Entry) error {
2124
entry.Data["uid"] = uid
2225
}
2326
}
27+
28+
if field, ok := entry.Data["type"]; ok && field == beTypeField {
29+
if bytes, i := entry.Context.Value(request.BackendBytes).(*int64); i {
30+
response, r := entry.Data["response"].(logging.Fields)
31+
b := atomic.LoadInt64(bytes)
32+
if r && b > 0 {
33+
response["bytes"] = b
34+
}
35+
}
36+
}
37+
2438
return nil
2539
}

logging/hooks/custom_logs.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,21 @@ func fireUpstream(entry *logrus.Entry) {
102102
// syncedUpstreamContext prepares the local backend variable.
103103
func syncedUpstreamContext(evalCtx *eval.Context, entry *logrus.Entry) *hcl.EvalContext {
104104
ctx := evalCtx.HCLContextSync()
105+
106+
tr, _ := entry.Context.Value(request.TokenRequest).(string)
107+
rtName, _ := entry.Context.Value(request.RoundTripName).(string)
108+
isTr := tr != ""
109+
110+
if rtName == "" {
111+
return ctx
112+
}
113+
105114
if _, ok := ctx.Variables[eval.BackendRequests]; ok {
106115
for k, v := range ctx.Variables[eval.BackendRequests].AsValueMap() {
107-
if k == entry.Context.Value(request.RoundTripName) {
116+
if isTr && k == eval.TokenRequestPrefix+rtName {
117+
ctx.Variables[eval.BackendRequest] = v
118+
break
119+
} else if k == rtName {
108120
ctx.Variables[eval.BackendRequest] = v
109121
break
110122
}
@@ -113,7 +125,10 @@ func syncedUpstreamContext(evalCtx *eval.Context, entry *logrus.Entry) *hcl.Eval
113125

114126
if _, ok := ctx.Variables[eval.BackendResponses]; ok {
115127
for k, v := range ctx.Variables[eval.BackendResponses].AsValueMap() {
116-
if k == entry.Context.Value(request.RoundTripName) {
128+
if isTr && k == eval.TokenRequestPrefix+rtName {
129+
ctx.Variables[eval.BackendResponse] = v
130+
break
131+
} else if k == rtName {
117132
ctx.Variables[eval.BackendResponse] = v
118133
break
119134
}

logging/stack.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package logging
2+
3+
import (
4+
"context"
5+
"sync"
6+
7+
"github.com/sirupsen/logrus"
8+
)
9+
10+
type entry struct {
11+
logEntry *logrus.Entry
12+
}
13+
14+
func (e *entry) Level(lvl logrus.Level) {
15+
e.logEntry.Level = lvl
16+
}
17+
18+
type Level interface {
19+
Level(level logrus.Level)
20+
}
21+
22+
type Stack struct {
23+
entries []*entry
24+
mu sync.Mutex
25+
}
26+
27+
const logStack = "logStack"
28+
29+
func NewStack(ctx context.Context) (context.Context, *Stack) {
30+
s := &Stack{}
31+
return context.WithValue(ctx, logStack, s), s
32+
}
33+
34+
func (s *Stack) Push(e *logrus.Entry) Level {
35+
s.mu.Lock()
36+
defer s.mu.Unlock()
37+
38+
item := &entry{logEntry: e}
39+
s.entries = append(s.entries, item)
40+
return item
41+
}
42+
43+
func (s *Stack) Fire() {
44+
s.mu.Lock()
45+
defer s.mu.Unlock()
46+
47+
for _, item := range s.entries {
48+
item.logEntry.Log(item.logEntry.Level)
49+
}
50+
}
51+
52+
func FromContext(ctx context.Context) (*Stack, bool) {
53+
s, exist := ctx.Value(logStack).(*Stack)
54+
return s, exist
55+
}

logging/upstream_log.go

+25-5
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ func (u *UpstreamLog) RoundTrip(req *http.Request) (*http.Response, error) {
7474

7575
fields["request"] = requestFields
7676

77+
berespBytes := int64(0)
7778
logCtxCh := make(chan hcl.Body, 10)
7879
outctx := context.WithValue(req.Context(), request.LogCustomUpstream, logCtxCh)
80+
outctx = context.WithValue(outctx, request.BackendBytes, &berespBytes)
7981
oCtx, openAPIContext := validation.NewWithContext(outctx)
8082
outreq := req.WithContext(httptrace.WithClientTrace(oCtx, clientTrace))
8183

@@ -116,15 +118,20 @@ func (u *UpstreamLog) RoundTrip(req *http.Request) (*http.Response, error) {
116118
if tr, ok := outreq.Context().Value(request.TokenRequest).(string); ok && tr != "" {
117119
fields["token_request"] = tr
118120

119-
if retries, ok := outreq.Context().Value(request.TokenRequestRetries).(uint8); ok && retries > 0 {
121+
if retries, exist := outreq.Context().Value(request.TokenRequestRetries).(uint8); exist && retries > 0 {
120122
fields["token_request_retry"] = retries
121123
}
122124
}
123125

124126
fields["status"] = 0
125127
if beresp != nil {
126128
fields["status"] = beresp.StatusCode
129+
cl := int64(0)
130+
if beresp.ContentLength > 0 {
131+
cl = beresp.ContentLength
132+
}
127133
responseFields := Fields{
134+
"bytes": cl,
128135
"headers": filterHeader(u.config.ResponseHeaders, beresp.Header),
129136
"status": beresp.StatusCode,
130137
}
@@ -146,16 +153,29 @@ func (u *UpstreamLog) RoundTrip(req *http.Request) (*http.Response, error) {
146153
fields["timings"] = timingResults
147154
//timings["ttlb"] = roundMS(rtDone.Sub(timeTTFB)) // TODO: depends on stream or buffer
148155

149-
entry := u.log.WithFields(logrus.Fields(fields)).WithContext(outreq.Context())
150-
entry.Time = startTime
156+
entry := u.log.
157+
WithFields(logrus.Fields(fields)).
158+
WithContext(outreq.Context()).
159+
WithTime(startTime)
160+
161+
stack, stacked := FromContext(outreq.Context())
151162

152163
if err != nil {
153164
if _, ok := err.(errors.GoError); !ok {
154165
err = errors.Backend.With(err)
155166
}
156-
entry.WithError(err).Error()
167+
entry = entry.WithError(err)
168+
if stacked {
169+
stack.Push(entry).Level(logrus.ErrorLevel)
170+
} else {
171+
entry.Error()
172+
}
157173
} else {
158-
entry.Info()
174+
if stacked {
175+
stack.Push(entry).Level(logrus.InfoLevel)
176+
} else {
177+
entry.Info()
178+
}
159179
}
160180

161181
return beresp, err

0 commit comments

Comments
 (0)