receiver/githubreceiver/model.go (200 lines of code) (raw):

// 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/v70/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" 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 AttributeCICDPipelineFilePath = "cicd.pipeline.file.path" // GitHub's Path in workflow_run AttributeCICDPipelinePreviousAttemptURLFull = "cicd.pipeline.run.previous_attempt.url.full" AttributeCICDPipelineWorkerID = "cicd.pipeline.worker.id" // GitHub's Runner ID AttributeCICDPipelineWorkerGroupID = "cicd.pipeline.worker.group.id" // GitHub's Runner Group ID AttributeCICDPipelineWorkerName = "cicd.pipeline.worker.name" // GitHub's Runner Name AttributeCICDPipelineWorkerGroupName = "cicd.pipeline.worker.group.name" // GitHub's Runner Group Name AttributeCICDPipelineWorkerNodeID = "cicd.pipeline.worker.node.id" // GitHub's Runner Node ID AttributeCICDPipelineWorkerLabels = "cicd.pipeline.worker.labels" // GitHub's Runner Labels // The following attributes are exclusive to GitHub but not listed under // Vendor Extensions within Semantic Conventions yet. AttributeGitHubAppInstallationID = "github.app.installation.id" // GitHub's Installation ID AttributeGitHubWorkflowRunAttempt = "github.workflow.run.attempt" // GitHub's Run Attempt 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 strings 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. // WARNING: Extremely long workflow file names could create extremely long // attribute keys which could lead to unknown issues in the backend and // create additional memory usage overhead when processing data (though // unlikely). // TODO: Evaluate if there is a need to truncate long workflow files names. // 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" // 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 AttributeVCSRepositoryOwner = "vcs.repository.owner" // GitHub's Owner Login AttributeVCSVendorName = "vcs.vendor.name" // GitHub ) // getWorkflowRunAttrs 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) getWorkflowRunAttrs(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 := strings.ToLower(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 listed in the // Workflow Run event and generate the templated 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 } // getWorkflowJobAttrs returns a pcommon.Map of attributes for the Workflow Job // GitHub event type and an error if one occurs. The attributes are associated // with the originally provided resource. func (gtr *githubTracesReceiver) getWorkflowJobAttrs(resource pcommon.Resource, e *github.WorkflowJobEvent) 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.GetWorkflowJob().GetHeadBranch()) attrs.PutStr(AttributeVCSRefHeadType, AttributeVCSRefHeadTypeBranch) attrs.PutStr(AttributeVCSRefHeadRevision, e.GetWorkflowJob().GetHeadSHA()) // CICD Worker (GitHub Runner) Attributes attrs.PutInt(AttributeCICDPipelineWorkerID, e.GetWorkflowJob().GetRunnerID()) attrs.PutInt(AttributeCICDPipelineWorkerGroupID, e.GetWorkflowJob().GetRunnerGroupID()) attrs.PutStr(AttributeCICDPipelineWorkerName, e.GetWorkflowJob().GetRunnerName()) attrs.PutStr(AttributeCICDPipelineWorkerGroupName, e.GetWorkflowJob().GetRunnerGroupName()) attrs.PutStr(AttributeCICDPipelineWorkerNodeID, e.GetWorkflowJob().GetNodeID()) if len(e.GetWorkflowJob().Labels) > 0 { labels := attrs.PutEmptySlice(AttributeCICDPipelineWorkerLabels) labels.EnsureCapacity(len(e.GetWorkflowJob().Labels)) for _, label := range e.GetWorkflowJob().Labels { l := strings.ToLower(label) labels.AppendEmpty().SetStr(l) } } // CICD Attributes attrs.PutStr(semconv.AttributeCicdPipelineName, e.GetWorkflowJob().GetName()) attrs.PutStr(AttributeCICDPipelineTaskRunSenderLogin, e.GetSender().GetLogin()) attrs.PutStr(semconv.AttributeCicdPipelineTaskRunURLFull, e.GetWorkflowJob().GetHTMLURL()) attrs.PutInt(semconv.AttributeCicdPipelineTaskRunID, e.GetWorkflowJob().GetID()) switch status := strings.ToLower(e.GetWorkflowJob().GetConclusion()); status { case "success": attrs.PutStr(AttributeCICDPipelineTaskRunStatus, AttributeCICDPipelineTaskRunStatusSuccess) case "failure": attrs.PutStr(AttributeCICDPipelineTaskRunStatus, AttributeCICDPipelineTaskRunStatusFailure) case "skipped": attrs.PutStr(AttributeCICDPipelineTaskRunStatus, AttributeCICDPipelineTaskRunStatusSkip) case "cancelled": attrs.PutStr(AttributeCICDPipelineTaskRunStatus, AttributeCICDPipelineTaskRunStatusCancellation) // Default sets to whatever is provided by the event. GitHub provides the // following additional values: neutral, timed_out, action_required, stale, // and null. default: attrs.PutStr(AttributeCICDPipelineRunStatus, status) } 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" // according to the semantic conventions for service.name and an error. // https://opentelemetry.io/docs/specs/semconv/attributes-registry/service/#service-attributes 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) }