diff --git a/.chloggen/gh-tracing.yaml b/.chloggen/gh-tracing.yaml new file mode 100644 index 000000000000..b8fad207ad4b --- /dev/null +++ b/.chloggen/gh-tracing.yaml @@ -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. filelogreceiver) +component: githubreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: add support for GitHub Actions workflow run events using deterministic Trace and Root Span IDs. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [37578] + +# (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: [] diff --git a/receiver/githubreceiver/README.md b/receiver/githubreceiver/README.md index f3009100b517..d6cfd4f38833 100644 --- a/receiver/githubreceiver/README.md +++ b/receiver/githubreceiver/README.md @@ -14,7 +14,26 @@ [contrib]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib -The GitHub receiver receives data from [GitHub](https://github.com). +# Table of Contents + +- [Overview](#overview) +- [Metrics - Getting Started](#metrics---getting-started) + - [Scraping](#scraping) +- [Traces - Getting Started](#traces---getting-started) + - [Receiver Configuration](#receiver-configuration) + - [Configuring Service Name](#configuring-service-name) + - [Configuration a GitHub App](#configuration-a-github-app) + +## Overview + +The GitHub receiver receives data from [GitHub](https://github.com) via two methods: + +1. Scrapes [version control system][vcsm] metrics from GitHub repositories and +organizations using the GraphQL and REST APIs. +2. Receives GitHub Actions events by serving a webhook endpoint, converting +those events into traces. + +## Metrics - Getting Started The current default set of metrics can be found in [documentation.md](./documentation.md). @@ -23,11 +42,6 @@ These metrics can be used as leading indicators ([capabilities][doracap]) to the [DORA][dorafour] metrics; helping provide insight into modern-day engineering practices. -[doracap]: https://dora.dev/capabilities/ -[dorafour]: https://dora.dev/guides/dora-metrics-four-keys/ - -## Metrics - Getting Started - The collection interval is common to all scrapers and is set to 30 seconds by default. > Note: Generally speaking, if the vendor allows for anonymous API calls, then you @@ -75,7 +89,7 @@ service: A [Grafana Dashboard for metrics from this receiver is on the marketplace](https://grafana.com/grafana/dashboards/20976-engineering-effectiveness-metrics/). -## Scraping +### Scraping > Important: > * The GitHub scraper does not emit metrics for branches that have not had @@ -83,9 +97,6 @@ A [Grafana Dashboard for metrics from this receiver is on the marketplace](https > * Due to GitHub API limitations, it is possible for the branch time metric to > change when rebases occur, recreating the commits with new timestamps. - - - For additional context on GitHub scraper limitations and inner workings please see the [Scraping README][ghsread]. @@ -103,9 +114,10 @@ Each GitHub Action workflow or job, along with its steps, are converted into trace spans, allowing the observation of workflow execution times, success, and failure rates. -### Configuration +### Receiver Configuration -**IMPORTANT: At this time the tracing portion of this receiver only serves a health check endpoint.** +**IMPORTANT** - Ensure to secure your WebHook endpoint with a secret and a Web +Application Firewall (WAF) or other security measure. The WebHook configuration exposes the following settings: @@ -114,6 +126,9 @@ The WebHook configuration exposes the following settings: * `health_path`: (default = `/health`) - The path for health checks. * `secret`: (optional) - The secret used to [validates the payload][valid]. * `required_header`: (optional) - The required header key and value for incoming requests. +* `service_name`: (optional) - The service name for the traces. See the +[Configuring Service Name](#configuring-service-name) section for more +information. The WebHook configuration block also accepts all the [confighttp][cfghttp] settings. @@ -123,23 +138,56 @@ An example configuration is as follows: ```yaml receivers: github: - scrapers: - ... : # Scraper configurations are required until Tracing functionality is complete. webhook: endpoint: localhost:19418 path: /events health_path: /health secret: ${env:SECRET_STRING_VAR} - required_header: - key: "X-GitHub-Event" - value: "action" + 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]. + +### Configuring Service Name + +The `service_name` configuration in the WebHook configuration can be used to set +a pre-defined `service.name` for all traces emitted by the receiver. This takes +priority over the internal generation of the `service.name`. In this +configuration, it would be important to create a GitHub receiver per GitHub app +configured for the set of repositories that match your `service.name`. + +However, a more efficient approach would be to leverage the default generation +of `service.name` by configuring [Custom Properties][cp] in each GitHub +repository. To do that simply add a `service_name` key with the desired value in +each repository and all events sent to the GitHub receiver will properly +associate with that `service.name`. Alternatively, the `service_name` will be +derived from the repository name. + +The order for creating the `service.name` is as follows: + +* `service_name` configuration in the WebHook configuration. +* `service_name` key in the repository's Custom Properties per repository. +* `service_name` derived from the repository name. +* `service.name` set to `unknown_service` per the semantic conventions as a fall back. + +### Configuring A GitHub App + +To configure a GitHub App, you will need to create a new GitHub App within your +organization. Refer to the general [GitHub App documentation][ghapp] for how to +create a GitHub App. During the subscription phase, subscribe to `workflow_run` and `workflow_job` events. + +> NOTE: Only `workflow_run` events are supported in created traces at this time. + [wjob]: https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_job [wrun]: https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_run [valid]: https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries [config.go] ./config.go [cfghttp]: https://pkg.go.dev/go.opentelemetry.io/collector/config/confighttp#ServerConfig +[cp]: https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization +[vcsm]: https://opentelemetry.io/docs/specs/semconv/cicd/cicd-metrics/#vcs-metrics +[doracap]: https://dora.dev/capabilities/ +[dorafour]: https://dora.dev/guides/dora-metrics-four-keys/ +[ghapp]: https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app diff --git a/receiver/githubreceiver/config.go b/receiver/githubreceiver/config.go index 4745c73fb0fe..02dbbfdabbcb 100755 --- a/receiver/githubreceiver/config.go +++ b/receiver/githubreceiver/config.go @@ -10,6 +10,7 @@ import ( "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configopaque" "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/scraper/scraperhelper" "go.uber.org/multierr" @@ -20,6 +21,13 @@ import ( const ( scrapersKey = "scrapers" + + // GitHub Delivery Headers: https://docs.github.com/en/webhooks/webhook-events-and-payloads#delivery-headers + defaultGitHubHookIDHeader = "X-GitHub-Hook-ID" // Unique identifier of the webhook. + defaultGitHubEventHeader = "X-GitHub-Event" // The name of the event that triggered the delivery. + defaultGitHubDeliveryHeader = "X-GitHub-Delivery" // A globally unique identifier (GUID) to identify the event. + defaultGitHubSignature256Header = "X-Hub-Signature-256" // The HMAC hex digest of the request body; generated using the SHA-256 hash function and the secret as the HMAC key. + defaultUserAgentHeader = "User-Agent" // Value always prefixed with "GitHub-Hookshot/" ) // Config that is exposed to this github receiver through the OTEL config.yaml @@ -31,16 +39,18 @@ type Config struct { } 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 - RequiredHeader RequiredHeader `mapstructure:"required_header"` // optional setting to set a required header for all requests to have - Secret string `mapstructure:"secret"` // secret for webhook + 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) + GitHubHeaders GitHubHeaders `mapstructure:",squash"` // GitLab headers set by default + Secret string `mapstructure:"secret"` // secret for webhook + ServiceName string `mapstructure:"service_name"` } -type RequiredHeader struct { - Key string `mapstructure:"key"` - Value string `mapstructure:"value"` +type GitHubHeaders struct { + Customizable map[string]string `mapstructure:","` // can be overwritten via required_headers + Fixed map[string]string `mapstructure:","` // are not allowed to be overwritten } var ( @@ -52,6 +62,7 @@ var ( 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") errRequireOneScraper = errors.New("must specify at least one scraper") + errGitHubHeader = errors.New("github default headers [X-GitHub-Event, X-GitHub-Delivery, X-GitHub-Hook-ID, X-Hub-Signature-256] cannot be configured") ) // Validate the configuration passed through the OTEL config.yaml @@ -78,8 +89,14 @@ func (cfg *Config) Validate() error { errs = multierr.Append(errs, errWriteTimeoutExceedsMaxValue) } - if (cfg.WebHook.RequiredHeader.Key != "" && cfg.WebHook.RequiredHeader.Value == "") || (cfg.WebHook.RequiredHeader.Value != "" && cfg.WebHook.RequiredHeader.Key == "") { - errs = multierr.Append(errs, errRequiredHeader) + for key, value := range cfg.WebHook.RequiredHeaders { + if key == "" || value == "" { + errs = multierr.Append(errs, errRequiredHeader) + } + + if _, exists := cfg.WebHook.GitHubHeaders.Fixed[key]; exists { + errs = multierr.Append(errs, errGitHubHeader) + } } return errs diff --git a/receiver/githubreceiver/config_test.go b/receiver/githubreceiver/config_test.go index c96b0e8dec13..4c7ee755ef92 100644 --- a/receiver/githubreceiver/config_test.go +++ b/receiver/githubreceiver/config_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/configopaque" "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/otelcol/otelcoltest" "go.opentelemetry.io/collector/scraper/scraperhelper" @@ -49,9 +50,19 @@ func TestLoadConfig(t *testing.T) { }, Path: "some/path", HealthPath: "health/path", - RequiredHeader: RequiredHeader{ - Key: "key-present", - Value: "value-present", + RequiredHeaders: map[string]configopaque.String{ + "key": "value-present", + }, + GitHubHeaders: GitHubHeaders{ + Customizable: map[string]string{ + "User-Agent": "", + }, + Fixed: map[string]string{ + "X-GitHub-Delivery": "", + "X-GitHub-Event": "", + "X-GitHub-Hook-ID": "", + "X-Hub-Signature-256": "", + }, }, } @@ -74,9 +85,19 @@ func TestLoadConfig(t *testing.T) { }, Path: "some/path", HealthPath: "health/path", - RequiredHeader: RequiredHeader{ - Key: "key-present", - Value: "value-present", + RequiredHeaders: map[string]configopaque.String{ + "key": "value-present", + }, + GitHubHeaders: GitHubHeaders{ + Customizable: map[string]string{ + "User-Agent": "", + }, + Fixed: map[string]string{ + "X-GitHub-Delivery": "", + "X-GitHub-Event": "", + "X-GitHub-Hook-ID": "", + "X-Hub-Signature-256": "", + }, }, }, } diff --git a/receiver/githubreceiver/factory.go b/receiver/githubreceiver/factory.go index 2bc0e1bc9f32..b521ba9ff72a 100644 --- a/receiver/githubreceiver/factory.go +++ b/receiver/githubreceiver/factory.go @@ -68,6 +68,17 @@ func createDefaultConfig() component.Config { ReadTimeout: defaultReadTimeout, WriteTimeout: defaultWriteTimeout, }, + GitHubHeaders: GitHubHeaders{ + Customizable: map[string]string{ + defaultUserAgentHeader: "", + }, + Fixed: map[string]string{ + defaultGitHubEventHeader: "", + defaultGitHubDeliveryHeader: "", + defaultGitHubHookIDHeader: "", + defaultGitHubSignature256Header: "", + }, + }, Path: defaultPath, HealthPath: defaultHealthPath, }, diff --git a/receiver/githubreceiver/go.mod b/receiver/githubreceiver/go.mod index 6dfd1d711c8e..37b490fc0cd6 100644 --- a/receiver/githubreceiver/go.mod +++ b/receiver/githubreceiver/go.mod @@ -14,6 +14,7 @@ require ( go.opentelemetry.io/collector/component/componentstatus v0.118.1-0.20250131104636-a737a48402e0 go.opentelemetry.io/collector/component/componenttest v0.118.1-0.20250131104636-a737a48402e0 go.opentelemetry.io/collector/config/confighttp v0.118.1-0.20250131104636-a737a48402e0 + go.opentelemetry.io/collector/config/configopaque v1.24.1-0.20250131104636-a737a48402e0 go.opentelemetry.io/collector/confmap v1.24.1-0.20250131104636-a737a48402e0 go.opentelemetry.io/collector/consumer v1.24.1-0.20250131104636-a737a48402e0 go.opentelemetry.io/collector/consumer/consumertest v0.118.1-0.20250131104636-a737a48402e0 @@ -75,13 +76,12 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect - github.com/vektah/gqlparser/v2 v2.5.20 // indirect + github.com/vektah/gqlparser/v2 v2.5.22 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/collector/client v1.24.1-0.20250131104636-a737a48402e0 // indirect go.opentelemetry.io/collector/config/configauth v0.118.1-0.20250131104636-a737a48402e0 // indirect go.opentelemetry.io/collector/config/configcompression v1.24.1-0.20250131104636-a737a48402e0 // indirect - go.opentelemetry.io/collector/config/configopaque v1.24.1-0.20250131104636-a737a48402e0 // indirect go.opentelemetry.io/collector/config/configtelemetry v0.118.1-0.20250131104636-a737a48402e0 // indirect go.opentelemetry.io/collector/config/configtls v1.24.1-0.20250131104636-a737a48402e0 // indirect go.opentelemetry.io/collector/confmap/provider/envprovider v1.24.1-0.20250131104636-a737a48402e0 // indirect diff --git a/receiver/githubreceiver/go.sum b/receiver/githubreceiver/go.sum index f91680319034..544581948229 100644 --- a/receiver/githubreceiver/go.sum +++ b/receiver/githubreceiver/go.sum @@ -118,8 +118,8 @@ github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08 github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= -github.com/vektah/gqlparser/v2 v2.5.20 h1:kPaWbhBntxoZPaNdBaIPT1Kh0i1b/onb5kXgEdP5JCo= -github.com/vektah/gqlparser/v2 v2.5.20/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnHZdw7tfhEGfTM= +github.com/vektah/gqlparser/v2 v2.5.22 h1:yaaeJ0fu+nv1vUMW0Hl+aS1eiv1vMfapBNjpffAda1I= +github.com/vektah/gqlparser/v2 v2.5.22/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnHZdw7tfhEGfTM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= diff --git a/receiver/githubreceiver/model.go b/receiver/githubreceiver/model.go new file mode 100644 index 000000000000..526f914db85a --- /dev/null +++ b/receiver/githubreceiver/model.go @@ -0,0 +1,259 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package githubreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/githubreceiver" + +import ( + "errors" + "strings" + + "github.com/google/go-github/v68/github" + "go.opentelemetry.io/collector/pdata/pcommon" + semconv "go.opentelemetry.io/collector/semconv/v1.27.0" +) + +// model.go contains specific attributes from the 1.28 and 1.29 releases of +// SemConv. They are manually added due to issue +// https://github.com/open-telemetry/weaver/issues/227 which will migrate code +// gen to weaver. Once that is done, these attributes will be migrated to the +// semantic conventions package. +const ( + // vcs.change.state with enum values of open, closed, or merged. + AttributeVCSChangeState = "vcs.change.state" + AttributeVCSChangeStateOpen = "open" + AttributeVCSChangeStateClosed = "closed" + AttributeVCSChangeStateMerged = "merged" + + // vcs.change.title + AttributeVCSChangeTitle = "vcs.change.title" + + // vcs.change.id + AttributeVCSChangeID = "vcs.change.id" + + // vcs.revision_delta.direction with enum values of behind or ahead. + AttributeVCSRevisionDeltaDirection = "vcs.revision_delta.direction" + AttributeVCSRevisionDeltaDirectionBehind = "behind" + AttributeVCSRevisionDeltaDirectionAhead = "ahead" + + // vcs.line_change.type with enum values of added or removed. + AttributeVCSLineChangeType = "vcs.line_change.type" + AttributeVCSLineChangeTypeAdded = "added" + AttributeVCSLineChangeTypeRemoved = "removed" + + // vcs.ref.type with enum values of branch or tag. + AttributeVCSRefType = "vcs.ref.type" + AttributeVCSRefTypeBranch = "branch" + AttributeVCSRefTypeTag = "tag" + + // vcs.repository.name + AttributeVCSRepositoryName = "vcs.repository.name" + + // vcs.ref.base.name + AttributeVCSRefBase = "vcs.ref.base" + + // vcs.ref.base.revision + AttributeVCSRefBaseRevision = "vcs.ref.base.revision" + + // vcs.ref.base.type with enum values of branch or tag. + AttributeVCSRefBaseType = "vcs.ref.base.type" + AttributeVCSRefBaseTypeBranch = "branch" + AttributeVCSRefBaseTypeTag = "tag" + + // vcs.ref.head.name + AttributeVCSRefHead = "vcs.ref.head" + + // vcs.ref.head.revision + AttributeVCSRefHeadRevision = "vcs.ref.head.revision" + + // vcs.ref.head.type with enum values of branch or tag. + AttributeVCSRefHeadType = "vcs.ref.head.type" + AttributeVCSRefHeadTypeBranch = "branch" + AttributeVCSRefHeadTypeTag = "tag" + + // The following prototype attributes that do not exist yet in semconv. + // They are highly experimental and subject to change. + + AttributeCICDPipelineRunURLFull = "cicd.pipeline.run.url.full" // equivalent to GitHub's `html_url` + + // These are being added in https://github.com/open-telemetry/semantic-conventions/pull/1681 + AttributeCICDPipelineRunStatus = "cicd.pipeline.run.status" // equivalent to GitHub's `conclusion` + AttributeCICDPipelineRunStatusSuccess = "success" + AttributeCICDPipelineRunStatusFailure = "failure" + AttributeCICDPipelineRunStatusCancellation = "cancellation" + AttributeCICDPipelineRunStatusError = "error" + AttributeCICDPipelineRunStatusSkip = "skip" + + AttributeCICDPipelineTaskRunStatus = "cicd.pipeline.run.task.status" // equivalent to GitHub's `conclusion` + AttributeCICDPipelineTaskRunStatusSuccess = "success" + AttributeCICDPipelineTaskRunStatusFailure = "failure" + AttributeCICDPipelineTaskRunStatusCancellation = "cancellation" + AttributeCICDPipelineTaskRunStatusError = "error" + AttributeCICDPipelineTaskRunStatusSkip = "skip" + + // The following attributes are not part of the semantic conventions yet. + AttributeCICDPipelineRunSenderLogin = "cicd.pipeline.run.sender.login" // GitHub's Run Sender Login + AttributeCICDPipelineTaskRunSenderLogin = "cicd.pipeline.task.run.sender.login" // GitHub's Task Sender Login + AttributeVCSVendorName = "vcs.vendor.name" // GitHub + AttributeVCSRepositoryOwner = "vcs.repository.owner" // GitHub's Owner Login + AttributeCICDPipelineFilePath = "cicd.pipeline.file.path" // GitHub's Path in workflow_run + AttributeCICDPipelinePreviousAttemptURLFull = "cicd.pipeline.run.previous_attempt.url.full" + + AttributeGitHubAppInstallationID = "github.app.installation.id" // GitHub's Installation ID + AttributeGitHubWorkflowRunAttempt = "github.workflow.run.attempt" // GitHub's Run Attempt + + // SECURITY: This information will always exist on the repository, but may + // be considered private if the repository is set to private. Care should be + // taken in the data pipeline for sanitizing sensitive user information if + // the user deems it as such. + AttributeVCSRefHeadRevisionAuthorName = "vcs.ref.head.revision.author.name" // GitHub's Head Revision Author Name + AttributeVCSRefHeadRevisionAuthorEmail = "vcs.ref.head.revision.author.email" // GitHub's Head Revision Author Email + + // The following attributes are exclusive to GitHub but not listed under + // Vendor Extensions within Semantic Conventions yet. + AttributeGitHubWorkflowTriggerActorUsername = "github.workflow.trigger.actor.username" // GitHub's Triggering Actor Username + + // github.reference.workflow acts as a template attribute where it'll be + // joined with a `name` and a `version` value. There is an unknown amount of + // reference workflows that are sent as a list of string by GitHub making it + // necessary to leverage template attributes. One key thing to note is the + // length of the names. Evaluate if this causes issues. + // eg. github.reference.workflow.my-great-workflow.path + // eg. github.reference.workflow.my-great-workflow.version + // eg. github.reference.workflow.my-great-workflow.revision + AttributeGitHubReferenceWorkflow = "github.reference.workflow" +) + +// getWorkflowAttrs returns a pcommon.Map of attributes for the Workflow Run +// GitHub event type and an error if one occurs. The attributes are associated +// with the originally provided resource. +func (gtr *githubTracesReceiver) getWorkflowAttrs(resource pcommon.Resource, e *github.WorkflowRunEvent) error { + attrs := resource.Attributes() + var err error + + svc, err := gtr.getServiceName(e.GetRepo().CustomProperties["service_name"], e.GetRepo().GetName()) + if err != nil { + err = errors.New("failed to get service.name") + } + + attrs.PutStr(semconv.AttributeServiceName, svc) + + // VCS Attributes + attrs.PutStr(AttributeVCSRepositoryName, e.GetRepo().GetName()) + attrs.PutStr(AttributeVCSVendorName, "github") + attrs.PutStr(AttributeVCSRefHead, e.GetWorkflowRun().GetHeadBranch()) + attrs.PutStr(AttributeVCSRefHeadType, AttributeVCSRefHeadTypeBranch) + attrs.PutStr(AttributeVCSRefHeadRevision, e.GetWorkflowRun().GetHeadSHA()) + attrs.PutStr(AttributeVCSRefHeadRevisionAuthorName, e.GetWorkflowRun().GetHeadCommit().GetCommitter().GetName()) + attrs.PutStr(AttributeVCSRefHeadRevisionAuthorEmail, e.GetWorkflowRun().GetHeadCommit().GetCommitter().GetEmail()) + + // CICD Attributes + attrs.PutStr(semconv.AttributeCicdPipelineName, e.GetWorkflowRun().GetName()) + attrs.PutStr(AttributeCICDPipelineRunSenderLogin, e.GetSender().GetLogin()) + attrs.PutStr(AttributeCICDPipelineRunURLFull, e.GetWorkflowRun().GetHTMLURL()) + attrs.PutInt(semconv.AttributeCicdPipelineRunID, e.GetWorkflowRun().GetID()) + switch status := e.GetWorkflowRun().GetConclusion(); status { + case "success": + attrs.PutStr(AttributeCICDPipelineRunStatus, AttributeCICDPipelineRunStatusSuccess) + case "failure": + attrs.PutStr(AttributeCICDPipelineRunStatus, AttributeCICDPipelineRunStatusFailure) + case "skipped": + attrs.PutStr(AttributeCICDPipelineRunStatus, AttributeCICDPipelineRunStatusSkip) + case "cancelled": + attrs.PutStr(AttributeCICDPipelineRunStatus, AttributeCICDPipelineRunStatusCancellation) + // Default sets to whatever is provided by the event. GitHub provides the + // following additional values: neutral, timed_out, action_required, stale, + // startup_failure, and null. + default: + attrs.PutStr(AttributeCICDPipelineRunStatus, status) + } + + if e.GetWorkflowRun().GetPreviousAttemptURL() != "" { + htmlURL := replaceAPIURL(e.GetWorkflowRun().GetPreviousAttemptURL()) + attrs.PutStr(AttributeCICDPipelinePreviousAttemptURLFull, htmlURL) + } + + // Determine if there are any referenced (shared) workflows imported by the + // Workflow run and generated the temmplated attributes for them. + if len(e.GetWorkflowRun().ReferencedWorkflows) > 0 { + for _, w := range e.GetWorkflowRun().ReferencedWorkflows { + var name string + name, err = splitRefWorkflowPath(w.GetPath()) + if err != nil { + return err + } + + template := AttributeGitHubReferenceWorkflow + "." + name + pathAttr := template + ".path" + revAttr := template + ".revision" + versionAttr := template + ".version" + + attrs.PutStr(pathAttr, w.GetPath()) + attrs.PutStr(revAttr, w.GetSHA()) + attrs.PutStr(versionAttr, w.GetRef()) + } + } + + return err +} + +// splitRefWorkflowPath splits the reference workflow path into just the file +// name normalized to lowercase without the file type. +func splitRefWorkflowPath(path string) (fileName string, err error) { + parts := strings.Split(path, "@") + if len(parts) != 2 { + return "", errors.New("invalid reference workflow path") + } + + parts = strings.Split(parts[0], "/") + if len(parts) == 0 { + return "", errors.New("invalid reference workflow path") + } + + last := parts[len(parts)-1] + parts = strings.Split(last, ".") + if len(parts) == 0 { + return "", errors.New("invalid reference workflow path") + } + + return strings.ToLower(parts[0]), nil +} + +// getServiceName returns a generated service.name resource attribute derived +// from 1) the service_name defined in the webhook configuration 2) a +// service.name value set in the custom_properties section of a GitHub event, or +// 3) the repository name. The value returned in those cases will always be a +// formatted string; where the string will be lowercase and underscores will be +// replaced by hyphens. If none of these are set, it returns "unknown_service" +// and an error. +// func (gtr *githubTracesReceiver) getServiceName(customProps map[string]interface{}, repoName string) (string, error) { +func (gtr *githubTracesReceiver) getServiceName(customProps any, repoName string) (string, error) { + switch { + case gtr.cfg.WebHook.ServiceName != "": + formatted := formatString(gtr.cfg.WebHook.ServiceName) + return formatted, nil + // customProps would be an index map[string]interface{} passed in but should + // only be non-nil if the index of `service_name` exists + case customProps != nil: + formatted := formatString(customProps.(string)) + return formatted, nil + case repoName != "": + formatted := formatString(repoName) + return formatted, nil + default: + // This should never happen, but in the event it does, unknown_service + // and a error will be returned to abide by semantic conventions. + return "unknown_service", errors.New("unable to generate service.name resource attribute") + } +} + +// formatString formats a string to lowercase and replaces underscores with +// hyphens. +func formatString(input string) string { + return strings.ToLower(strings.ReplaceAll(input, "_", "-")) +} + +// replaceAPIURL replaces a GitHub API URL with the HTML URL version. +func replaceAPIURL(apiURL string) (htmlURL string) { + // TODO: Support enterpise server configuration with custom domain. + return strings.Replace(apiURL, "api.github.com/repos", "github.com", 1) +} diff --git a/receiver/githubreceiver/model_test.go b/receiver/githubreceiver/model_test.go new file mode 100644 index 000000000000..e61e60b8fb93 --- /dev/null +++ b/receiver/githubreceiver/model_test.go @@ -0,0 +1,125 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package githubreceiver + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormatString(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "converts underscores to hyphens", + input: "hello_world", + expected: "hello-world", + }, + { + name: "converts to lowercase", + input: "HELLO_WORLD", + expected: "hello-world", + }, + { + name: "handles mixed case and multiple underscores", + input: "Hello_Big_WORLD", + expected: "hello-big-world", + }, + { + name: "handles string with no underscores", + input: "HelloWorld", + expected: "helloworld", + }, + { + name: "handles empty string", + input: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatString(tt.input) + if got != tt.expected { + t.Errorf("formatString() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestSplitRefWorkflowPath(t *testing.T) { + tests := []struct { + name string + input string + expected string + wantErr bool + }{ + { + name: "simple workflow with version", + input: "my-great-workflow@v1.0.0", + expected: "my-great-workflow", + wantErr: false, + }, + { + name: "workflow with SHA", + input: "my-great-workflow@3421498310493281409328140932840192384", + expected: "my-great-workflow", + wantErr: false, + }, + { + name: "full path workflow", + input: "org/repo/.github/my-file-path/with/folder/build-woot.yaml@v0.2.3", + expected: "build-woot", + wantErr: false, + }, + { + name: "uppercase file", + input: "org/repo/.github/my-file-path/with/folder/BUILD-WOOT.yaml@v0.2.3", + expected: "build-woot", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := splitRefWorkflowPath(tt.input) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestReplaceAPIURL(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "converts api.github.com URL to html URL", + input: "https://api.github.com/repos/open-telemetry/opentelemetry-collector-contrib/pull/1234", + expected: "https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/1234", + }, + { + name: "converts api.github.com workflow URL to html URL", + input: "https://api.github.com/repos/open-telemetry/opentelemetry-collector-contrib/actions/runs/1234", + expected: "https://github.com/open-telemetry/opentelemetry-collector-contrib/actions/runs/1234", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := replaceAPIURL(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/receiver/githubreceiver/testdata/config.yaml b/receiver/githubreceiver/testdata/config.yaml index 612b31b0213a..0c9666cae89d 100644 --- a/receiver/githubreceiver/testdata/config.yaml +++ b/receiver/githubreceiver/testdata/config.yaml @@ -10,9 +10,8 @@ receivers: write_timeout: "500ms" path: "some/path" health_path: "health/path" - required_header: - key: key-present - value: value-present + required_headers: + key: value-present github/customname: initial_delay: 1s @@ -25,9 +24,8 @@ receivers: write_timeout: "500ms" path: "some/path" health_path: "health/path" - required_header: - key: key-present - value: value-present + required_headers: + key: value-present processors: nop: diff --git a/receiver/githubreceiver/trace_event_handling.go b/receiver/githubreceiver/trace_event_handling.go new file mode 100644 index 000000000000..cbe28bfe1475 --- /dev/null +++ b/receiver/githubreceiver/trace_event_handling.go @@ -0,0 +1,140 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package githubreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/githubreceiver" + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + + "github.com/google/go-github/v68/github" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.uber.org/zap" +) + +func (gtr *githubTracesReceiver) handleWorkflowRun(e *github.WorkflowRunEvent) (ptrace.Traces, error) { + t := ptrace.NewTraces() + r := t.ResourceSpans().AppendEmpty() + + resource := r.Resource() + + err := gtr.getWorkflowAttrs(resource, e) + if err != nil { + return ptrace.Traces{}, fmt.Errorf("failed to get workflow attributes: %w", err) + } + + traceID, err := newTraceID(e.GetWorkflowRun().GetID(), e.GetWorkflowRun().GetRunAttempt()) + if err != nil { + gtr.logger.Sugar().Errorf("Failed to generate trace ID", zap.String("error", fmt.Sprint(err))) + } + + err = gtr.createRootSpan(r, e, traceID) + if err != nil { + gtr.logger.Error("Failed to create root span", zap.Error(err)) + return ptrace.Traces{}, errors.New("failed to create root span") + } + return t, nil +} + +// TODO: Add and implement handleWorkflowJob, tying corresponding job spans to +// the proper root span and trace ID. +// func (gtr *githubTracesReceiver) handleWorkflowJob(ctx context.Context, e *github.WorkflowJobEvent) (ptrace.Traces, error) { +// t := ptrace.NewTraces() +// r := t.ResourceSpans().AppendEmpty() +// s := r.InstrumentationLibrarySpans().AppendEmpty() +// return t, nil +// } + +// newTraceID creates a deterministic Trace ID based on the provided +// inputs of runID and runAttempt. +func newTraceID(runID int64, runAttempt int) (pcommon.TraceID, error) { + // Original implementation appended `t` to TraceId's and `s` to + // ParentSpanId's This was done to separate the two types of IDs eventhough + // SpanID's are only 8 bytes, while TraceID's are 16 bytes. + // TODO: Determine if there is a better way to handle this. + input := fmt.Sprintf("%d%dt", runID, runAttempt) + // TODO: Determine if this is the best hashing algorithm to use. This is + // more likely to generate a unique hash compared to MD5 or SHA1. Could + // alternatively use UUID library to generate a unique ID by also using a + // hash. + hash := sha256.Sum256([]byte(input)) + idHex := hex.EncodeToString(hash[:]) + + var id pcommon.TraceID + _, err := hex.Decode(id[:], []byte(idHex[:32])) + if err != nil { + return pcommon.TraceID{}, err + } + + return id, nil +} + +// newParentId creates a deterministic Parent Span ID based on the provided +// runID and runAttempt. `s` is appended to the end of the input to +// differentiate between a deterministic traceID and the parentSpanID. +func newParentSpanID(runID int64, runAttempt int) (pcommon.SpanID, error) { + input := fmt.Sprintf("%d%ds", runID, runAttempt) + hash := sha256.Sum256([]byte(input)) + spanIDHex := hex.EncodeToString(hash[:]) + + var spanID pcommon.SpanID + _, err := hex.Decode(spanID[:], []byte(spanIDHex[16:32])) + if err != nil { + return pcommon.SpanID{}, err + } + + return spanID, nil +} + +// createRootSpan creates a root span based on the provided event, associated +// with the deterministic traceID. +func (gtr *githubTracesReceiver) createRootSpan( + resourceSpans ptrace.ResourceSpans, + event *github.WorkflowRunEvent, + traceID pcommon.TraceID, +) error { + scopeSpans := resourceSpans.ScopeSpans().AppendEmpty() + span := scopeSpans.Spans().AppendEmpty() + + rootSpanID, err := newParentSpanID(event.GetWorkflowRun().GetID(), event.GetWorkflowRun().GetRunAttempt()) + if err != nil { + return fmt.Errorf("failed to generate root span ID: %w", err) + } + + span.SetTraceID(traceID) + span.SetSpanID(rootSpanID) + span.SetName(event.GetWorkflowRun().GetName()) + span.SetKind(ptrace.SpanKindServer) + span.SetStartTimestamp(pcommon.NewTimestampFromTime(event.GetWorkflowRun().GetRunStartedAt().Time)) + span.SetEndTimestamp(pcommon.NewTimestampFromTime(event.GetWorkflowRun().GetUpdatedAt().Time)) + + switch event.WorkflowRun.GetConclusion() { + case "success": + span.Status().SetCode(ptrace.StatusCodeOk) + case "failure": + span.Status().SetCode(ptrace.StatusCodeError) + default: + span.Status().SetCode(ptrace.StatusCodeUnset) + } + + span.Status().SetMessage(event.GetWorkflowRun().GetConclusion()) + + // 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") + previousRunAttempt := event.GetWorkflowRun().GetRunAttempt() - 1 + previousTraceID, err := newTraceID(event.GetWorkflowRun().GetID(), previousRunAttempt) + if err != nil { + return fmt.Errorf("failed to generate previous traceID: %w", err) + } + + link := span.Links().AppendEmpty() + link.SetTraceID(previousTraceID) + gtr.logger.Debug("successfully linked to previous trace ID", zap.String("previousTraceID", previousTraceID.String())) + } + + return nil +} diff --git a/receiver/githubreceiver/trace_event_handling_test.go b/receiver/githubreceiver/trace_event_handling_test.go new file mode 100644 index 000000000000..1e3c9de6bbb9 --- /dev/null +++ b/receiver/githubreceiver/trace_event_handling_test.go @@ -0,0 +1,217 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package githubreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/githubreceiver" + +import ( + "testing" + "time" + + "github.com/google/go-github/v68/github" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/collector/receiver/receivertest" + "go.uber.org/zap" +) + +func TestHandleWorkflowRun(t *testing.T) { + // Helper function to create a test workflow run event + createTestWorkflowRunEvent := func(id int64, runAttempt int, name, conclusion string, startTime, updateTime time.Time) *github.WorkflowRunEvent { + return &github.WorkflowRunEvent{ + WorkflowRun: &github.WorkflowRun{ + ID: &id, + Name: &name, + RunAttempt: &runAttempt, + RunStartedAt: &github.Timestamp{Time: startTime}, + UpdatedAt: &github.Timestamp{Time: updateTime}, + Conclusion: &conclusion, + }, + Repo: &github.Repository{ + Name: github.Ptr("test-repo"), + Organization: &github.Organization{Login: github.Ptr("test-org")}, + DefaultBranch: github.Ptr("main"), + CustomProperties: map[string]any{ + "service_name": "test-service", + }, + }, + Workflow: &github.Workflow{ + Name: github.Ptr("test-workflow"), + Path: github.Ptr(".github/workflows/test.yml"), + }, + } + } + + tests := []struct { + name string + event *github.WorkflowRunEvent + wantErr bool + validate func(t *testing.T, traces ptrace.Traces) + }{ + { + name: "successful workflow run", + event: createTestWorkflowRunEvent( + 123, + 1, + "Test Workflow", + "success", + time.Now().Add(-time.Hour), + time.Now(), + ), + wantErr: false, + validate: func(t *testing.T, traces ptrace.Traces) { + require.Equal(t, 1, traces.ResourceSpans().Len()) + + rs := traces.ResourceSpans().At(0) + require.Equal(t, 1, rs.ScopeSpans().Len()) + + spans := rs.ScopeSpans().At(0).Spans() + require.Equal(t, 1, spans.Len()) + + span := spans.At(0) + require.Equal(t, "Test Workflow", span.Name()) + require.Equal(t, ptrace.SpanKindServer, span.Kind()) + require.Equal(t, ptrace.StatusCodeOk, span.Status().Code()) + require.Equal(t, "success", span.Status().Message()) + }, + }, + { + name: "failed workflow run", + event: createTestWorkflowRunEvent( + 124, + 1, + "Test Workflow", + "failure", + time.Now().Add(-time.Hour), + time.Now(), + ), + wantErr: false, + validate: func(t *testing.T, traces ptrace.Traces) { + spans := traces.ResourceSpans().At(0).ScopeSpans().At(0).Spans() + span := spans.At(0) + require.Equal(t, ptrace.StatusCodeError, span.Status().Code()) + require.Equal(t, "failure", span.Status().Message()) + }, + }, + { + name: "workflow run with retry", + event: func() *github.WorkflowRunEvent { + e := createTestWorkflowRunEvent( + 125, + 2, + "Test Workflow", + "success", + time.Now().Add(-time.Hour), + time.Now(), + ) + previousURL := "https://api.github.com/repos/test-org/test-repo/actions/runs/125/attempts/1" + e.WorkflowRun.PreviousAttemptURL = &previousURL + return e + }(), + wantErr: false, + validate: func(t *testing.T, traces ptrace.Traces) { + spans := traces.ResourceSpans().At(0).ScopeSpans().At(0).Spans() + span := spans.At(0) + require.Equal(t, 1, span.Links().Len()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a new receiver with a test logger + logger := zap.NewNop() + receiver := &githubTracesReceiver{ + logger: logger, + cfg: createDefaultConfig().(*Config), + settings: receivertest.NewNopSettings(), + } + + // Handle the workflow run event + traces, err := receiver.handleWorkflowRun(tt.event) + + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + tt.validate(t, traces) + }) + } +} + +func TestNewParentSpanID(t *testing.T) { + tests := []struct { + name string + runID int64 + runAttempt int + wantError bool + }{ + { + name: "basic span ID generation", + runID: 12345, + runAttempt: 1, + wantError: false, + }, + { + name: "different run ID", + runID: 54321, + runAttempt: 1, + wantError: false, + }, + { + name: "different attempt", + runID: 12345, + runAttempt: 2, + wantError: false, + }, + { + name: "zero values", + runID: 0, + runAttempt: 0, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // First call to get span ID + spanID1, err1 := newParentSpanID(tt.runID, tt.runAttempt) + + if tt.wantError { + require.Error(t, err1) + return + } + require.NoError(t, err1) + + // Verify span ID is not empty + require.NotEqual(t, pcommon.SpanID{}, spanID1, "span ID should not be empty") + + // Verify consistent results for same input + spanID2, err2 := newParentSpanID(tt.runID, tt.runAttempt) + require.NoError(t, err2) + require.Equal(t, spanID1, spanID2, "same inputs should generate same span ID") + + // Verify different inputs generate different span IDs + differentSpanID, err3 := newParentSpanID(tt.runID+1, tt.runAttempt) + require.NoError(t, err3) + require.NotEqual(t, spanID1, differentSpanID, "different inputs should generate different span IDs") + }) + } +} + +func TestNewParentSpanID_Consistency(t *testing.T) { + // Test that generates the same span ID for same inputs across multiple calls + runID := int64(12345) + runAttempt := 1 + + spanID1, err1 := newParentSpanID(runID, runAttempt) + require.NoError(t, err1) + + for i := 0; i < 5; i++ { + spanID2, err2 := newParentSpanID(runID, runAttempt) + require.NoError(t, err2) + require.Equal(t, spanID1, spanID2, "span ID should be consistent across multiple calls") + } +} diff --git a/receiver/githubreceiver/traces_receiver.go b/receiver/githubreceiver/trace_receiver.go similarity index 62% rename from receiver/githubreceiver/traces_receiver.go rename to receiver/githubreceiver/trace_receiver.go index 13486b70ab1d..0c0e88b637e7 100644 --- a/receiver/githubreceiver/traces_receiver.go +++ b/receiver/githubreceiver/trace_receiver.go @@ -15,6 +15,7 @@ import ( "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/component/componentstatus" "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/receiver" "go.opentelemetry.io/collector/receiver/receiverhelper" "go.uber.org/zap" @@ -93,6 +94,9 @@ func (gtr *githubTracesReceiver) Start(ctx context.Context, host component.Host) // setup health route router.HandleFunc(gtr.cfg.WebHook.HealthPath, gtr.handleHealthCheck) + // setup webhook route for traces + router.HandleFunc(gtr.cfg.WebHook.Path, gtr.handleReq) + // webhook server standup and configuration gtr.server, err = gtr.cfg.WebHook.ServerConfig.ToServer(ctx, host, gtr.settings.TelemetrySettings, router) if err != nil { @@ -124,6 +128,66 @@ func (gtr *githubTracesReceiver) Shutdown(_ context.Context) error { return err } +// handleReq handles incoming request sent to the webhook endoint. On success +// returns a 200 response code. +func (gtr *githubTracesReceiver) handleReq(w http.ResponseWriter, req *http.Request) { + ctx := gtr.obsrecv.StartTracesOp(req.Context()) + + p, err := github.ValidatePayload(req, []byte(gtr.cfg.WebHook.Secret)) + if err != nil { + gtr.logger.Sugar().Debugf("unable to validate payload", zap.Error(err)) + http.Error(w, "invalid payload", http.StatusBadRequest) + return + } + + eventType := github.WebHookType(req) + event, err := github.ParseWebHook(eventType, p) + if err != nil { + gtr.logger.Sugar().Debugf("failed to parse event", zap.Error(err)) + http.Error(w, "failed to parse event", http.StatusBadRequest) + return + } + + var td ptrace.Traces + switch e := event.(type) { + case *github.WorkflowRunEvent: + if e.GetWorkflowRun().GetStatus() != "completed" { + gtr.logger.Debug("workflow run not complete, skipping...", zap.String("status", e.GetWorkflowRun().GetStatus())) + w.WriteHeader(http.StatusNoContent) + return + } + td, err = gtr.handleWorkflowRun(e) + case *github.WorkflowJobEvent: + if e.GetWorkflowJob().GetStatus() != "completed" { + gtr.logger.Debug("workflow job not complete, skipping...", zap.String("status", e.GetWorkflowJob().GetStatus())) + w.WriteHeader(http.StatusNoContent) + return + } + return + // TODO: Enable when handleWorkflowJob is implemented + // td, err = gtr.handleWorkflowJob(ctx, e) + case *github.PingEvent: + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + return + default: + gtr.logger.Sugar().Debugf("event type not supported", zap.String("event_type", eventType)) + http.Error(w, "event type not supported", http.StatusBadRequest) + return + } + + if td.SpanCount() > 0 { + err = gtr.traceConsumer.ConsumeTraces(ctx, td) + if err != nil { + http.Error(w, "failed to consume traces", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + } + + gtr.obsrecv.EndTracesOp(ctx, "protobuf", td.SpanCount(), err) +} + // Simple healthcheck endpoint. func (gtr *githubTracesReceiver) handleHealthCheck(w http.ResponseWriter, _ *http.Request) { w.Header().Add("Content-Type", "application/json") diff --git a/receiver/githubreceiver/traces_receiver_test.go b/receiver/githubreceiver/trace_receiver_test.go similarity index 100% rename from receiver/githubreceiver/traces_receiver_test.go rename to receiver/githubreceiver/trace_receiver_test.go