Skip to content

Commit

Permalink
[receiver/gitlab] add tracing via webhook skeleton (open-telemetry#36838
Browse files Browse the repository at this point in the history
)

#### Description
This PR adds the structure and trace skeleton for a new and already
accepted Gitlabreceiver. (thanks @atoulme for sponsoring this!)

The Gitlabreceiver aligns very closely with the Githubreceiver and this
PR mostly mirrors the change from this PR:
open-telemetry#36632

I'm working together with @adrielp on building out the Gitlabreceiver.
More PRs to introduce metrics and actual tracing functionality are about
to follow with subsequent PRs.

#### Link to tracking issue

open-telemetry#35207

#### Testing
Added basic tests and built the component to test that the health check
endpoint, when tracing is enabled, operates correctly.

#### Documentation
Docs how to configure the Gitlabreceiver via webhooks have been added.
While the Gitlabreceiver can be configured after this PR, it will not
actually do anything since it is under development and just the skeleton
PR.
  • Loading branch information
niwoerner authored Jan 13, 2025
1 parent 12551d3 commit 778d8f7
Show file tree
Hide file tree
Showing 23 changed files with 1,167 additions and 0 deletions.
30 changes: 30 additions & 0 deletions .chloggen/gl-receiver-skeleton-traces.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: new_component

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: gitlabreceiver

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Adds webhook skeleton to GitLab receiver to receive events from GitLab for tracing.

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [35207]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:
This PR adds a skeleton for the GitLab receiver to receive events from GitLab
for tracing via a webhook. The trace portion of this receiver will run and
respond to GET requests for the health check only.

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [user]
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ receiver/filestatsreceiver/ @open-telemetry/collector-cont
receiver/flinkmetricsreceiver/ @open-telemetry/collector-contrib-approvers @JonathanWamsley
receiver/fluentforwardreceiver/ @open-telemetry/collector-contrib-approvers @dmitryax
receiver/githubreceiver/ @open-telemetry/collector-contrib-approvers @adrielp @andrzej-stencel @crobert-1 @TylerHelmuth
receiver/gitlabreceiver/ @open-telemetry/collector-contrib-approvers @adrielp @atoulme
receiver/googlecloudmonitoringreceiver/ @open-telemetry/collector-contrib-approvers @dashpole @TylerHelmuth @abhishek-at-cloudwerx
receiver/googlecloudpubsubreceiver/ @open-telemetry/collector-contrib-approvers @alexvanboxel
receiver/googlecloudspannerreceiver/ @open-telemetry/collector-contrib-approvers @dashpole @dsimil @KiranmayiB @harishbohara11
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ body:
- receiver/flinkmetrics
- receiver/fluentforward
- receiver/github
- receiver/gitlab
- receiver/googlecloudmonitoring
- receiver/googlecloudpubsub
- receiver/googlecloudspanner
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ body:
- receiver/flinkmetrics
- receiver/fluentforward
- receiver/github
- receiver/gitlab
- receiver/googlecloudmonitoring
- receiver/googlecloudpubsub
- receiver/googlecloudspanner
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/other.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ body:
- receiver/flinkmetrics
- receiver/fluentforward
- receiver/github
- receiver/gitlab
- receiver/googlecloudmonitoring
- receiver/googlecloudpubsub
- receiver/googlecloudspanner
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/unmaintained.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ body:
- receiver/flinkmetrics
- receiver/fluentforward
- receiver/github
- receiver/gitlab
- receiver/googlecloudmonitoring
- receiver/googlecloudpubsub
- receiver/googlecloudspanner
Expand Down
1 change: 1 addition & 0 deletions receiver/gitlabreceiver/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
55 changes: 55 additions & 0 deletions receiver/gitlabreceiver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# GitLab Receiver

