Skip to content
27 changes: 27 additions & 0 deletions .chloggen/spanevents.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

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

# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog)
component: receiver/github

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add `include_span_events` for GitHub Workflow Runs and Jobs for enhanced troubleshooting

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

# (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:

# 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]
30 changes: 30 additions & 0 deletions receiver/githubreceiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ The WebHook configuration exposes the following settings:
* `service_name`: (optional) - The service name for the traces. See the
[Configuring Service Name](#configuring-service-name) section for more
information.
* `include_span_events`: (default = `false`) - When set to `true`, attaches the raw webhook event JSON as a span event. The workflow run event is attached to the workflow run span, and the workflow job event is attached to the job span.

The WebHook configuration block also accepts all the [confighttp][cfghttp]
settings.
Expand Down Expand Up @@ -206,6 +207,35 @@ The precedence for setting the `service.name` resource attribute is as follows:
3. `service_name` derived from the repository name.
4. `service.name` set to `unknown_service` per the semantic conventions as a fall back.

### Span Events

When `include_span_events` is enabled, the receiver attaches the raw GitHub webhook event JSON as a span event to the corresponding span:

- **Workflow Run events**: Attached as a span event named `github.workflow_run.event` to the root workflow run span
- **Workflow Job events**: Attached as a span event named `github.workflow_job.event` to the job span

The raw event is stored in the `event.payload` attribute as a JSON string. This allows for detailed inspection of the complete webhook payload, including fields that may not be mapped to span attributes.

**Note**: The raw event payload can be large (typically 5-50KB). Consider the impact on storage and performance before enabling this feature in production environments.

An example configuration with span events enabled:

```yaml
receivers:
github:
webhook:
endpoint: localhost:19418
path: /events
health_path: /health
secret: ${env:SECRET_STRING_VAR}
required_headers:
WAF-Header: "value"
include_span_events: true
scrapers: # The validation expects at least a dummy scraper config
scraper:
github_org: open-telemetry
```

### Configuring A GitHub App

To configure a GitHub App, you will need to create a new GitHub App within your
Expand Down
1 change: 1 addition & 0 deletions receiver/githubreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type WebHook struct {
GitHubHeaders GitHubHeaders `mapstructure:",squash"` // GitLab headers set by default
Secret string `mapstructure:"secret"` // secret for webhook
ServiceName string `mapstructure:"service_name"`
IncludeSpanEvents bool `mapstructure:"include_span_events"` // attach raw webhook event JSON as span events
}

type GitHubHeaders struct {
Expand Down
29 changes: 29 additions & 0 deletions receiver/githubreceiver/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,32 @@ func TestConfig_Unmarshal(t *testing.T) {
})
}
}

func TestIncludeSpanEventsConfig(t *testing.T) {
tests := []struct {
name string
config *Config
expected bool
}{
{
name: "default config has span events disabled",
config: createDefaultConfig().(*Config),
expected: false,
},
{
name: "span events can be enabled",
config: &Config{
WebHook: WebHook{
IncludeSpanEvents: true,
},
},
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.config.WebHook.IncludeSpanEvents)
})
}
}
16 changes: 9 additions & 7 deletions receiver/githubreceiver/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import (
// This file implements a factory for the github receiver

const (
defaultReadTimeout = 500 * time.Millisecond
defaultWriteTimeout = 500 * time.Millisecond
defaultPath = "/events"
defaultHealthPath = "/health"
defaultEndpoint = "localhost:8080"
defaultReadTimeout = 500 * time.Millisecond
defaultWriteTimeout = 500 * time.Millisecond
defaultPath = "/events"
defaultHealthPath = "/health"
defaultEndpoint = "localhost:8080"
defaultIncludeSpanEvents = false
)

var (
Expand Down Expand Up @@ -79,8 +80,9 @@ func createDefaultConfig() component.Config {
defaultGitHubSignature256Header: "",
},
},
Path: defaultPath,
HealthPath: defaultHealthPath,
Path: defaultPath,
HealthPath: defaultHealthPath,
IncludeSpanEvents: defaultIncludeSpanEvents,
},
}
}
Expand Down
28 changes: 23 additions & 5 deletions receiver/githubreceiver/trace_event_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"go.uber.org/zap"
)

