Repository: avenga/couper
Pull request #411 (2021-12-22 15:41:23)

Add logging for endpoint sequence items
Log previous sequence item name

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/config/configload/endpoint.go b/config/configload/endpoint.go
index 01058cdc..8a8fe324 100644
--- a/config/configload/endpoint.go
+++ b/config/configload/endpoint.go
@@ -155,11 +155,11 @@ func refineEndpoints(definedBackends Backends, endpoints config.Endpoints, check
 			}
 		}
 
-		names := map[string]struct{}{}
+		names := map[string]hcl.Body{}
 		unique := map[string]struct{}{}
 		itemRange := endpoint.Remain.MissingItemRange()
 		for _, p := range endpoint.Proxies {
-			names[p.Name] = struct{}{}
+			names[p.Name] = p.Remain
 
 			if err := validLabelName(p.Name, &itemRange); err != nil {
 				return err
@@ -173,7 +173,7 @@ func refineEndpoints(definedBackends Backends, endpoints config.Endpoints, check
 		}
 
 		for _, r := range endpoint.Requests {
-			names[r.Name] = struct{}{}
+			names[r.Name] = r.Remain
 
 			if err = validLabelName(r.Name, &itemRange); err != nil {
 				return err
@@ -194,10 +194,12 @@ func refineEndpoints(definedBackends Backends, endpoints config.Endpoints, check
 			}}
 		}
 
-		addSequenceDeps(names, endpoint)
+		if err = buildSequences(names, endpoint); err != nil {
+			return err
+		}
 
 		epErrorHandler := collect.ErrorHandlerSetters(endpoint)
-		if err := configureErrorHandler(epErrorHandler, definedBackends); err != nil {
+		if err = configureErrorHandler(epErrorHandler, definedBackends); err != nil {
 			return err
 		}
 	}
diff --git a/config/configload/endpoint_sequence.go b/config/configload/endpoint_sequence.go
index bb8ca7a0..0109af2c 100644
--- a/config/configload/endpoint_sequence.go
+++ b/config/configload/endpoint_sequence.go
@@ -1,51 +1,88 @@
 package configload
 
 import (
+	"sort"
+
 	"github.com/hashicorp/hcl/v2"
 
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/body"
 )
 
-// addSequenceDeps collects possible dependencies from variables.
-func addSequenceDeps(names map[string]struct{}, endpoint *config.Endpoint) {
-	var items []config.SequenceItem
-	for _, conf := range endpoint.Proxies {
-		items = append(items, conf)
+// buildSequences collects possible dependencies from 'backend_responses' variable.
+func buildSequences(names map[string]hcl.Body, endpoint *config.Endpoint) (err error) {
+	sequences := map[string]*config.Sequence{}
+
+	defer func() {
+		if rc := recover(); rc != nil {
+			err = rc.(error)
+		}
+	}()
+
+	var sortedNames sort.StringSlice
+	for name := range names {
+		sortedNames = append(sortedNames, name)
+	}
+	sortedNames.Sort()
+
+	for _, name := range sortedNames {
+		b := names[name]
+		refs := responseReferences(b)
+
+		if len(refs) == 0 {
+			continue
+		}
+
+		seq, exist := sequences[name]
+		if !exist {
+			seq = &config.Sequence{Name: name, BodyRange: b.MissingItemRange()}
+			sequences[name] = seq
+		}
+
+		for _, r := range refs {
+			ref, ok := sequences[r]
+			if !ok {
+				ref = &config.Sequence{Name: r, BodyRange: b.MissingItemRange()}
+				sequences[r] = ref
+			}
+			// Do not add ourselves
+			// Use case: modify response headers with current response
+			if seq != ref {
+				seq.Add(ref)
+			}
+		}
 	}
-	for _, conf := range endpoint.Requests {
-		items = append(items, conf)
+
+	for _, s := range sequences {
+		if !s.HasParent() {
+			endpoint.Sequences = append(endpoint.Sequences, s)
+		}
 	}
 
-	for _, seqItem := range items {
-		allExpressions := body.CollectExpressions(seqItem.HCLBody())
-		for _, expr := range allExpressions {
-			for _, traversal := range expr.Variables() {
-				if traversal.RootName() != "backend_responses" || len(traversal) < 2 {
-					continue
-				}
-
-				// do we have a ref ?
-				for _, t := range traversal[1:] {
-					tr, ok := t.(hcl.TraverseAttr)
-					if !ok {
-						continue
-					}
-
-					_, ok = names[tr.Name]
-					if !ok {
-						continue
-					}
-
-					for _, i := range items {
-						if i.GetName() != tr.Name || i == seqItem {
-							continue
-						}
-						seqItem.Add(i)
-						break
-					}
-				}
+	return err
+}
+
+func responseReferences(b hcl.Body) []string {
+	var result []string
+	unique := map[string]struct{}{}
+
+	for _, expr := range body.CollectExpressions(b) {
+		for _, traversal := range expr.Variables() {
+			if traversal.RootName() != "backend_responses" || len(traversal) < 2 {
+				continue
+			}
+
+			tr, ok := traversal[1].(hcl.TraverseAttr)
+			if !ok {
+				continue
+			}
+
+			if _, ok = unique[tr.Name]; !ok {
+				unique[tr.Name] = struct{}{}
+				result = append(result, tr.Name)
 			}
 		}
 	}
+
+	return result
 }
diff --git a/config/endpoint.go b/config/endpoint.go
index e96c52b8..d0f5b0aa 100644
--- a/config/endpoint.go
+++ b/config/endpoint.go
@@ -23,8 +23,9 @@ type Endpoint struct {
 	Scope                cty.Value `hcl:"beta_scope,optional"`
 
 	// internally configured due to multi-label options
-	Proxies  Proxies
-	Requests Requests
+	Proxies   Proxies
+	Requests  Requests
+	Sequences Sequences
 }
 
 // Endpoints represents a list of <Endpoint> objects.
diff --git a/config/proxy.go b/config/proxy.go
index 09567721..a7afc6af 100644
--- a/config/proxy.go
+++ b/config/proxy.go
@@ -10,7 +10,6 @@ import (
 var (
 	_ BackendReference = &Proxy{}
 	_ Inline           = &Proxy{}
-	_ SequenceItem     = &Proxy{}
 )
 
 // Proxy represents the <Proxy> object.
@@ -22,7 +21,6 @@ type Proxy struct {
 
 	// internally used
 	Backend hcl.Body
-	depends []SequenceItem
 }
 
 // Proxies represents a list of <Proxy> objects.
@@ -87,15 +85,3 @@ func (p Proxy) Schema(inline bool) *hcl.BodySchema {
 
 	return meta.SchemaWithAttributes(schema)
 }
-
-func (p *Proxy) Add(item SequenceItem) {
-	p.depends = append(p.depends, item)
-}
-
-func (p *Proxy) Deps() []SequenceItem {
-	return p.depends[:]
-}
-
-func (p *Proxy) GetName() string {
-	return p.Name
-}
diff --git a/config/request.go b/config/request.go
index 814059ac..40ca709b 100644
--- a/config/request.go
+++ b/config/request.go
@@ -9,7 +9,6 @@ import (
 var (
 	_ BackendReference = &Request{}
 	_ Inline           = &Request{}
-	_ SequenceItem     = &Request{}
 )
 
 // Request represents the <Request> object.
@@ -20,7 +19,6 @@ type Request struct {
 
 	// Internally used
 	Backend hcl.Body
-	depends []SequenceItem
 }
 
 // Requests represents a list of <Requests> objects.
@@ -69,15 +67,3 @@ func (r Request) Schema(inline bool) *hcl.BodySchema {
 
 	return newBackendSchema(schema, r.HCLBody())
 }
-
-func (r *Request) Add(item SequenceItem) {
-	r.depends = append(r.depends, item)
-}
-
-func (r *Request) Deps() []SequenceItem {
-	return r.depends[:]
-}
-
-func (r *Request) GetName() string {
-	return r.Name
-}
diff --git a/config/request/context_key.go b/config/request/context_key.go
index f1934c3d..72ab8a31 100644
--- a/config/request/context_key.go
+++ b/config/request/context_key.go
@@ -12,6 +12,7 @@ const (
 	Endpoint
 	EndpointExpectedStatus
 	EndpointKind
+	EndpointSequenceDependsOn
 	Error
 	Handler
 	LogCustomAccess
diff --git a/config/runtime/endpoint.go b/config/runtime/endpoint.go
index 30138801..82b18c94 100644
--- a/config/runtime/endpoint.go
+++ b/config/runtime/endpoint.go
@@ -2,21 +2,19 @@ package runtime
 
 import (
 	"fmt"
-	"reflect"
-	"sort"
 	"strings"
 
-	"github.com/avenga/couper/cache"
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
 	"github.com/sirupsen/logrus"
 
+	"github.com/avenga/couper/cache"
 	"github.com/avenga/couper/config"
 	"github.com/avenga/couper/config/runtime/server"
 	"github.com/avenga/couper/errors"
 	"github.com/avenga/couper/eval"
 	"github.com/avenga/couper/handler"
 	"github.com/avenga/couper/handler/producer"
-	"github.com/hashicorp/hcl/v2"
-	"github.com/hashicorp/hcl/v2/gohcl"
 )
 
 func newEndpointMap(srvConf *config.Server, serverOptions *server.Options) (endpointMap, error) {
@@ -92,7 +90,6 @@ func newEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint,
 		blockBodies = append(blockBodies, response.Context)
 	}
 
-	var items []config.SequenceItem
 	allProxies := make(map[string]*producer.Proxy)
 	for _, proxyConf := range endpointConf.Proxies {
 		backend, innerBody, berr := newBackend(confCtx, proxyConf.Backend, log, proxyEnv, memStore)
@@ -106,7 +103,6 @@ func newEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint,
 		}
 
 		allProxies[proxyConf.Name] = p
-		items = append(items, proxyConf)
 		blockBodies = append(blockBodies, proxyConf.Backend, innerBody, proxyConf.HCLBody())
 	}
 
@@ -124,15 +120,10 @@ func newEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint,
 		}
 
 		allRequests[requestConf.Name] = pr
-		items = append(items, requestConf)
 		blockBodies = append(blockBodies, requestConf.Backend, innerBody, requestConf.HCLBody())
 	}
 
-	errCh := make(chan error, 1)
-	sequences, requests, proxies := newSequences(allProxies, allRequests, errCh, items...)
-	if err := <-errCh; err != nil {
-		return nil, err
-	}
+	sequences, requests, proxies := newSequences(allProxies, allRequests, endpointConf.Sequences...)
 
 	backendConf := *DefaultBackendConf
 	if diags := gohcl.DecodeBody(endpointConf.Remain, confCtx, &backendConf); diags.HasErrors() {
@@ -183,21 +174,10 @@ func newEndpointOptions(confCtx *hcl.EvalContext, endpointConf *config.Endpoint,
 
 // newSequences lookups any request related dependency and sort them into a sequence.
 // Also return left-overs for parallel usage.
-func newSequences(proxies map[string]*producer.Proxy, requests map[string]*producer.Request, errCh chan<- error,
-	items ...config.SequenceItem) (producer.Sequences, producer.Requests, producer.Proxies) {
-
-	defer func() {
-		if rc := recover(); rc != nil {
-			errCh <- rc.(error)
-		}
-		close(errCh)
-	}()
-
-	// start with default item
-	sort.SliceStable(items, func(i, j int) bool {
-		return items[i].GetName() == "default"
-	})
+func newSequences(proxies map[string]*producer.Proxy, requests map[string]*producer.Request,
+	items ...*config.Sequence) (producer.Sequences, producer.Requests, producer.Proxies) {
 
+	// just collect for filtering
 	var allDeps [][]string
 	for _, item := range items {
 		deps := make([]string, 0)
@@ -206,50 +186,13 @@ func newSequences(proxies map[string]*producer.Proxy, requests map[string]*produ
 		allDeps = append(allDeps, deps)
 	}
 
-	// part of other chain? filter out
-	allDeps = func() (result [][]string) {
-	all:
-		for _, deps := range allDeps {
-			if len(deps) == 1 {
-				continue
-			}
-
-			for _, otherDeps := range allDeps {
-				if len(deps) >= len(otherDeps) {
-					continue
-				}
-				if reflect.DeepEqual(deps, otherDeps[:len(deps)]) {
-					continue all
-				}
-			}
-			result = append(result, deps)
-		}
-
-		return result
-	}()
-
 	var reqs producer.Requests
 	var ps producer.Proxies
 	var seqs producer.Sequences
 
-	for _, deps := range allDeps {
-		var seq producer.Sequence
-		for _, dep := range deps {
-			if p, ok := proxies[dep]; ok {
-				seq = append(seq, &producer.SequenceItem{
-					Backend: p.RoundTrip,
-					Name:    p.Name,
-				})
-			}
-			if r, ok := requests[dep]; ok {
-				seq = append(seq, &producer.SequenceItem{
-					Backend: r.Backend,
-					Context: r.Context,
-					Name:    r.Name,
-				})
-			}
-		}
-		seqs = append(seqs, seq)
+	// read from prepared config sequences
+	for _, seq := range items {
+		seqs = append(seqs, newSequence(seq, proxies, requests))
 	}
 
 proxyLeftovers:
@@ -279,28 +222,65 @@ reqLeftovers:
 	return seqs, reqs, ps
 }
 
-func resolveSequence(item config.SequenceItem, resolved, seen *[]string) {
-	name := item.GetName()
+func newSequence(seq *config.Sequence,
+	proxies map[string]*producer.Proxy,
+	requests map[string]*producer.Request) producer.Roundtrip {
+
+	deps := seq.Deps()
+	var rt producer.Roundtrip
+
+	var previous []string
+	if len(deps) > 1 { // more deps per item can be parallelized
+		var seqs producer.Sequences
+		for _, d := range deps {
+			seqs = append(seqs, newSequence(d, proxies, requests))
+			previous = append(previous, d.Name)
+		}
+		rt = seqs
+	} else if len(deps) == 1 {
+		rt = newSequence(deps[0], proxies, requests)
+		previous = append(previous, deps[0].Name)
+	}
+
+	item := newSequenceItem(seq.Name, strings.Join(previous, ","), proxies, requests)
+	if rt != nil {
+		return producer.Sequence{rt, item}
+	}
+	return item
+}
+
+func newSequenceItem(name, previous string,
+	proxies map[string]*producer.Proxy,
+	requests map[string]*producer.Request) producer.Roundtrip {
+	if p, ok := proxies[name]; ok {
+		return producer.Proxies{
+			&producer.Proxy{
+				Name:             p.Name,
+				RoundTrip:        p.RoundTrip,
+				PreviousSequence: previous,
+			}}
+	}
+	if r, ok := requests[name]; ok {
+		return producer.Requests{
+			&producer.Request{
+				Backend:          r.Backend,
+				Context:          r.Context,
+				Name:             r.Name,
+				PreviousSequence: previous,
+			}}
+	}
+	return nil
+}
+
+func resolveSequence(item *config.Sequence, resolved, seen *[]string) {
+	name := item.Name
 	*seen = append(*seen, name)
 	for _, dep := range item.Deps() {
-		if !containsString(resolved, dep.GetName()) {
-			if !containsString(seen, dep.GetName()) {
+		if !containsString(resolved, dep.Name) {
+			if !containsString(seen, dep.Name) {
 				resolveSequence(dep, resolved, seen)
 				continue
 			}
-
-			r := hcl.Range{} // try to obtain some config context
-			if b, ok := item.(interface{ HCLBody() hcl.Body }); ok {
-				r = b.HCLBody().MissingItemRange()
-			}
-			err := &hcl.Diagnostic{
-				Detail:   "circular sequence reference: " + strings.Join(*resolved, ",") + ": " + dep.GetName(),
-				Severity: hcl.DiagError,
-				Subject:  &r,
-				Summary:  "configuration error",
-			}
-			panic(err)
-
 		}
 	}
 
diff --git a/config/sequence.go b/config/sequence.go
index 264dadc6..8e48989e 100644
--- a/config/sequence.go
+++ b/config/sequence.go
@@ -1,8 +1,90 @@
 package config
 
-type SequenceItem interface {
-	Body
-	Add(item SequenceItem)
-	Deps() []SequenceItem
-	GetName() string
+import (
+	"fmt"
+	"strings"
+
+	"github.com/hashicorp/hcl/v2"
+)
+
+type Sequences []*Sequence
+
+type Sequence struct {
+	BodyRange hcl.Range
+	Name      string
+	deps      Sequences
+	parent    *Sequence
+	seen      map[string]struct{}
+}
+
+func (s *Sequence) Add(ref *Sequence) {
+	if strings.TrimSpace(ref.Name) == "" {
+		return
+	}
+
+	ref.parent = s
+
+	if s.hasSeen(ref.Name) { // collect names to populate error message
+		refs := []string{ref.Name}
+		p := s.parent
+		for p != s {
+			p = p.parent
+			name := p.Name
+			deps := p.Deps()
+			if len(deps) > 0 {
+				name = deps[0].Name
+			}
+			refs = append(refs, name)
+		}
+
+		err := &hcl.Diagnostic{
+			Detail: fmt.Sprintf("circular sequence reference: %s",
+				strings.Join(append(refs, refs[0]), ",")),
+			Severity: hcl.DiagError,
+			Subject:  &s.BodyRange,
+			Summary:  "configuration error",
+		}
+		panic(err)
+	}
+
+	s.deps = append(s.deps, ref)
+}
+
+// Deps returns sequence dependency in reversed order since they have to be solved first.
+func (s *Sequence) Deps() Sequences {
+	if len(s.deps) < 2 {
+		return s.deps
+	}
+
+	var revert Sequences
+	for i := len(s.deps); i > 0; i-- {
+		revert = append(revert, s.deps[i-1])
+	}
+	return revert
+}
+
+func (s *Sequence) HasParent() bool {
+	return s != nil && s.parent != nil
+}
+
+func (s *Sequence) hasSeen(name string) bool {
+	if s == nil {
+		return false
+	}
+
+	if s.seen == nil {
+		s.seen = make(map[string]struct{})
+	}
+
+	if _, exist := s.seen[name]; exist {
+		return true
+	}
+
+	s.seen[name] = struct{}{}
+
+	if s.HasParent() && s.parent.hasSeen(name) {
+		return true
+	}
+
+	return false
 }
diff --git a/handler/endpoint.go b/handler/endpoint.go
index 6fdf4533..798c4e1b 100644
--- a/handler/endpoint.go
+++ b/handler/endpoint.go
@@ -42,9 +42,9 @@ type EndpointOptions struct {
 	ReqBodyLimit   int64
 	ServerOpts     *server.Options
 
-	Proxies   producer.Roundtrips
+	Proxies   producer.Roundtrip
 	Redirect  *producer.Redirect
-	Requests  producer.Roundtrips
+	Requests  producer.Roundtrip
 	Sequences producer.Sequences
 	Response  *producer.Response
 }
@@ -205,11 +205,16 @@ func (e *Endpoint) newRedirect() *http.Response {
 func (e *Endpoint) produce(req *http.Request) (producer.ResultMap, error) {
 	results := make(producer.ResultMap)
 
-	trips := []producer.Roundtrips{e.opts.Proxies, e.opts.Requests, e.opts.Sequences}
+	trips := []producer.Roundtrip{e.opts.Proxies, e.opts.Requests, e.opts.Sequences}
 	tripCh := make(chan chan *producer.Result, len(trips))
 	for _, trip := range trips {
+		// use-case: just a response block within an endpoint
+		if trip == nil {
+			continue
+		}
+
 		resultCh := make(chan *producer.Result, trip.Len())
-		go func(rt producer.Roundtrips, rc chan *producer.Result) {
+		go func(rt producer.Roundtrip, rc chan *producer.Result) {
 			rt.Produce(req, resultCh)
 			close(rc)
 		}(trip, resultCh)
diff --git a/handler/producer/produce.go b/handler/producer/produce.go
index b2796350..64a17f69 100644
--- a/handler/producer/produce.go
+++ b/handler/producer/produce.go
@@ -5,13 +5,13 @@ import (
 )
 
 var (
-	_ Roundtrips = Proxies{}
-	_ Roundtrips = Requests{}
-	_ Roundtrips = Sequences{}
-	_ Roundtrips = Sequence{}
+	_ Roundtrip = Proxies{}
+	_ Roundtrip = Requests{}
+	_ Roundtrip = Sequences{}
+	_ Roundtrip = Sequence{}
 )
 
-type Roundtrips interface {
+type Roundtrip interface {
 	Produce(req *http.Request, results chan<- *Result)
 	Len() int
 }
diff --git a/handler/producer/proxy.go b/handler/producer/proxy.go
index 180f67fd..5b52b8b9 100644
--- a/handler/producer/proxy.go
+++ b/handler/producer/proxy.go
@@ -14,8 +14,9 @@ import (
 )
 
 type Proxy struct {
-	Name      string // label
-	RoundTrip http.RoundTripper
+	Name             string // label
+	PreviousSequence string
+	RoundTrip        http.RoundTripper
 }
 
 type Proxies []*Proxy
@@ -45,6 +46,9 @@ func (pr Proxies) Produce(clientReq *http.Request, results chan<- *Result) {
 		currentName = proxy.Name
 		outCtx := withRoundTripName(ctx, proxy.Name)
 		outCtx = context.WithValue(outCtx, request.RoundTripProxy, true)
+		if proxy.PreviousSequence != "" {
+			outCtx = context.WithValue(outCtx, request.EndpointSequenceDependsOn, proxy.PreviousSequence)
+		}
 
 		// span end by result reader
 		outCtx, _ = telemetry.NewSpanFromContext(outCtx, proxy.Name, trace.WithSpanKind(trace.SpanKindServer))
diff --git a/handler/producer/request.go b/handler/producer/request.go
index dadc06b0..7bf784bb 100644
--- a/handler/producer/request.go
+++ b/handler/producer/request.go
@@ -21,9 +21,10 @@ import (
 
 // Request represents the producer <Request> object.
 type Request struct {
-	Backend http.RoundTripper
-	Context hcl.Body
-	Name    string // label
+	Backend          http.RoundTripper
+	Context          hcl.Body
+	Name             string // label
+	PreviousSequence string
 }
 
 // Requests represents the producer <Requests> object.
@@ -55,6 +56,9 @@ func (r Requests) Produce(req *http.Request, results chan<- *Result) {
 	for _, or := range r {
 		// span end by result reader
 		outCtx, span := telemetry.NewSpanFromContext(withRoundTripName(ctx, or.Name), or.Name, trace.WithSpanKind(trace.SpanKindClient))
+		if or.PreviousSequence != "" {
+			outCtx = context.WithValue(outCtx, request.EndpointSequenceDependsOn, or.PreviousSequence)
+		}
 
 		bodyContent, _, diags := or.Context.PartialContent(config.Request{Remain: or.Context}.Schema(true))
 		if diags.HasErrors() {
diff --git a/handler/producer/sequence.go b/handler/producer/sequence.go
index 992f7144..aa4a50f2 100644
--- a/handler/producer/sequence.go
+++ b/handler/producer/sequence.go
@@ -17,10 +17,10 @@ type SequenceItem struct {
 }
 
 // Sequence represents a list of serialized items.
-type Sequence []*SequenceItem
+type Sequence []Roundtrip
 
 // Sequences holds several list of serialized items.
-type Sequences []Sequence
+type Sequences []Roundtrip
 
 func (seqs Sequences) Produce(req *http.Request, results chan<- *Result) {
 	var rootSpan trace.Span
@@ -32,7 +32,8 @@ func (seqs Sequences) Produce(req *http.Request, results chan<- *Result) {
 	resultsCh := make(chan *Result, seqs.Len())
 
 	for _, s := range seqs {
-		go s.Produce(req, resultsCh)
+		outreq := req.WithContext(ctx)
+		go s.Produce(outreq, resultsCh)
 	}
 
 	for i := 0; i < seqs.Len(); i++ {
@@ -52,7 +53,8 @@ func (s Sequence) Produce(req *http.Request, results chan<- *Result) {
 	var rootSpan trace.Span
 
 	ctx := req.Context()
-	if len(s) > 0 {
+	l := s.Len()
+	if l > 0 {
 		ctx, rootSpan = telemetry.NewSpanFromContext(ctx, "sequence", trace.WithSpanKind(trace.SpanKindProducer))
 	}
 	defer func() {
@@ -61,25 +63,17 @@ func (s Sequence) Produce(req *http.Request, results chan<- *Result) {
 		}
 	}()
 
-	result := make(chan *Result, 1)
+	result := make(chan *Result, l)
 
 	var lastResult *Result
 	var lastBeresps []*http.Response
 	var moreEntries bool
 	for _, seq := range s {
-		outreq := req.WithContext(ctx)
+		outCtx := ctx
 
-		if seq.Context == nil {
-			Proxies{&Proxy{Name: seq.Name,
-				RoundTrip: seq.Backend,
-			}}.Produce(outreq, result)
-		} else {
-			Requests{&Request{
-				Backend: seq.Backend,
-				Context: seq.Context,
-				Name:    seq.Name,
-			}}.Produce(outreq, result)
-		}
+		outreq := req.WithContext(outCtx)
+
+		seq.Produce(outreq, result)
 
 		select {
 		case <-outreq.Context().Done():
@@ -109,8 +103,9 @@ func (s Sequence) Produce(req *http.Request, results chan<- *Result) {
 }
 
 func (s Sequence) Len() int {
-	if len(s) > 0 {
-		return 1
+	var sum int
+	for _, t := range s {
+		sum += t.Len()
 	}
-	return 0
+	return sum
 }
diff --git a/logging/upstream_log.go b/logging/upstream_log.go
index a6033efd..a9821784 100644
--- a/logging/upstream_log.go
+++ b/logging/upstream_log.go
@@ -46,6 +46,10 @@ func (u *UpstreamLog) RoundTrip(req *http.Request) (*http.Response, error) {
 		"method": req.Method,
 	}
 
+	if depOn, ok := req.Context().Value(request.EndpointSequenceDependsOn).(string); ok && depOn != "" {
+		fields["depends_on"] = depOn
+	}
+
 	if u.config.TypeFieldKey != "" {
 		fields["type"] = u.config.TypeFieldKey
 	}


Test patch:
diff --git a/handler/producer/request_test.go b/handler/producer/request_test.go
index a53883a0..1d3c3f9f 100644
--- a/handler/producer/request_test.go
+++ b/handler/producer/request_test.go
@@ -98,7 +98,7 @@ func Test_ProduceExpectedStatus(t *testing.T) {
 
 		testNames := []string{"request", "proxy"}
 
-		for i, rt := range []producer.Roundtrips{requests, proxies} {
+		for i, rt := range []producer.Roundtrip{requests, proxies} {
 			t.Run(testNames[i]+"_"+tt.name, func(t *testing.T) {
 
 				ctx := eval.NewContext(nil, nil).WithClientRequest(clientRequest)
diff --git a/server/http_endpoints_test.go b/server/http_endpoints_test.go
index 38ba37df..36384d14 100644
--- a/server/http_endpoints_test.go
+++ b/server/http_endpoints_test.go
@@ -9,7 +9,7 @@ import (
 	"io"
 	"net/http"
 	"net/http/httptest"
-	"path"
+	"path/filepath"
 	"reflect"
 	"strconv"
 	"strings"
@@ -17,7 +17,9 @@ import (
 	"time"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/hashicorp/hcl/v2"
 
+	"github.com/avenga/couper/config/configload"
 	"github.com/avenga/couper/internal/test"
 	"github.com/avenga/couper/logging"
 )
@@ -81,7 +83,7 @@ func TestEndpoints_ProxyReqRes(t *testing.T) {
 	client := newClient()
 	helper := test.New(t)
 
-	shutdown, logHook := newCouper(path.Join(testdataPath, "01_couper.hcl"), helper)
+	shutdown, logHook := newCouper(filepath.Join(testdataPath, "01_couper.hcl"), helper)
 	defer shutdown()
 
 	defer func() {
@@ -123,7 +125,7 @@ func TestEndpoints_BerespBody(t *testing.T) {
 	client := newClient()
 	helper := test.New(t)
 
-	shutdown, logHook := newCouper(path.Join(testdataPath, "08_couper.hcl"), helper)
+	shutdown, logHook := newCouper(filepath.Join(testdataPath, "08_couper.hcl"), helper)
 	defer shutdown()
 
 	defer func() {
@@ -162,7 +164,7 @@ func TestEndpoints_ReqBody(t *testing.T) {
 	client := newClient()
 	helper := test.New(t)
 
-	shutdown, logHook := newCouper(path.Join(testdataPath, "08_couper.hcl"), helper)
+	shutdown, logHook := newCouper(filepath.Join(testdataPath, "08_couper.hcl"), helper)
 	defer shutdown()
 
 	defer func() {
@@ -210,7 +212,7 @@ func TestEndpoints_ProxyReqResCancel(t *testing.T) {
 	client := newClient()
 	helper := test.New(t)
 
-	shutdown, logHook := newCouper(path.Join(testdataPath, "01_couper.hcl"), helper)
+	shutdown, logHook := newCouper(filepath.Join(testdataPath, "01_couper.hcl"), helper)
 	defer shutdown()
 
 	defer func() {
@@ -251,7 +253,7 @@ func TestEndpoints_RequestLimit(t *testing.T) {
 	client := newClient()
 	helper := test.New(t)
 
-	shutdown, _ := newCouper(path.Join(testdataPath, "06_couper.hcl"), helper)
+	shutdown, _ := newCouper(filepath.Join(testdataPath, "06_couper.hcl"), helper)
 	defer shutdown()
 
 	body := strings.NewReader(`{"foo" = "bar"}`)
@@ -273,7 +275,7 @@ func TestEndpoints_Res(t *testing.T) {
 	client := newClient()
 	helper := test.New(t)
 
-	shutdown, logHook := newCouper(path.Join(testdataPath, "02_couper.hcl"), helper)
+	shutdown, logHook := newCouper(filepath.Join(testdataPath, "02_couper.hcl"), helper)
 	defer shutdown()
 
 	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/v1", nil)
@@ -306,7 +308,7 @@ func TestEndpoints_UpstreamBasicAuthAndXFF(t *testing.T) {
 	client := newClient()
 	helper := test.New(t)
 
-	shutdown, _ := newCouper(path.Join(testdataPath, "03_couper.hcl"), helper)
+	shutdown, _ := newCouper(filepath.Join(testdataPath, "03_couper.hcl"), helper)
 	defer shutdown()
 
 	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/anything", nil)
@@ -348,7 +350,7 @@ func TestEndpoints_Muxing(t *testing.T) {
 	client := newClient()
 	helper := test.New(t)
 
-	shutdown, _ := newCouper(path.Join(testdataPath, "05_couper.hcl"), helper)
+	shutdown, _ := newCouper(filepath.Join(testdataPath, "05_couper.hcl"), helper)
 	defer shutdown()
 
 	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/v1", nil)
@@ -370,7 +372,7 @@ func TestEndpoints_DoNotExecuteResponseOnErrors(t *testing.T) {
 	client := newClient()
 	helper := test.New(t)
 
-	shutdown, _ := newCouper(path.Join(testdataPath, "09_couper.hcl"), helper)
+	shutdown, _ := newCouper(filepath.Join(testdataPath, "09_couper.hcl"), helper)
 	defer shutdown()
 
 	req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil)
@@ -405,7 +407,7 @@ func TestEndpoints_DoNotExecuteResponseOnErrors(t *testing.T) {
 func TestHTTPServer_NoGzipForSmallContent(t *testing.T) {
 	client := newClient()
 
-	confPath := path.Join("testdata/endpoints/10_couper.hcl")
+	confPath := filepath.Join("testdata/endpoints/10_couper.hcl")
 	shutdown, _ := newCouper(confPath, test.New(t))
 	defer shutdown()
 
@@ -447,7 +449,7 @@ func TestEndpointSequence(t *testing.T) {
 	client := test.NewHTTPClient()
 	helper := test.New(t)
 
-	shutdown, _ := newCouper(path.Join(testdataPath, "11_couper.hcl"), helper)
+	shutdown, hook := newCouper(filepath.Join(testdataPath, "11_couper.hcl"), helper)
 	defer shutdown()
 
 	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -461,21 +463,37 @@ func TestEndpointSequence(t *testing.T) {
 	}))
 	defer origin.Close()
 
+	type log map[string]string
+
 	type testcase struct {
 		name           string
 		path           string
 		expectedHeader test.Header
 		expectedBody   string
+		expectedLog    log
 	}
 
 	for _, tc := range []testcase{
-		{"simple request sequence", "/simple", test.Header{"x": "my-value"}, `{"value":"my-value"}`},
-		{"simple request/proxy sequence", "/simple-proxy", test.Header{"x": "my-value", "y": `{"value":"my-proxy-value"}`}, ""},
-		{"simple proxy/request sequence", "/simple-proxy-named", test.Header{"x": "my-value"}, ""},
-		{"complex request/proxy sequence", "/complex-proxy", test.Header{"x": "my-value"}, ""},
-		{"complex request/proxy sequences", "/parallel-complex-proxy", test.Header{"x": "my-value", "y": "my-value", "z": "my-value"}, ""},
+		{"simple request sequence", "/simple", test.Header{"x": "my-value"}, `{"value":"my-value"}`, log{"default": "resolve"}},
+		{"simple request/proxy sequence", "/simple-proxy", test.Header{"x": "my-value", "y": `{"value":"my-proxy-value"}`}, "", log{"default": "resolve"}},
+		{"simple proxy/request sequence", "/simple-proxy-named", test.Header{"x": "my-value"}, "", log{"default": "resolve"}},
+		{"complex request/proxy sequence", "/complex-proxy", test.Header{"x": "my-value"}, "", log{"default": "resolve", "resolve": "resolve_first"}},
+		{"complex request/proxy sequences", "/parallel-complex-proxy", test.Header{"x": "my-value", "y": "my-value", "z": "my-value"}, "", log{"default": "resolve", "resolve": "resolve_first"}},
+		{"complex nested request/proxy sequences", "/parallel-complex-nested", test.Header{
+			"a": "my-value",
+			"b": "my-value",
+			"x": "my-value",
+			"y": "my-value",
+		}, "", log{
+			"default":       "resolve",
+			"resolve":       "resolve_first",
+			"resolve_gamma": "resolve_gamma_first",
+			"last":          "resolve,resolve_gamma",
+		}},
 	} {
 		t.Run(tc.name, func(st *testing.T) {
+			hook.Reset()
+
 			h := test.New(st)
 
 			req, err := http.NewRequest(http.MethodGet, "http://domain.local:8080"+tc.path, nil)
@@ -506,6 +524,28 @@ func TestEndpointSequence(t *testing.T) {
 				}
 			}
 
+			for _, e := range hook.AllEntries() {
+				if e.Data["type"] != "couper_backend" {
+					continue
+				}
+
+				requestName, _ := e.Data["request"].(logging.Fields)["name"].(string)
+
+				// test result for expected named ones
+				if _, exist := tc.expectedLog[requestName]; !exist {
+					continue
+				}
+
+				dependsOn, ok := e.Data["depends_on"]
+				if !ok {
+					st.Fatal("Expected 'depends_on' log field")
+				}
+
+				if dependsOn != tc.expectedLog[requestName] {
+					st.Errorf("Expected 'depends_on' log for %q with field value: %q, got: %q", requestName, tc.expectedLog[requestName], dependsOn)
+				}
+			}
+
 		})
 	}
 
@@ -515,7 +555,7 @@ func TestEndpointSequenceClientCancel(t *testing.T) {
 	client := test.NewHTTPClient()
 	helper := test.New(t)
 
-	shutdown, hook := newCouper(path.Join(testdataPath, "12_couper.hcl"), helper)
+	shutdown, hook := newCouper(filepath.Join(testdataPath, "12_couper.hcl"), helper)
 	defer shutdown()
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -576,7 +616,7 @@ func TestEndpointSequenceBackendTimeout(t *testing.T) {
 	client := test.NewHTTPClient()
 	helper := test.New(t)
 
-	shutdown, hook := newCouper(path.Join(testdataPath, "13_couper.hcl"), helper)
+	shutdown, hook := newCouper(filepath.Join(testdataPath, "13_couper.hcl"), helper)
 	defer shutdown()
 
 	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -634,11 +674,36 @@ func TestEndpointSequenceBackendTimeout(t *testing.T) {
 
 }
 
+func TestEndpointCyclicSequence(t *testing.T) {
+	for _, testcase := range []struct{ file, exp string }{
+		{file: "15_couper.hcl", exp: "circular sequence reference: a,b,a"},
+		{file: "16_couper.hcl", exp: "circular sequence reference: a,aa,aaa,a"},
+	} {
+		t.Run(testcase.file, func(st *testing.T) {
+			// since we will switch the working dir, reset afterwards
+			defer cleanup(func() {}, test.New(t))
+
+			path := filepath.Join(testdataPath, testcase.file)
+			_, err := configload.LoadFile(path)
+
+			diags, ok := err.(*hcl.Diagnostic)
+			if !ok {
+				st.Errorf("Expected an cyclic hcl diagnostics error, got: %v", reflect.TypeOf(err))
+				st.Fatal(err, path)
+			}
+
+			if diags.Detail != testcase.exp {
+				st.Errorf("\nWant:\t%s\nGot:\t%s", testcase.exp, diags.Detail)
+			}
+		})
+	}
+}
+
 func TestEndpointErrorHandler(t *testing.T) {
 	client := test.NewHTTPClient()
 	helper := test.New(t)
 
-	shutdown, hook := newCouper(path.Join(testdataPath, "14_couper.hcl"), helper)
+	shutdown, hook := newCouper(filepath.Join(testdataPath, "14_couper.hcl"), helper)
 	defer shutdown()
 	defer func() {
 		for _, e := range hook.AllEntries() {
diff --git a/server/testdata/endpoints/11_couper.hcl b/server/testdata/endpoints/11_couper.hcl
index 27a39ec7..09876da7 100644
--- a/server/testdata/endpoints/11_couper.hcl
+++ b/server/testdata/endpoints/11_couper.hcl
@@ -110,4 +110,52 @@ server { # sequences
       }
     }
   }
+
+  endpoint "/parallel-complex-nested" {
+    request "last" { # seq waiting for the other sequences in parallel
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      headers = {
+        a = backend_responses.resolve_gamma.headers.y-value
+        b = backend_responses.resolve.headers.y-value
+      }
+    }
+
+    request "resolve" { # use the reflected origin header to obtain the y-value
+      url = "${backend_responses.resolve_first.headers.origin}/"
+    }
+
+    request "resolve_first" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      headers = {
+        origin = request.headers.origin
+      }
+    }
+
+    request "resolve_gamma" { # use the reflected origin header to obtain the y-value
+      url = "${backend_responses.resolve_gamma_first.headers.origin}/"
+    }
+
+    request "resolve_gamma_first" {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      headers = {
+        origin = request.headers.origin
+      }
+    }
+
+    proxy {
+      url = "${env.COUPER_TEST_BACKEND_ADDR}/reflect"
+      set_request_headers = {
+        x = backend_responses.resolve.headers.y-value
+      }
+    }
+
+    response {
+      headers = {
+        a = backend_responses.last.headers.a
+        b = backend_responses.last.headers.b
+        x = backend_responses.default.headers.x
+        y = backend_responses.resolve_gamma.headers.y-value
+      }
+    }
+  }
 }
diff --git a/server/testdata/endpoints/15_couper.hcl b/server/testdata/endpoints/15_couper.hcl
new file mode 100644
index 00000000..1370868b
--- /dev/null
+++ b/server/testdata/endpoints/15_couper.hcl
@@ -0,0 +1,25 @@
+server {
+  api {
+    endpoint "/" {
+      request "b" {
+        url = "http://localhost/b"
+        form_body = {
+          a = backend_responses.a.status
+        }
+      }
+
+      request "a" {
+        url = "http://localhost/a"
+        form_body = {
+          aa = backend_responses.b.status
+        }
+      }
+
+      response {
+        json_body = {
+          a = backend_responses.a.status
+        }
+      }
+    }
+  }
+}
diff --git a/server/testdata/endpoints/16_couper.hcl b/server/testdata/endpoints/16_couper.hcl
new file mode 100644
index 00000000..d4c2af28
--- /dev/null
+++ b/server/testdata/endpoints/16_couper.hcl
@@ -0,0 +1,27 @@
+server {
+  endpoint "/" {
+    request "aaa" {
+      url = "http://localhost:8083/aaa"
+      form_body = {
+        a = backend_responses.a.status
+      }
+    }
+    request "aa" {
+      url = "http://localhost:8083/aa"
+      form_body = {
+        aaa = backend_responses.aaa.status
+      }
+    }
+    request "a" {
+      url = "http://localhost:8083/a"
+      form_body = {
+        aa = backend_responses.aa.status
+      }
+    }
+    response {
+      json_body = {
+        a = backend_responses.a.status
+      }
+    }
+  }
+}