<!-- status autogenerated section -->
| Status | |
| ------------- |-----------|
| Stability | [development]: traces |
| Distributions | [] |
| Issues | [![Open issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aopen%20label%3Areceiver%2Fgitlab%20&label=open&color=orange&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aopen+is%3Aissue+label%3Areceiver%2Fgitlab) [![Closed issues](https://img.shields.io/github/issues-search/open-telemetry/opentelemetry-collector-contrib?query=is%3Aissue%20is%3Aclosed%20label%3Areceiver%2Fgitlab%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues?q=is%3Aclosed+is%3Aissue+label%3Areceiver%2Fgitlab) |
| [Code Owners](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#becoming-a-code-owner) | [@adrielp](https://www.github.com/adrielp), [@atoulme](https://www.github.com/atoulme) |

[development]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#development
<!-- end autogenerated section -->

## Traces - Getting Started

Workflow tracing support is actively being added to the GitLab receiver.
This is accomplished through the processing of GitLab webhook
events for pipelines. The [`pipeline`](https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#pipeline-events) event payloads are then constructed into `trace`
telemetry.

Each GitLab pipeline, along with its jobs, is converted
into trace spans, allowing the observation of workflow execution times,
success, and failure rates.

### Configuration

**IMPORTANT: At this time the tracing portion of this receiver only serves a health check endpoint.**

The WebHook configuration exposes the following settings:

* `endpoint`: (default = `localhost:8080`) - The address and port to bind the WebHook to.
* `path`: (default = `/events`) - The path for Action events to be sent to.
* `health_path`: (default = `/health`) - The path for health checks.
* `secret`: (optional) - The secret used to [validate the payload](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#custom-headers).
* `required_headers`: (optional) - One or more key-value pairs representing required headers for incoming requests. These headers must not conflict with the fixed default GitLab headers. See the customizable and fixed GitLab headers in [config.go](./config.go).

The WebHook configuration block also accepts all the [confighttp](https://pkg.go.dev/go.opentelemetry.io/collector/config/confighttp#ServerConfig)
settings.

An example configuration is as follows:

```yaml
receivers:
gitlab:
webhook:
endpoint: localhost:19418
path: /events
health_path: /health
secret: ${env:SECRET_STRING_VAR}
required_headers:
WAF-Header: "value"
```
For tracing, all configuration is set under the `webhook` key. The full set
of exposed configuration values can be found in [`config.go`](config.go).
136 changes: 136 additions & 0 deletions receiver/gitlabreceiver/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package gitlabreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/gitlabreceiver"

import (
"errors"
"time"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/confighttp"
"go.opentelemetry.io/collector/config/configopaque"
"go.opentelemetry.io/collector/confmap"
"go.uber.org/multierr"
)

const (
defaultReadTimeout = 500 * time.Millisecond
defaultWriteTimeout = 500 * time.Millisecond

defaultEndpoint = "localhost:8080"

defaultPath = "/events"
defaultHealthPath = "/health"

// GitLab default headers: https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#delivery-headers
defaultUserAgentHeader = "User-Agent"
defaultGitlabInstanceHeader = "X-Gitlab-Instance"
defaultGitlabWebhookUUIDHeader = "X-Gitlab-Webhook-UUID"
defaultGitlabEventHeader = "X-Gitlab-Event"
defaultGitlabEventUUIDHeader = "X-Gitlab-Event-UUID"
defaultIdempotencyKeyHeader = "Idempotency-Key"
)

var (
errReadTimeoutExceedsMaxValue = errors.New("the duration specified for read_timeout exceeds the maximum allowed value of 10s")
errWriteTimeoutExceedsMaxValue = errors.New("the duration specified for write_timeout exceeds the maximum allowed value of 10s")
errRequiredHeader = errors.New("both key and value are required to assign a required_header")
errGitlabHeader = errors.New("gitlab default headers [X-Gitlab-Webhook-UUID, X-Gitlab-Event, X-Gitlab-Event-UUID, Idempotency-Key] cannot be configured")
errConfigNotValid = errors.New("configuration is not valid for the gitlab receiver")
)

// Config that is exposed to this gitlab receiver through the OTEL config.yaml
type Config struct {
WebHook WebHook `mapstructure:"webhook"`
}

type WebHook struct {
confighttp.ServerConfig `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct

Path string `mapstructure:"path"` // path for data collection. default is /events
HealthPath string `mapstructure:"health_path"` // path for health check api. default is /health_check

RequiredHeaders map[string]configopaque.String `mapstructure:"required_headers"` // optional setting to set one or more required headers for all requests to have (except the health check)
GitlabHeaders GitlabHeaders `mapstructure:",squash"` // GitLab headers set by default

Secret string `mapstructure:"secret"` // secret for webhook
}

type GitlabHeaders struct {
Customizable map[string]string `mapstructure:","` // can be overwritten via required_headers
Fixed map[string]string `mapstructure:","` // are not allowed to be overwritten
}

func createDefaultConfig() component.Config {
return &Config{
WebHook: WebHook{
ServerConfig: confighttp.ServerConfig{
Endpoint: defaultEndpoint,
ReadTimeout: defaultReadTimeout,
WriteTimeout: defaultWriteTimeout,
},
GitlabHeaders: GitlabHeaders{
Customizable: map[string]string{
defaultUserAgentHeader: "",
defaultGitlabInstanceHeader: "https://gitlab.com",
},
Fixed: map[string]string{
defaultGitlabWebhookUUIDHeader: "",
defaultGitlabEventHeader: "Pipeline Hook",
defaultGitlabEventUUIDHeader: "",
defaultIdempotencyKeyHeader: "",
},
},
Path: defaultPath,
HealthPath: defaultHealthPath,
},
}
}

func (cfg *Config) Validate() error {
var errs error

maxReadWriteTimeout, _ := time.ParseDuration("10s")

if cfg.WebHook.ServerConfig.ReadTimeout > maxReadWriteTimeout {
errs = multierr.Append(errs, errReadTimeoutExceedsMaxValue)
}

if cfg.WebHook.ServerConfig.WriteTimeout > maxReadWriteTimeout {
errs = multierr.Append(errs, errWriteTimeoutExceedsMaxValue)
}

for key, value := range cfg.WebHook.RequiredHeaders {
if key == "" || value == "" {
errs = multierr.Append(errs, errRequiredHeader)
}

if _, exists := cfg.WebHook.GitlabHeaders.Fixed[key]; exists {
errs = multierr.Append(errs, errGitlabHeader)
}
}

return errs
}

func (cfg *Config) Unmarshal(componentParser *confmap.Conf) error {
if componentParser == nil {
return nil
}

// load the non-dynamic config normally
err := componentParser.Unmarshal(cfg, confmap.WithIgnoreUnused())
if err != nil {
return err
}

// overwrite customizable GitLab default headers if configured within the required_headers
for key, header := range cfg.WebHook.RequiredHeaders {
if _, exists := cfg.WebHook.GitlabHeaders.Customizable[key]; exists {
cfg.WebHook.GitlabHeaders.Customizable[key] = string(header)
}
}

return nil
}
123 changes: 123 additions & 0 deletions receiver/gitlabreceiver/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package gitlabreceiver

import (
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config/confighttp"
"go.opentelemetry.io/collector/config/configopaque"
"go.opentelemetry.io/collector/otelcol/otelcoltest"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/gitlabreceiver/internal/metadata"
)

func TestCreateDefaultConfig(t *testing.T) {
factory := NewFactory()
cfg := factory.CreateDefaultConfig()

expectedConfig := &Config{
WebHook: WebHook{
ServerConfig: confighttp.ServerConfig{
Endpoint: defaultEndpoint,
ReadTimeout: defaultReadTimeout,
WriteTimeout: defaultWriteTimeout,
},
Path: defaultPath,
HealthPath: defaultHealthPath,
GitlabHeaders: GitlabHeaders{
Customizable: map[string]string{
defaultUserAgentHeader: "",
defaultGitlabInstanceHeader: "https://gitlab.com",
},
Fixed: map[string]string{
defaultGitlabWebhookUUIDHeader: "",
defaultGitlabEventHeader: "Pipeline Hook",
defaultGitlabEventUUIDHeader: "",
defaultIdempotencyKeyHeader: "",
},
},
},
}

assert.Equal(t, expectedConfig, cfg, "failed to create default config")
assert.NoError(t, componenttest.CheckConfigStruct(cfg))
}

func TestLoadConfig(t *testing.T) {
factories, err := otelcoltest.NopFactories()
require.NoError(t, err)

factory := NewFactory()
factories.Receivers[metadata.Type] = factory

cfg, err := otelcoltest.LoadConfigAndValidate(filepath.Join("testdata", "config.yaml"), factories)

require.NoError(t, err)
require.NotNil(t, cfg)

assert.Len(t, cfg.Receivers, 2)

expectedConfig := &Config{
WebHook: WebHook{
ServerConfig: confighttp.ServerConfig{
Endpoint: "localhost:8080",
ReadTimeout: 500 * time.Millisecond,
WriteTimeout: 500 * time.Millisecond,
},
Path: "some/path",
HealthPath: "health/path",
RequiredHeaders: map[string]configopaque.String{
"key1-present": "value1-present",
},
GitlabHeaders: GitlabHeaders{
Customizable: map[string]string{
defaultUserAgentHeader: "",
defaultGitlabInstanceHeader: "https://gitlab.com",
},
Fixed: map[string]string{
defaultGitlabWebhookUUIDHeader: "",
defaultGitlabEventHeader: "Pipeline Hook",
defaultGitlabEventUUIDHeader: "",
defaultIdempotencyKeyHeader: "",
},
},
},
}

r0 := cfg.Receivers[component.NewID(metadata.Type)]

assert.Equal(t, expectedConfig, r0)

// r1 requires multiple headers and overwrites gitlab default headers
expectedConfig.WebHook.RequiredHeaders = map[string]configopaque.String{
"key1-present": "value1-present",
"key2-present": "value2-present",
"User-Agent": "GitLab/1.2.3-custom-version",
"X-Gitlab-Instance": "https://gitlab.self-hosted.xyz",
}

expectedConfig.WebHook.GitlabHeaders = GitlabHeaders{
Customizable: map[string]string{
defaultUserAgentHeader: "GitLab/1.2.3-custom-version",
defaultGitlabInstanceHeader: "https://gitlab.self-hosted.xyz",
},
Fixed: map[string]string{
defaultGitlabWebhookUUIDHeader: "",
defaultGitlabEventHeader: "Pipeline Hook",
defaultGitlabEventUUIDHeader: "",
defaultIdempotencyKeyHeader: "",
},
}

r1 := cfg.Receivers[component.NewIDWithName(metadata.Type, "customname")].(*Config)

assert.Equal(t, expectedConfig, r1)
}
6 changes: 6 additions & 0 deletions receiver/gitlabreceiver/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

//go:generate mdatagen metadata.yaml

package gitlabreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/gitlabreceiver"
Loading

0 comments on commit 778d8f7

Please sign in to comment.