Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions apis/projectcontour/v1alpha1/contourconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ type ContourConfigurationSpec struct {
// Contour's default is { address: "0.0.0.0", port: 8000 }.
// +optional
Metrics *MetricsConfig `json:"metrics,omitempty"`

// Tracing defines properties for exporting trace data to OpenTelemetry.
Tracing *TracingConfig `json:"tracing,omitempty"`
}

// XDSServerType is the type of xDS server implementation.
Expand Down Expand Up @@ -659,6 +662,56 @@ type RateLimitServiceConfig struct {
EnableResourceExhaustedCode *bool `json:"enableResourceExhaustedCode,omitempty"`
}

// TracingConfig defines properties for exporting trace data to OpenTelemetry.
type TracingConfig struct {
// IncludePodDetail defines a flag.
// If it is true, contour will add the pod name and namespace to the span of the trace.
// the default is true.
// Note: The Envoy pods MUST have the HOSTNAME and CONTOUR_NAMESPACE environment variables set for this to work properly.
// +optional
IncludePodDetail *bool `json:"includePodDetail,omitempty"`

// ServiceName defines the name for the service.
// contour's default is contour.
ServiceName *string `json:"serviceName,omitempty"`

// OverallSampling defines the sampling rate of trace data.
// contour's default is 100.
// +optional
OverallSampling *string `json:"overallSampling,omitempty"`

// MaxPathTagLength defines maximum length of the request path
// to extract and include in the HttpUrl tag.
// contour's default is 256.
// +optional
MaxPathTagLength *uint32 `json:"maxPathTagLength,omitempty"`

// CustomTags defines a list of custom tags with unique tag name.
// +optional
CustomTags []*CustomTag `json:"customTags,omitempty"`

// ExtensionService identifies the extension service defining the otel-collector.
ExtensionService *NamespacedName `json:"extensionService"`
}

// CustomTag defines custom tags with unique tag name
// to create tags for the active span.
type CustomTag struct {
// TagName is the unique name of the custom tag.
TagName string `json:"tagName"`

// Literal is a static custom tag value.
// Precisely one of Literal, RequestHeaderName must be set.
// +optional
Literal string `json:"literal,omitempty"`

// RequestHeaderName indicates which request header
// the label value is obtained from.
// Precisely one of Literal, RequestHeaderName must be set.
// +optional
RequestHeaderName string `json:"requestHeaderName,omitempty"`
}