func (gtr *githubTracesReceiver) handleWorkflowRun(e *github.WorkflowRunEvent) (ptrace.Traces, error) {
func (gtr *githubTracesReceiver) handleWorkflowRun(e *github.WorkflowRunEvent, rawPayload []byte) (ptrace.Traces, error) {
t := ptrace.NewTraces()
r := t.ResourceSpans().AppendEmpty()

Expand All @@ -35,7 +35,7 @@ func (gtr *githubTracesReceiver) handleWorkflowRun(e *github.WorkflowRunEvent) (
gtr.logger.Sugar().Error("failed to generate trace ID", zap.Error(err))
}

err = gtr.createRootSpan(r, e, traceID)
err = gtr.createRootSpan(r, e, traceID, rawPayload)
if err != nil {
gtr.logger.Sugar().Error("failed to create root span", zap.Error(err))
return ptrace.Traces{}, errors.New("failed to create root span")
Expand All @@ -46,7 +46,7 @@ func (gtr *githubTracesReceiver) handleWorkflowRun(e *github.WorkflowRunEvent) (
// handleWorkflowJob handles the creation of spans for a GitHub Workflow Job
// events, including the underlying steps within each job. A `job` maps to the
// semantic conventions for a `cicd.pipeline.task`.
func (gtr *githubTracesReceiver) handleWorkflowJob(e *github.WorkflowJobEvent) (ptrace.Traces, error) {
func (gtr *githubTracesReceiver) handleWorkflowJob(e *github.WorkflowJobEvent, rawPayload []byte) (ptrace.Traces, error) {
t := ptrace.NewTraces()
r := t.ResourceSpans().AppendEmpty()

Expand All @@ -62,7 +62,7 @@ func (gtr *githubTracesReceiver) handleWorkflowJob(e *github.WorkflowJobEvent) (
gtr.logger.Sugar().Error("failed to generate trace ID", zap.Error(err))
}

parentID, err := gtr.createParentSpan(r, e, traceID)
parentID, err := gtr.createParentSpan(r, e, traceID, rawPayload)
if err != nil {
gtr.logger.Sugar().Error("failed to create parent span", zap.Error(err))
return ptrace.Traces{}, errors.New("failed to create parent span")
Expand Down Expand Up @@ -127,6 +127,7 @@ func (gtr *githubTracesReceiver) createRootSpan(
resourceSpans ptrace.ResourceSpans,
event *github.WorkflowRunEvent,
traceID pcommon.TraceID,
rawPayload []byte,
) error {
scopeSpans := resourceSpans.ScopeSpans().AppendEmpty()
span := scopeSpans.Spans().AppendEmpty()
Expand Down Expand Up @@ -154,6 +155,14 @@ func (gtr *githubTracesReceiver) createRootSpan(

span.Status().SetMessage(event.GetWorkflowRun().GetConclusion())

// Attach raw event as span event if configured
if gtr.cfg.WebHook.IncludeSpanEvents {
spanEvent := span.Events().AppendEmpty()
spanEvent.SetName("github.workflow_run.event")
spanEvent.SetTimestamp(pcommon.NewTimestampFromTime(event.GetWorkflowRun().GetRunStartedAt().Time))
spanEvent.Attributes().PutStr("event.payload", string(rawPayload))
}

// Attempt to link to previous trace ID if applicable
if event.GetWorkflowRun().GetPreviousAttemptURL() != "" && event.GetWorkflowRun().GetRunAttempt() > 1 {
gtr.logger.Debug("Linking to previous trace ID for WorkflowRunEvent")
Expand All @@ -173,10 +182,11 @@ func (gtr *githubTracesReceiver) createRootSpan(

// createParentSpan creates a parent span based on the provided event, associated
// with the deterministic traceID.
func (*githubTracesReceiver) createParentSpan(
func (gtr *githubTracesReceiver) createParentSpan(
resourceSpans ptrace.ResourceSpans,
event *github.WorkflowJobEvent,
traceID pcommon.TraceID,
rawPayload []byte,
) (pcommon.SpanID, error) {
scopeSpans := resourceSpans.ScopeSpans().AppendEmpty()
span := scopeSpans.Spans().AppendEmpty()
Expand Down Expand Up @@ -211,6 +221,14 @@ func (*githubTracesReceiver) createParentSpan(

span.Status().SetMessage(event.GetWorkflowJob().GetConclusion())

// Attach raw event as span event if configured
if gtr.cfg.WebHook.IncludeSpanEvents {
spanEvent := span.Events().AppendEmpty()
spanEvent.SetName("github.workflow_job.event")
spanEvent.SetTimestamp(pcommon.NewTimestampFromTime(event.GetWorkflowJob().GetCreatedAt().Time))
spanEvent.Attributes().PutStr("event.payload", string(rawPayload))
}

return jobSpanID, nil
}

Expand Down
Loading