// PolicyConfig holds default policy used if not explicitly set by the user
type PolicyConfig struct {
// RequestHeadersPolicy defines the request headers set/removed on all routes
Expand Down
45 changes: 45 additions & 0 deletions apis/projectcontour/v1alpha1/contourconfig_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package v1alpha1

import (
"fmt"
"strconv"

"k8s.io/apimachinery/pkg/util/sets"
)
Expand All @@ -38,6 +39,9 @@ func (c *ContourConfigurationSpec) Validate() error {
if c.Gateway != nil {
validateFuncs = append(validateFuncs, c.Gateway.Validate)
}
if c.Tracing != nil {
validateFuncs = append(validateFuncs, c.Tracing.Validate)
}

for _, validate := range validateFuncs {
if err := validate(); err != nil {
Expand All @@ -48,6 +52,47 @@ func (c *ContourConfigurationSpec) Validate() error {
return nil
}

func (t *TracingConfig) Validate() error {
if t.ExtensionService == nil {
return fmt.Errorf("tracing.extensionService must be defined")
}

if t.OverallSampling != nil {
_, err := strconv.ParseFloat(*t.OverallSampling, 64)
if err != nil {
return fmt.Errorf("invalid tracing sampling: %v", err)
}
}

var customTagNames []string

for _, customTag := range t.CustomTags {
var fieldCount int
if customTag.TagName == "" {
return fmt.Errorf("tracing.customTag.tagName must be defined")
}

for _, customTagName := range customTagNames {
if customTagName == customTag.TagName {
return fmt.Errorf("tagName %s is duplicate", customTagName)
}
}

if customTag.Literal != "" {
fieldCount++
}

if customTag.RequestHeaderName != "" {
fieldCount++
}
if fieldCount != 1 {
return fmt.Errorf("must set exactly one of Literal or RequestHeaderName")
}
customTagNames = append(customTagNames, customTag.TagName)
}
return nil
}

func (x XDSServerType) Validate() error {
switch x {
case ContourServerType, EnvoyServerType:
Expand Down
55 changes: 55 additions & 0 deletions apis/projectcontour/v1alpha1/contourconfig_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"testing"

"github.com/projectcontour/contour/apis/projectcontour/v1alpha1"
"github.com/projectcontour/contour/internal/ref"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -158,6 +159,60 @@ func TestContourConfigurationSpecValidate(t *testing.T) {
c.Gateway.GatewayRef = &v1alpha1.NamespacedName{Namespace: "ns", Name: "name"}
require.Error(t, c.Validate())
})

t.Run("tracing validation", func(t *testing.T) {
c := v1alpha1.ContourConfigurationSpec{
Tracing: &v1alpha1.TracingConfig{},
}

require.Error(t, c.Validate())

c.Tracing.ExtensionService = &v1alpha1.NamespacedName{
Name: "otel-collector",
Namespace: "projectcontour",
}
require.NoError(t, c.Validate())

c.Tracing.OverallSampling = ref.To("number")
require.Error(t, c.Validate())

c.Tracing.OverallSampling = ref.To("10")
require.NoError(t, c.Validate())

customTags := []*v1alpha1.CustomTag{
{
TagName: "first tag",
Literal: "literal",
},
}
c.Tracing.CustomTags = customTags
require.NoError(t, c.Validate())

customTags = append(customTags, &v1alpha1.CustomTag{
TagName: "second tag",
RequestHeaderName: "x-custom-header",
})
c.Tracing.CustomTags = customTags
require.NoError(t, c.Validate())

customTags = append(customTags, &v1alpha1.CustomTag{
TagName: "first tag",
RequestHeaderName: "x-custom-header",
})
c.Tracing.CustomTags = customTags
require.Error(t, c.Validate())

customTags = []*v1alpha1.CustomTag{
{
TagName: "first tag",
Literal: "literal",
RequestHeaderName: "x-custom-header",
},
}
c.Tracing.CustomTags = customTags
require.Error(t, c.Validate())

})
}

func TestSanitizeCipherSuites(t *testing.T) {
Expand Down
71 changes: 71 additions & 0 deletions apis/projectcontour/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions changelogs/unreleased/5043-yangyy93-major.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## Add Tracing Support

Contour now supports exporting tracing data to [OpenTelemetry][1]

The Contour configuration file and ContourConfiguration CRD will be extended with a new optional `tracing` section. This configuration block, if present, will enable tracing and will define the trace properties needed to generate and export trace data.

### Contour supports the following configurations
- Custom service name, the default is `contour`.
- Custom sampling rate, the default is `100`.
- Custom the maximum length of the request path, the default is `256`.
- Customize span tags from literal and request headers.
- Customize whether to include the pod's hostname and namespace.

[1]: https://opentelemetry.io/
73 changes: 73 additions & 0 deletions cmd/contour/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ func (s *Server) doServe() error {
ConnectionBalancer: contourConfiguration.Envoy.Listener.ConnectionBalancer,
}

if listenerConfig.TracingConfig, err = s.setupTracingService(contourConfiguration.Tracing); err != nil {
return err
}

if listenerConfig.RateLimitConfig, err = s.setupRateLimitService(contourConfiguration); err != nil {
return err
}
Expand Down Expand Up @@ -594,6 +598,75 @@ func (s *Server) doServe() error {
return s.mgr.Start(signals.SetupSignalHandler())
}

func (s *Server) setupTracingService(tracingConfig *contour_api_v1alpha1.TracingConfig) (*xdscache_v3.TracingConfig, error) {
if tracingConfig == nil {
return nil, nil
}

// ensure the specified ExtensionService exists
extensionSvc := &contour_api_v1alpha1.ExtensionService{}
key := client.ObjectKey{
Namespace: tracingConfig.ExtensionService.Namespace,
Name: tracingConfig.ExtensionService.Name,
}
// Using GetAPIReader() here because the manager's caches won't be started yet,
// so reads from the manager's client (which uses the caches for reads) will fail.
if err := s.mgr.GetAPIReader().Get(context.Background(), key, extensionSvc); err != nil {
return nil, fmt.Errorf("error getting tracing extension service %s: %v", key, err)
}
// get the response timeout from the ExtensionService
var responseTimeout timeout.Setting
var err error

if tp := extensionSvc.Spec.TimeoutPolicy; tp != nil {
responseTimeout, err = timeout.Parse(tp.Response)
if err != nil {
return nil, fmt.Errorf("error parsing tracing extension service %s response timeout: %v", key, err)
}
}

var sni string
if extensionSvc.Spec.UpstreamValidation != nil {
sni = extensionSvc.Spec.UpstreamValidation.SubjectName
}

var customTags []*xdscache_v3.CustomTag

if ref.Val(tracingConfig.IncludePodDetail, true) {
customTags = append(customTags, &xdscache_v3.CustomTag{
TagName: "podName",
EnvironmentName: "HOSTNAME",
}, &xdscache_v3.CustomTag{
TagName: "podNamespace",
EnvironmentName: "CONTOUR_NAMESPACE",
})
}

for _, customTag := range tracingConfig.CustomTags {
customTags = append(customTags, &xdscache_v3.CustomTag{
TagName: customTag.TagName,
Literal: customTag.Literal,
RequestHeaderName: customTag.RequestHeaderName,
})
}

overallSampling, err := strconv.ParseFloat(ref.Val(tracingConfig.OverallSampling, "100"), 64)
if err != nil || overallSampling == 0 {
overallSampling = 100.0
}

return &xdscache_v3.TracingConfig{
ServiceName: ref.Val(tracingConfig.ServiceName, "contour"),
ExtensionService: key,
SNI: sni,
Timeout: responseTimeout,
OverallSampling: overallSampling,
MaxPathTagLength: ref.Val(tracingConfig.MaxPathTagLength, 256),
CustomTags: customTags,
}, nil

}

func (s *Server) setupRateLimitService(contourConfiguration contour_api_v1alpha1.ContourConfigurationSpec) (*xdscache_v3.RateLimitConfig, error) {
if contourConfiguration.RateLimitService == nil {
return nil, nil
Expand Down
Loading