diff --git a/.buildkite/pipeline.release.yml b/.buildkite/pipeline.release.yml index 77542e1b..63cea827 100644 --- a/.buildkite/pipeline.release.yml +++ b/.buildkite/pipeline.release.yml @@ -26,6 +26,6 @@ steps: - aws-ssm#v1.0.0: parameters: GITHUB_RELEASE_ACCESS_TOKEN: /pipelines/buildkite/buildkite-agent-metrics/GITHUB_RELEASE_ACCESS_TOKEN - - docker-compose#v4.11.0: + - docker-compose#v4.14.0: config: .buildkite/docker-compose.yaml run: agent diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 795b5d85..240c7e52 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,43 +1,36 @@ steps: - name: ":test_tube: Test" key: test + command: go test -v -race ./... plugins: - - docker#v2.0.0: - image: "golang:1.20" - command: ["go", "test", "-v", "-race", "."] - environment: - - GO111MODULE=on + - docker#v5.9.0: + image: golang:1.21 - - name: ":golang: Compile" - key: build - plugins: - - golang-cross-compile#v1.3.0: - build: main.go - import: github.com/buildkite/buildkite-agent-metrics - targets: - - version: "1.20.2" - goos: linux - goarch: amd64 - gomodule: "on" - - version: "1.20.2" - goos: linux - goarch: arm64 - gomodule: "on" - - version: "1.20.2" - goos: windows - goarch: amd64 - gomodule: "on" - - version: "1.20.2" - goos: darwin - goarch: amd64 - gomodule: "on" - - version: "1.20.2" - goos: darwin - goarch: arm64 - gomodule: "on" + - group: ":hammer_and_wrench: Binary builds" + steps: + - name: ":{{matrix.os}}: Build {{matrix.os}} {{matrix.arch}} binary" + command: .buildkite/steps/build-binary.sh {{matrix.os}} {{matrix.arch}} + key: build-binary + depends_on: + - test + plugins: + - docker#v5.9.0: + image: golang:1.21 + mount-buildkite-agent: true + matrix: + setup: + os: + - darwin + - linux + - windows + arch: + - amd64 + - arm64 - name: ":lambda: Build Lambda" key: build-lambda + depends_on: + - test command: .buildkite/steps/build-lambda.sh - name: ":s3: Upload to S3" @@ -58,6 +51,6 @@ steps: - name: ":pipeline:" key: upload-release-steps depends_on: - - build + - build-binary - upload-to-s3 command: .buildkite/steps/upload-release-steps.sh diff --git a/.buildkite/steps/build-binary.sh b/.buildkite/steps/build-binary.sh new file mode 100755 index 00000000..2b3c6e38 --- /dev/null +++ b/.buildkite/steps/build-binary.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +set -eu + +GOOS=${1:-linux} +GOARCH=${2:-amd64} + +export GOOS +export GOARCH +export CGO_ENABLED=0 + +go build -o "buildkite-agent-metrics-${GOOS}-${GOARCH}" . +buildkite-agent artifact upload "buildkite-agent-metrics-${GOOS}-${GOARCH}" diff --git a/.buildkite/steps/build-lambda.sh b/.buildkite/steps/build-lambda.sh index 6fa70f00..6c843396 100755 --- a/.buildkite/steps/build-lambda.sh +++ b/.buildkite/steps/build-lambda.sh @@ -1,15 +1,17 @@ -#!/bin/bash +#!/usr/bin/env sh + set -eu docker run --rm --volume "$PWD:/code" \ - --workdir "/code" \ + --workdir /code \ --rm \ - golang:1.20 \ - sh -c "go get ./lambda && go build -o ./lambda/handler ./lambda" + --env CGO_ENABLED=0 \ + golang:1.21 \ + go build -tags lambda.norpc -o lambda/bootstrap ./lambda -chmod +x ./lambda/handler +chmod +x lambda/bootstrap mkdir -p dist/ -zip -j handler.zip lambda/handler +zip -j handler.zip lambda/bootstrap buildkite-agent artifact upload handler.zip diff --git a/CHANGELOG.md b/CHANGELOG.md index c1bf0c7e..eb1005c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,63 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [v5.9.3](https://github.com/buildkite/buildkite-agent-metrics/tree/v5.9.3) (2023-12-19) +[Full Changelog](https://github.com/buildkite/buildkite-agent-metrics/compare/v5.9.2...v5.9.3) + +### Changed +- Add v5 to module path [#248](https://github.com/buildkite/buildkite-agent-metrics/pull/248) (@DrJosh9000) + +### Dependencies +- build(deps): bump golang.org/x/crypto from 0.14.0 to 0.17.0 [#246](https://github.com/buildkite/buildkite-agent-metrics/pull/246) (@dependabot[bot]) +- build(deps): bump cloud.google.com/go/monitoring from 1.16.3 to 1.17.0 [#245](https://github.com/buildkite/buildkite-agent-metrics/pull/245) (@dependabot[bot]) +- build(deps): bump github.com/aws/aws-lambda-go from 1.41.0 to 1.42.0 [#244](https://github.com/buildkite/buildkite-agent-metrics/pull/244) (@dependabot[bot]) + +## [v5.9.2](https://github.com/buildkite/buildkite-agent-metrics/tree/v5.9.2) (2023-12-12) +[Full Changelog](https://github.com/buildkite/buildkite-agent-metrics/compare/v5.9.1...v5.9.2) + +### Fixed +- Fix non-Secrets Manager token providers [#243](https://github.com/buildkite/buildkite-agent-metrics/pull/243) (@DrJosh9000) + +### Changed +- Allow env vars to control debug logging for the lambda [#238](https://github.com/buildkite/buildkite-agent-metrics/pull/238) (@triarius) + +### Dependencies +- Bump github.com/aws/aws-sdk-go from 1.48.3 to 1.48.4 to 1.48.16 [#237](https://github.com/buildkite/buildkite-agent-metrics/pull/237), [#241](https://github.com/buildkite/buildkite-agent-metrics/pull/241) (@dependabot[bot]) + +## [v5.9.1](https://github.com/buildkite/buildkite-agent-metrics/tree/v5.9.1) (2023-11-27) +[Full Changelog](https://github.com/buildkite/buildkite-agent-metrics/compare/v5.9.0...v5.9.1) + +### Changed +- Support for multiple secrets manager secrets command seperated [#233](https://github.com/buildkite/buildkite-agent-metrics/pull/233) (@lucylura) + +### Fixed +- Ignore Cluster label/dimension/tag for empty unclustered queues. This may fix continuity errors when clusters are not used [#234](https://github.com/buildkite/buildkite-agent-metrics/pull/234) (@triarius) + +### Internal +- Document SSM Parameters names may be comma separated [#235](https://github.com/buildkite/buildkite-agent-metrics/pull/235) (@triarius) + +### Dependencies +- build(deps): bump github.com/aws/aws-sdk-go from 1.47.3 to 1.48.3 [#232](https://github.com/buildkite/buildkite-agent-metrics/pull/232) (@dependabot[bot]) + +## [v5.9.0](https://github.com/buildkite/buildkite-agent-metrics/tree/v5.9.0) (2023-11-22) +[Full Changelog](https://github.com/buildkite/buildkite-agent-metrics/compare/v5.8.0...v5.9.0) + +> [!WARNING] +> This release adds a new Cluster label/tag/dimension, which is populated when using agent cluster tokens. This may break continuity with existing time series! + +### Added +- Collect from multiple clusters [#227](https://github.com/buildkite/buildkite-agent-metrics/pull/227) (@DrJosh9000) +- feat(gcp): add env vars for buildkite queues and gcp project id [#212](https://github.com/buildkite/buildkite-agent-metrics/pull/212) (@NotArpit) + +### Fixed +- Change build process to better support `provided.al2` [#225](https://github.com/buildkite/buildkite-agent-metrics/pull/225) (@triarius) +- fix(collector): exit on 401 when queues specified [#211](https://github.com/buildkite/buildkite-agent-metrics/pull/211) (@NotArpit) +- Fix another reference to go1.x [#230](https://github.com/buildkite/buildkite-agent-metrics/pull/230) (@jradtilbrook) + +### Internal +- Split Collect [#226](https://github.com/buildkite/buildkite-agent-metrics/pull/226) (@DrJosh9000) +- Various dependency updates [#206](https://github.com/buildkite/buildkite-agent-metrics/pull/206), [#208](https://github.com/buildkite/buildkite-agent-metrics/pull/208), [#213](https://github.com/buildkite/buildkite-agent-metrics/pull/213), [#215](https://github.com/buildkite/buildkite-agent-metrics/pull/215), [#216](https://github.com/buildkite/buildkite-agent-metrics/pull/216), [#217](https://github.com/buildkite/buildkite-agent-metrics/pull/217), [#218](https://github.com/buildkite/buildkite-agent-metrics/pull/218), [#219](https://github.com/buildkite/buildkite-agent-metrics/pull/219), [#220](https://github.com/buildkite/buildkite-agent-metrics/pull/220), [#221](https://github.com/buildkite/buildkite-agent-metrics/pull/221), [#222](https://github.com/buildkite/buildkite-agent-metrics/pull/222), [#223](https://github.com/buildkite/buildkite-agent-metrics/pull/223) (@dependabot[bot]) + ## [v5.8.0](https://github.com/buildkite/buildkite-agent-metrics/tree/v5.8.0) (2023-09-15) [Full Changelog](https://github.com/buildkite/buildkite-agent-metrics/compare/v5.7.0...v5.8.0) diff --git a/Dockerfile b/Dockerfile index ec2d02c4..089424fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM golang:1.20 as builder +FROM golang:1.21 as builder WORKDIR /go/src/github.com/buildkite/buildkite-agent-metrics/ COPY . . RUN GO111MODULE=on GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o buildkite-agent-metrics . -FROM alpine:3.17 +FROM alpine:3.18 RUN apk update && apk add curl ca-certificates COPY --from=builder /go/src/github.com/buildkite/buildkite-agent-metrics/buildkite-agent-metrics . EXPOSE 8080 8125 diff --git a/Dockerfile.lambda b/Dockerfile.lambda index 2001aa6a..7d7a2438 100644 --- a/Dockerfile.lambda +++ b/Dockerfile.lambda @@ -4,4 +4,4 @@ RUN yum install -y unzip wget && \ wget "https://github.com/buildkite/buildkite-agent-metrics/releases/latest/download/handler.zip" && \ unzip handler.zip && rm -f handler.zip -ENTRYPOINT ["./handler"] +ENTRYPOINT ["./bootstrap"] diff --git a/README.md b/README.md index 919b0696..d1d63ff2 100644 --- a/README.md +++ b/README.md @@ -4,83 +4,114 @@ A command-line tool for collecting [Buildkite](https://buildkite.com/) agent met [![Build status](https://badge.buildkite.com/80d04fcde3a306bef44e77aadb1f1ffdc20ebb3c8f1f585a60.svg)](https://buildkite.com/buildkite/buildkite-agent-metrics) - ## Installing -Either download the latest binary from [Github Releases](https://github.com/buildkite/buildkite-agent-metrics/releases) or install with: +Either download the latest binary from +[Github Releases](https://github.com/buildkite/buildkite-agent-metrics/releases) or install with: ```bash -go get github.com/buildkite/buildkite-agent-metrics +go install github.com/buildkite/buildkite-agent-metrics/v5@latest ``` ## Running -Several running modes are supported. All of them require an Agent Registration Token, found on the [Buildkite Agents page](https://buildkite.com/organizations/-/agents). +Several running modes are supported. All of them require an Agent Registration +Token, found on the +[Buildkite Agents page](https://buildkite.com/organizations/-/agents). ### Running as a Daemon -The simplest deployment is to run as a long-running daemon that collects metrics across all queues in an organization. +The simplest deployment is to run as a long-running daemon that collects metrics +across all queues in an organization. -``` +```shell buildkite-agent-metrics -token abc123 -interval 30s ``` -Restrict it to a single queue with `-queue` if you're scaling a single cluster of agents: +Restrict it to a single queue with `-queue`: -``` +```shell buildkite-agent-metrics -token abc123 -interval 30s -queue my-queue ``` -Restrict it to a multiple queues with `-queue`: +Restrict it to multiple queues by repeating `-queue`: -``` +```shell buildkite-agent-metrics -token abc123 -interval 30s -queue my-queue1 -queue my-queue2 ``` +When using clusters, you can pass a cluster registration token to gather metrics +only for that cluster: + +```shell +buildkite-agent-metrics -token clustertoken ... +``` + +You can repeat `-token` to gather metrics for multiple clusters: + +```shell +buildkite-agent-metrics -token clusterAtoken -token clusterBtoken ... +``` + ### Running as an AWS Lambda -An AWS Lambda bundle is created and published as part of the build process. The lambda will require the [`cloudwatch:PutMetricData`](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/publishingMetrics.html) IAM permission. - -It's entrypoint is `handler`, it requires a `go1.x` environment and respects the following env vars: - - - `BUILDKITE_BACKEND` : The name of the backend to use (e.g. `cloudwatch`, `statsd`, `prometheus` or `stackdriver`). - - `BUILDKITE_QUEUE` : A comma separated list of Buildkite queues to process (e.g. `backend-deploy,ui-deploy`). - - `BUILDKITE_QUIET` : A boolean specifying that only `ERROR` log lines must be printed. (e.g. `1`, `true`). - - `BUILDKITE_CLOUDWATCH_DIMENSIONS` : A comma separated list in the form of Key=Value, Other=Value containing the Cloudwatch dimensions to index metrics under. - -Additionally, one of the following groups of environment variables must be set in order to define how the Lambda function -should obtain the required Buildkite Agent API token: - -##### Option 1 - Provide the token as plain-text - -- `BUILDKITE_AGENT_TOKEN` : The Buildkite Agent API token to use. - +An AWS Lambda bundle is created and published as part of the build process. The +lambda will require the +[`cloudwatch:PutMetricData`](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/publishingMetrics.html) +IAM permission. + +It requires a `provided.al2` environment and respects the following env vars: + +- `BUILDKITE_BACKEND` : The name of the backend to use (e.g. `cloudwatch`, + `statsd`, `newrelic`. For the lambda, `prometheus` and `stackdriver` are not + supported). +- `BUILDKITE_QUEUE` : A comma separated list of Buildkite queues to process + (e.g. `backend-deploy,ui-deploy`). +- `BUILDKITE_QUIET` : A boolean specifying that only `ERROR` log lines must be + printed. (e.g. `1`, `true`). +- `BUILDKITE_CLOUDWATCH_DIMENSIONS` : A comma separated list in the form of + `Key=Value,Other=Value` containing the Cloudwatch dimensions to index metrics + under. + +Additionally, one of the following groups of environment variables must be set +in order to define how the Lambda function should obtain the required Buildkite +Agent API token: + +#### Option 1 - Provide the token(s) as plain-text + +- `BUILDKITE_AGENT_TOKEN` : The Buildkite Agent API token to use. You can supply + multiple tokens comma-separated. + #### Option 2 - Retrieve token from AWS Systems Manager -- `BUILDKITE_AGENT_TOKEN_SSM_KEY` : The parameter name which contains the token value in AWS -Systems Manager. - -**Note**: Parameters stored as `String` and `SecureString` are currently supported. +- `BUILDKITE_AGENT_TOKEN_SSM_KEY` : The parameter name which contains the token + value in AWS Systems Manager. You can supply multiple names comma-separated. -#### Option 3 - Retrieve token from AWS Secrets Manager +**Note**: Parameters stored as `String` and `SecureString` are currently +supported. -- `BUILDKITE_AGENT_SECRETS_MANAGER_SECRET_ID`: The id of the secret which contains the token value -in AWS Secrets Manager. +#### Option 3 - Retrieve token from AWS Secrets Manager -- (Optional) `BUILDKITE_AGENT_SECRETS_MANAGER_JSON_KEY`: The JSON key containing the token value in the secret JSON blob. +- `BUILDKITE_AGENT_SECRETS_MANAGER_SECRET_ID`: The id of the secret which + contains the token value in AWS Secrets Manager. You can supply + multiple ids comma-separated. +- (Optional) `BUILDKITE_AGENT_SECRETS_MANAGER_JSON_KEY`: The JSON key containing + the token value in the secret JSON blob. -**Note 1**: Both `SecretBinary` and `SecretString` are supported. In the case of `SecretBinary`, the secret payload will -be automatically decoded and returned as a plain-text string. +**Note 1**: Both `SecretBinary` and `SecretString` are supported. In the case of +`SecretBinary`, the secret payload will be automatically decoded and returned as +a plain-text string. -**Note 2**: `BUILDKITE_AGENT_SECRETS_MANAGER_JSON_KEY` can be used on secrets of type `SecretBinary` only if their -binary payload corresponds to a valid JSON object containing the provided key. +**Note 2**: `BUILDKITE_AGENT_SECRETS_MANAGER_JSON_KEY` can be used on secrets of +type `SecretBinary` only if their binary payload corresponds to a valid JSON +object containing the provided key. ```bash aws lambda create-function \ --function-name buildkite-agent-metrics \ --memory 128 \ --role arn:aws:iam::account-id:role/execution_role \ - --runtime go1.x \ + --runtime provided.al2 \ --zip-file fileb://handler.zip \ --handler handler ``` @@ -89,20 +120,23 @@ aws lambda create-function \ You can build a docker image for the `buildkite-agent-metrics` following: -``` +```shell docker build -t buildkite-agent-metrics . ``` -This will create a local docker image named as `buildkite-agent-metrics` that you can tag and push to your own registry. +This will create a local docker image named as `buildkite-agent-metrics` that +you can tag and push to your own registry. -You can use the command-line arguments in a docker execution in the same way as described before: +You can use the command-line arguments in a docker execution in the same way as +described before: -``` +```shell docker run --rm buildkite-agent-metrics -token abc123 -interval 30s -queue my-queue ``` ### Supported command line flags -``` + +```shell $ buildkite-agent-metrics --help Usage of buildkite-agent-metrics: -backend string @@ -149,48 +183,72 @@ Usage of buildkite-agent-metrics: By default metrics will be submitted to CloudWatch but the backend can be switched to StatsD or Prometheus using the command-line argument `-backend statsd` or `-backend prometheus` respectively. +#### Cloudwatch + The Cloudwatch backend supports the following arguments: -* `-cloudwatch-dimensions`: A optional custom dimension in the form of `Key=Value, Key=Value` +- `-cloudwatch-dimensions`: A optional custom dimension in the form of `Key=Value, Key=Value` + +#### StatsD (Datadog) The StatsD backend supports the following arguments: -* `-statsd-host HOST`: The StatsD host and port (defaults to `127.0.0.1:8125`). -* `-statsd-tags`: Some StatsD servers like the agent provided by Datadog support tags. If specified, metrics will be tagged by `queue` otherwise metrics will include the queue name in the metric. Only enable this option if you know your StatsD server supports tags. +- `-statsd-host HOST`: The StatsD host and port (defaults to `127.0.0.1:8125`). +- `-statsd-tags`: Some StatsD servers like the agent provided by Datadog support + tags. If specified, metrics will be tagged by `queue` otherwise metrics will + include the queue name in the metric. Only enable this option if you know + your StatsD server supports tags. + +#### Prometheus The Prometheus backend supports the following arguments: -* `-prometheus-addr`: The local address to listen on (defaults to `:8080`). -* `-prometheus-path`: The path under `prometheus-addr` to expose metrics on (defaults to `/metrics`). +- `-prometheus-addr`: The local address to listen on (defaults to `:8080`). +- `-prometheus-path`: The path under `prometheus-addr` to expose metrics on + (defaults to `/metrics`). + +#### Stackdriver The Stackdriver backend supports the following arguments: -* `-stackdriver-projectid`: The Google Cloud Platform project to report metrics for. +- `-stackdriver-projectid`: The Google Cloud Platform project to report metrics + for. The New Relic backend supports the following arguments: -* `-newrelic-app-name`: String for the New Relic app name -* `-newrelic-license-key`: The New Relic license key. Must be of type `INGEST` +- `-newrelic-app-name`: String for the New Relic app name +- `-newrelic-license-key`: The New Relic license key. Must be of type `INGEST` ### Upgrading from v2 to v3 1. The `-org` argument is no longer needed -2. The `-token` argument is now an _Agent Registration Token_ — the same used in the Buildkite Agent configuration file, and found on the [Buildkite Agents page](https://buildkite.com/organizations/-/agents). -3. Build and pipeline metrics have been removed, focusing on agents and jobs by queue for auto–scaling. - If you have a compelling reason to gather build or pipeline metrics please continue to use the [previous version](https://github.com/buildkite/buildkite-agent-metrics/releases/tag/v2.1.0) or [open an issue](https://github.com/buildkite/buildkite-agent-metrics/issues) with details. +2. The `-token` argument is now an _Agent Registration Token_ — the same used in + the Buildkite Agent configuration file, and found on the + [Buildkite Agents page](https://buildkite.com/organizations/-/agents). +3. Build and pipeline metrics have been removed, focusing on agents and jobs by + queue for auto–scaling. + If you have a compelling reason to gather build or pipeline metrics please + continue to use the + [previous version](https://github.com/buildkite/buildkite-agent-metrics/releases/tag/v2.1.0) + or [open an issue](https://github.com/buildkite/buildkite-agent-metrics/issues) + with details. ## Development -This tool is built with Go 1.11+ and [Go Modules](https://github.com/golang/go/wiki/Modules). +This tool is built with Go 1.20+ and assumes +[Go Modules](https://github.com/golang/go/wiki/Modules) by default. -You can build and run the binary tool locally with golang installed: +You can build and run the binary tool locally with Go installed: -``` -export GO111MODULE=on +```shell go run *.go -token [buildkite agent registration token] ``` -Currently this will publish metrics to Cloudwatch under the custom metric prefix of `Buildkite`, using AWS credentials from your environment. The machine will require the [`cloudwatch:PutMetricData`](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/publishingMetrics.html) IAM permission. +Currently this will publish metrics to Cloudwatch under the custom metric prefix +of `Buildkite`, using AWS credentials from your environment. The machine will +require the +[`cloudwatch:PutMetricData`](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/publishingMetrics.html) +IAM permission. ### The `token` package @@ -217,7 +275,7 @@ go generate token/ssm_test.go The following metrics are gathered when no specific queue is supplied: -``` +```plain Buildkite > (Org) > RunningJobsCount Buildkite > (Org) > ScheduledJobsCount Buildkite > (Org) > UnfinishedJobsCount @@ -240,9 +298,13 @@ Buildkite > (Org, Queue) > TotalAgentsCount When a queue is specified, only that queue's metrics are published. We send metrics for Jobs in the following states: -* **Scheduled**, the job hasn't been assigned to an agent yet. If you have agent capacity, this value should be close to 0. -* **Waiting**, the job is known to exist but isn't schedulable yet due to dependencies, `wait` statements, etc. This information is mostly useful to an autoscaler, since it represents work that will start soon. -* **Running**, an agent is actively executing this job. + +- **Scheduled**: the job hasn't been assigned to an agent yet. If you have agent + capacity, this value should be close to 0. +- **Waiting**: the job is known to exist but isn't schedulable yet due to + dependencies, `wait` statements, etc. This information is mostly useful to an + autoscaler, since it represents work that will start soon. +- **Running**: an agent is actively executing this job. ## License diff --git a/backend/backends.go b/backend/backends.go index 2f22ebfd..905d49e8 100644 --- a/backend/backends.go +++ b/backend/backends.go @@ -1,6 +1,6 @@ package backend -import "github.com/buildkite/buildkite-agent-metrics/collector" +import "github.com/buildkite/buildkite-agent-metrics/v5/collector" // Backend is a receiver of metrics type Backend interface { diff --git a/backend/cloudwatch.go b/backend/cloudwatch.go index 7abad8d1..b2c5fa97 100644 --- a/backend/cloudwatch.go +++ b/backend/cloudwatch.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/cloudwatch" - "github.com/buildkite/buildkite-agent-metrics/collector" + "github.com/buildkite/buildkite-agent-metrics/v5/collector" ) // CloudWatchDimension is a dimension to add to metrics @@ -65,7 +65,18 @@ func (cb *CloudWatchBackend) Collect(r *collector.Result) error { // Set the baseline org dimension dimensions := []*cloudwatch.Dimension{ - &cloudwatch.Dimension{Name: aws.String("Org"), Value: aws.String(r.Org)}, + { + Name: aws.String("Org"), + Value: aws.String(r.Org), + }, + } + + // Add cluster dimension if a cluster token was used + if r.Cluster != "" { + dimensions = append(dimensions, &cloudwatch.Dimension{ + Name: aws.String("Cluster"), + Value: aws.String(r.Cluster), + }) } // Add custom dimension if provided diff --git a/backend/newrelic.go b/backend/newrelic.go index 8579b7a8..67bdcbec 100644 --- a/backend/newrelic.go +++ b/backend/newrelic.go @@ -4,7 +4,7 @@ import ( "log" "time" - "github.com/buildkite/buildkite-agent-metrics/collector" + "github.com/buildkite/buildkite-agent-metrics/v5/collector" newrelic "github.com/newrelic/go-agent" ) @@ -40,8 +40,8 @@ func NewNewRelicBackend(appName string, licenseKey string) (*NewRelicBackend, er // Collect metrics func (nr *NewRelicBackend) Collect(r *collector.Result) error { // Publish event for each queue - for name, c := range r.Queues { - data := toCustomEvent(name, c) + for queue, metrics := range r.Queues { + data := toCustomEvent(r.Cluster, queue, metrics) err := nr.client.RecordCustomEvent("BuildkiteQueueMetrics", data) if err != nil { return err @@ -54,11 +54,15 @@ func (nr *NewRelicBackend) Collect(r *collector.Result) error { } // toCustomEvent converts a map of metrics to a valid New Relic event body -func toCustomEvent(queueName string, queueMetrics map[string]int) map[string]interface{} { - eventData := map[string]interface{}{ +func toCustomEvent(clusterName, queueName string, queueMetrics map[string]int) map[string]any { + eventData := map[string]any{ "Queue": queueName, } + if clusterName != "" { + eventData["Cluster"] = clusterName + } + for k, v := range queueMetrics { eventData[k] = v } diff --git a/backend/newrelic_test.go b/backend/newrelic_test.go index 4a38348c..9b150603 100644 --- a/backend/newrelic_test.go +++ b/backend/newrelic_test.go @@ -1,19 +1,23 @@ package backend import ( - "reflect" "testing" + + "github.com/google/go-cmp/cmp" ) func TestToCustomEvent(t *testing.T) { tcs := []struct { - queueName string // input queue - metrics map[string]int // input metrics - expected map[string]interface{} // output shaped data + desc string + clusterName string + queueName string // input queue + metrics map[string]int // input metrics + want map[string]any // output shaped data }{ - // test 1 partial data { - queueName: "partial-data-test", + desc: "partial data", + clusterName: "test-cluster", + queueName: "partial-data-test", metrics: map[string]int{ "BusyAgentCount": 0, "BusyAgentPercentage": 0, @@ -21,7 +25,8 @@ func TestToCustomEvent(t *testing.T) { "TotalAgentCount": 3, "RunningJobsCount": 0, }, - expected: map[string]interface{}{ + want: map[string]any{ + "Cluster": "test-cluster", "Queue": "partial-data-test", "BusyAgentCount": 0, "BusyAgentPercentage": 0, @@ -30,9 +35,10 @@ func TestToCustomEvent(t *testing.T) { "RunningJobsCount": 0, }, }, - // test 2 complete data { - queueName: "complete-data-test", + desc: "complete data", + clusterName: "test-cluster", + queueName: "complete-data-test", metrics: map[string]int{ "BusyAgentCount": 2, "BusyAgentPercentage": 20, @@ -42,7 +48,8 @@ func TestToCustomEvent(t *testing.T) { "ScheduledJobsCount": 0, "WaitingJobsCount": 0, }, - expected: map[string]interface{}{ + want: map[string]any{ + "Cluster": "test-cluster", "Queue": "complete-data-test", "BusyAgentCount": 2, "BusyAgentPercentage": 20, @@ -55,12 +62,13 @@ func TestToCustomEvent(t *testing.T) { }, } - for n, tc := range tcs { - got := toCustomEvent(tc.queueName, tc.metrics) - - if !reflect.DeepEqual(got, tc.expected) { - t.Errorf("toCustomEvent test #%d failed, result %+v did not equal expected %+v", n, got, tc.expected) - } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + got := toCustomEvent(tc.clusterName, tc.queueName, tc.metrics) + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("toCustomEvent output diff (-got +want):\n%s", diff) + } + }) } } diff --git a/backend/prometheus.go b/backend/prometheus.go index 83e70392..a9765014 100644 --- a/backend/prometheus.go +++ b/backend/prometheus.go @@ -7,7 +7,7 @@ import ( "regexp" "strings" - "github.com/buildkite/buildkite-agent-metrics/collector" + "github.com/buildkite/buildkite-agent-metrics/v5/collector" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -18,60 +18,74 @@ var ( ) type Prometheus struct { - totals map[string]prometheus.Gauge + totals map[string]*prometheus.GaugeVec queues map[string]*prometheus.GaugeVec pipelines map[string]*prometheus.GaugeVec } -func NewPrometheusBackend(path, addr string) *Prometheus { - go func() { - http.Handle(path, promhttp.Handler()) - log.Fatal(http.ListenAndServe(addr, nil)) - }() - - return newPrometheus() -} - -func newPrometheus() *Prometheus { +func NewPrometheusBackend() *Prometheus { return &Prometheus{ - totals: make(map[string]prometheus.Gauge), + totals: make(map[string]*prometheus.GaugeVec), queues: make(map[string]*prometheus.GaugeVec), pipelines: make(map[string]*prometheus.GaugeVec), } } -func (p *Prometheus) Collect(r *collector.Result) error { +// Serve runs a Prometheus metrics HTTP server. +func (p *Prometheus) Serve(path, addr string) { + m := http.NewServeMux() + m.Handle(path, promhttp.Handler()) + log.Fatal(http.ListenAndServe(addr, m)) +} +func (p *Prometheus) Collect(r *collector.Result) error { // Clear the gauges to prevent stale values from persisting forever. for _, gauge := range p.queues { gauge.Reset() } for name, value := range r.Totals { + labelNames := []string{} + if r.Cluster != "" { + labelNames = append(labelNames, "cluster") + } gauge, ok := p.totals[name] if !ok { - gauge = prometheus.NewGauge(prometheus.GaugeOpts{ + gauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: fmt.Sprintf("buildkite_total_%s", camelToUnderscore(name)), Help: fmt.Sprintf("Buildkite Total: %s", name), - }) + }, labelNames) prometheus.MustRegister(gauge) p.totals[name] = gauge } - gauge.Set(float64(value)) + + if r.Cluster != "" { + gauge.WithLabelValues(r.Cluster).Set(float64(value)) + } else { + gauge.WithLabelValues().Set(float64(value)) + } } for queue, counts := range r.Queues { for name, value := range counts { gauge, ok := p.queues[name] if !ok { + labelNames := []string{"queue"} + if r.Cluster != "" { + labelNames = append(labelNames, "cluster") + } gauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: fmt.Sprintf("buildkite_queues_%s", camelToUnderscore(name)), Help: fmt.Sprintf("Buildkite Queues: %s", name), - }, []string{"queue"}) + }, labelNames) prometheus.MustRegister(gauge) p.queues[name] = gauge } - gauge.WithLabelValues(queue).Set(float64(value)) + if r.Cluster != "" { + gauge.WithLabelValues(queue, r.Cluster).Set(float64(value)) + } else { + gauge.WithLabelValues(queue).Set(float64(value)) + } } } diff --git a/backend/prometheus_test.go b/backend/prometheus_test.go index b5840862..f5720641 100644 --- a/backend/prometheus_test.go +++ b/backend/prometheus_test.go @@ -4,13 +4,13 @@ import ( "fmt" "testing" - "github.com/buildkite/buildkite-agent-metrics/collector" + "github.com/buildkite/buildkite-agent-metrics/v5/collector" + "github.com/google/go-cmp/cmp" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) const ( - wantHaveFmt = "want %v, have %v" runningBuildsCount = iota scheduledBuildsCount runningJobsCount @@ -35,7 +35,8 @@ func newTestResult(t *testing.T) *collector.Result { } res := &collector.Result{ - Totals: totals, + Totals: totals, + Cluster: "test_cluster", Queues: map[string]map[string]int{ "default": totals, "deploy": totals, @@ -44,6 +45,8 @@ func newTestResult(t *testing.T) *collector.Result { return res } +// gatherMetrics runs the Prometheus gatherer, and returns the metric families +// grouped by name. func gatherMetrics(t *testing.T) map[string]*dto.MetricFamily { t.Helper() @@ -54,115 +57,169 @@ func gatherMetrics(t *testing.T) map[string]*dto.MetricFamily { r := prometheus.NewRegistry() prometheus.DefaultRegisterer = r - p := newPrometheus() + p := NewPrometheusBackend() p.Collect(newTestResult(t)) - if mfs, err := r.Gather(); err != nil { - t.Fatal(err) + mfs, err := r.Gather() + if err != nil { + t.Fatalf("prometheus.Registry.Gather() = %v", err) return nil - } else { - mfsm := make(map[string]*dto.MetricFamily) - for _, mf := range mfs { - mfsm[*mf.Name] = mf - } - return mfsm } + + mfsm := make(map[string]*dto.MetricFamily) + for _, mf := range mfs { + mfsm[*mf.Name] = mf + } + return mfsm } func TestCollect(t *testing.T) { - mfs := gatherMetrics(t) + metricFamilies := gatherMetrics(t) - if want, have := 16, len(mfs); want != have { - t.Errorf("wanted %d Prometheus metrics, have: %d", want, have) + if got, want := len(metricFamilies), 16; got != want { + t.Errorf("len(metricFamilies) = %d, want %d", got, want) + } + + type promMetric struct { + Labels map[string]string + Value float64 } tcs := []struct { - Group string - PromName string - PromHelp string - PromLabels []string - PromValue float64 - PromType dto.MetricType + group string + metricName string + wantHelp string + wantType dto.MetricType + wantMetrics []promMetric }{ { - "Total", - "buildkite_total_running_jobs_count", - "Buildkite Total: RunningJobsCount", - []string{}, - runningJobsCount, - dto.MetricType_GAUGE, + group: "Total", + metricName: "buildkite_total_running_jobs_count", + wantHelp: "Buildkite Total: RunningJobsCount", + wantType: dto.MetricType_GAUGE, + wantMetrics: []promMetric{ + { + Labels: map[string]string{"cluster": "test_cluster"}, + Value: runningJobsCount, + }, + }, }, { - "Total", - "buildkite_total_scheduled_jobs_count", - "Buildkite Total: ScheduledJobsCount", - []string{}, - scheduledJobsCount, - dto.MetricType_GAUGE, + group: "Total", + metricName: "buildkite_total_scheduled_jobs_count", + wantHelp: "Buildkite Total: ScheduledJobsCount", + wantType: dto.MetricType_GAUGE, + wantMetrics: []promMetric{ + { + Labels: map[string]string{"cluster": "test_cluster"}, + Value: scheduledJobsCount, + }, + }, }, { - "Queues", - "buildkite_queues_scheduled_builds_count", - "Buildkite Queues: ScheduledBuildsCount", - []string{"default", "deploy"}, - scheduledBuildsCount, - dto.MetricType_GAUGE, + group: "Queues", + metricName: "buildkite_queues_scheduled_builds_count", + wantHelp: "Buildkite Queues: ScheduledBuildsCount", + wantType: dto.MetricType_GAUGE, + wantMetrics: []promMetric{ + { + Labels: map[string]string{ + "cluster": "test_cluster", + "queue": "default", + }, + Value: scheduledBuildsCount, + }, + { + Labels: map[string]string{ + "cluster": "test_cluster", + "queue": "deploy", + }, + Value: scheduledBuildsCount, + }, + }, }, { - "Queues", - "buildkite_queues_idle_agent_count", - "Buildkite Queues: IdleAgentCount", - []string{"default", "deploy"}, - idleAgentCount, - dto.MetricType_GAUGE, + group: "Queues", + metricName: "buildkite_queues_idle_agent_count", + wantHelp: "Buildkite Queues: IdleAgentCount", + wantType: dto.MetricType_GAUGE, + wantMetrics: []promMetric{ + { + Labels: map[string]string{ + "cluster": "test_cluster", + "queue": "default", + }, + Value: idleAgentCount, + }, + { + Labels: map[string]string{ + "cluster": "test_cluster", + "queue": "deploy", + }, + Value: idleAgentCount, + }, + }, }, } for _, tc := range tcs { - t.Run(fmt.Sprintf("%s/%s", tc.Group, tc.PromName), func(t *testing.T) { - mf, ok := mfs[tc.PromName] + t.Run(fmt.Sprintf("%s/%s", tc.group, tc.metricName), func(t *testing.T) { + mf, ok := metricFamilies[tc.metricName] if !ok { - t.Errorf("no metric found for name %s", tc.PromName) + t.Errorf("no metric found for name %s", tc.metricName) } - if want, have := tc.PromHelp, mf.GetHelp(); want != have { - t.Errorf(wantHaveFmt, want, have) + if got, want := mf.GetHelp(), tc.wantHelp; got != want { + t.Errorf("mf.GetHelp() = %q, want %q", got, want) } - if want, have := tc.PromType, mf.GetType(); want != have { - t.Errorf(wantHaveFmt, want, have) + if got, want := mf.GetType(), tc.wantType; got != want { + t.Errorf("mf.GetType() = %q, want %q", got, want) } + // Convert the metric family into an easier-to-diff form. ms := mf.GetMetric() - for i, m := range ms { - if want, have := tc.PromValue, m.GetGauge().GetValue(); want != have { - t.Errorf(wantHaveFmt, want, have) + var gotMetrics []promMetric + for _, m := range ms { + gotMetric := promMetric{ + Value: m.Gauge.GetValue(), + Labels: make(map[string]string), } - - if len(tc.PromLabels) > 0 { - if want, have := tc.PromLabels[i], m.Label[0].GetValue(); want != have { - t.Errorf(wantHaveFmt, want, have) - } + for _, label := range m.Label { + gotMetric.Labels[label.GetName()] = label.GetValue() } + gotMetrics = append(gotMetrics, gotMetric) } + if diff := cmp.Diff(gotMetrics, tc.wantMetrics); diff != "" { + t.Errorf("metrics diff (-got +want):\n%s", diff) + } }) } } func TestCamelToUnderscore(t *testing.T) { tcs := []struct { - Camel string - Underscore string + input string + want string }{ - {"TotalAgentCount", "total_agent_count"}, - {"Total@#4JobsCount", "total@#4_jobs_count"}, - {"BuildkiteQueuesIdleAgentCount1_11", "buildkite_queues_idle_agent_count1_11"}, + { + input: "TotalAgentCount", + want: "total_agent_count", + }, + { + input: "Total@#4JobsCount", + want: "total@#4_jobs_count", + }, + { + input: "BuildkiteQueuesIdleAgentCount1_11", + want: "buildkite_queues_idle_agent_count1_11", + }, } for _, tc := range tcs { - if want, have := tc.Underscore, camelToUnderscore(tc.Camel); want != have { - t.Errorf(wantHaveFmt, want, have) + if got := camelToUnderscore(tc.input); got != tc.want { + t.Errorf("camelToUnderscore(%q) = %q, want %q", tc.input, got, tc.want) } } } diff --git a/backend/stackdriver.go b/backend/stackdriver.go index 276acb7a..d2ed219e 100644 --- a/backend/stackdriver.go +++ b/backend/stackdriver.go @@ -7,20 +7,22 @@ import ( "strings" "time" - "github.com/buildkite/buildkite-agent-metrics/collector" + "github.com/buildkite/buildkite-agent-metrics/v5/collector" "google.golang.org/genproto/googleapis/api/label" "google.golang.org/protobuf/types/known/timestamppb" - monitoring "cloud.google.com/go/monitoring/apiv3" + monitoring "cloud.google.com/go/monitoring/apiv3/v2" + "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" "google.golang.org/genproto/googleapis/api/metric" - monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3" ) const ( - metricName = "custom.googleapis.com/buildkite/%s/%s" - queueLabelKey = "Queue" - queueDescription = "Queue Descriptor" - totalMetricsQueue = "Total" + metricTypeFmt = "custom.googleapis.com/buildkite/%s/%s" + clusterLabelKey = "Cluster" + clusterDescription = "Name of the Buildkite Cluster, or empty" + queueLabelKey = "Queue" + queueDescription = "Name of the Queue" + totalMetricsQueue = "Total" ) // Stackdriver does not allow dashes in metric names @@ -55,10 +57,14 @@ func (sd *StackDriverBackend) Collect(r *collector.Result) error { Seconds: time.Now().Unix(), } orgName := dashReplacer.Replace(r.Org) + metricTypeFunc := func(name string) string { + return fmt.Sprintf(metricTypeFmt, orgName, name) + } + for name, value := range r.Totals { mt, present := sd.metricTypes[name] if !present { - mt = fmt.Sprintf(metricName, orgName, name) + mt = metricTypeFunc(name) metricReq := createCustomMetricRequest(&sd.projectID, &mt) _, err := sd.client.CreateMetricDescriptor(ctx, metricReq) if err != nil { @@ -69,7 +75,7 @@ func (sd *StackDriverBackend) Collect(r *collector.Result) error { log.Printf("[Collect] created custom metric [%s]", mt) sd.metricTypes[name] = mt } - req := createTimeSeriesValueRequest(&sd.projectID, &mt, totalMetricsQueue, value, now) + req := createTimeSeriesValueRequest(&sd.projectID, &mt, r.Cluster, totalMetricsQueue, value, now) err := sd.client.CreateTimeSeries(ctx, req) if err != nil { retErr := fmt.Errorf("[Collect] could not write metric [%s] value [%d], %v ", mt, value, err) @@ -80,8 +86,8 @@ func (sd *StackDriverBackend) Collect(r *collector.Result) error { for queue, counts := range r.Queues { for name, value := range counts { - mt := fmt.Sprintf(metricName, orgName, name) - req := createTimeSeriesValueRequest(&sd.projectID, &mt, queue, value, now) + mt := metricTypeFunc(name) + req := createTimeSeriesValueRequest(&sd.projectID, &mt, r.Cluster, queue, value, now) err := sd.client.CreateTimeSeries(ctx, req) if err != nil { retErr := fmt.Errorf("[Collect] could not write metric [%s] value [%d], %v ", mt, value, err) @@ -96,12 +102,20 @@ func (sd *StackDriverBackend) Collect(r *collector.Result) error { // createCustomMetricRequest creates a custom metric request as specified by the metric type. func createCustomMetricRequest(projectID *string, metricType *string) *monitoringpb.CreateMetricDescriptorRequest { - l := &label.LabelDescriptor{ + clusterLabel := &label.LabelDescriptor{ + Key: clusterLabelKey, + ValueType: label.LabelDescriptor_STRING, + Description: clusterDescription, + } + queueLabel := &label.LabelDescriptor{ Key: queueLabelKey, ValueType: label.LabelDescriptor_STRING, Description: queueDescription, } - labels := []*label.LabelDescriptor{l} + labels := []*label.LabelDescriptor{ + clusterLabel, + queueLabel, + } md := &metric.MetricDescriptor{ Name: *metricType, Type: *metricType, @@ -120,13 +134,16 @@ func createCustomMetricRequest(projectID *string, metricType *string) *monitorin } // createTimeSeriesValueRequest creates a StackDriver value request for the specified metric -func createTimeSeriesValueRequest(projectID *string, metricType *string, queue string, value int, time *timestamppb.Timestamp) *monitoringpb.CreateTimeSeriesRequest { +func createTimeSeriesValueRequest(projectID *string, metricType *string, cluster, queue string, value int, time *timestamppb.Timestamp) *monitoringpb.CreateTimeSeriesRequest { req := &monitoringpb.CreateTimeSeriesRequest{ Name: "projects/" + *projectID, TimeSeries: []*monitoringpb.TimeSeries{{ Metric: &metric.Metric{ - Type: *metricType, - Labels: map[string]string{queueLabelKey: queue}, + Type: *metricType, + Labels: map[string]string{ + clusterLabelKey: cluster, + queueLabelKey: queue, + }, }, Points: []*monitoringpb.Point{{ Interval: &monitoringpb.TimeInterval{ diff --git a/backend/stackdriver_test.go b/backend/stackdriver_test.go index 99488048..2e9fd488 100644 --- a/backend/stackdriver_test.go +++ b/backend/stackdriver_test.go @@ -1,14 +1,14 @@ package backend import ( - "fmt" - "reflect" "testing" "time" + "cloud.google.com/go/monitoring/apiv3/v2/monitoringpb" + "github.com/google/go-cmp/cmp" "google.golang.org/genproto/googleapis/api/label" "google.golang.org/genproto/googleapis/api/metric" - monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3" + "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -22,62 +22,89 @@ func Test_createCustomMetricRequest(t *testing.T) { args args want *monitoringpb.CreateMetricDescriptorRequest }{ - {name: "HappyPath", args: args{projectID: "test-project-id", metricType: "test-metric-type"}, want: &monitoringpb.CreateMetricDescriptorRequest{ - Name: "projects/test-project-id", - MetricDescriptor: &metric.MetricDescriptor{ - Name: "test-metric-type", - Type: "test-metric-type", - MetricKind: metric.MetricDescriptor_GAUGE, - ValueType: metric.MetricDescriptor_INT64, - Description: fmt.Sprintf("Buildkite metric: [test-metric-type]"), - DisplayName: "test-metric-type", - Labels: []*label.LabelDescriptor{{ - Key: queueLabelKey, - ValueType: label.LabelDescriptor_STRING, - Description: queueDescription, - }}, + { + name: "HappyPath", + args: args{ + projectID: "test-project-id", + metricType: "test-metric-type", + }, + want: &monitoringpb.CreateMetricDescriptorRequest{ + Name: "projects/test-project-id", + MetricDescriptor: &metric.MetricDescriptor{ + Name: "test-metric-type", + Type: "test-metric-type", + MetricKind: metric.MetricDescriptor_GAUGE, + ValueType: metric.MetricDescriptor_INT64, + Description: "Buildkite metric: [test-metric-type]", + DisplayName: "test-metric-type", + Labels: []*label.LabelDescriptor{ + { + Key: clusterLabelKey, + ValueType: label.LabelDescriptor_STRING, + Description: clusterDescription, + }, + { + Key: queueLabelKey, + ValueType: label.LabelDescriptor_STRING, + Description: queueDescription, + }, + }, + }, }, - }}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := createCustomMetricRequest(&tt.args.projectID, &tt.args.metricType); !reflect.DeepEqual(got, tt.want) { - t.Errorf("createCustomMetricRequest() = %v, want %v", got, tt.want) + got := createCustomMetricRequest(&tt.args.projectID, &tt.args.metricType) + if diff := cmp.Diff(got, tt.want, protocmp.Transform()); diff != "" { + t.Errorf("createCustomMetricRequest diff (-got +want):\n%s", diff) } }) } } func Test_createTimeSeriesValueRequest(t *testing.T) { - now := timestamppb.Timestamp{ + now := ×tamppb.Timestamp{ Seconds: time.Now().Unix(), } type args struct { projectID string metricType string + cluster string queue string value int - time timestamppb.Timestamp + time *timestamppb.Timestamp } tests := []struct { name string args args want *monitoringpb.CreateTimeSeriesRequest }{ - {name: "Happy Path", - args: args{projectID: "test-project-id", metricType: "test-metric-type", queue: "test-queue", value: 13, time: now}, + { + name: "Happy Path", + args: args{ + projectID: "test-project-id", + metricType: "test-metric-type", + cluster: "test-cluster", + queue: "test-queue", + value: 13, + time: now, + }, want: &monitoringpb.CreateTimeSeriesRequest{ Name: "projects/test-project-id", TimeSeries: []*monitoringpb.TimeSeries{{ Metric: &metric.Metric{ - Type: "test-metric-type", - Labels: map[string]string{queueLabelKey: "test-queue"}, + Type: "test-metric-type", + Labels: map[string]string{ + clusterLabelKey: "test-cluster", + queueLabelKey: "test-queue", + }, }, Points: []*monitoringpb.Point{{ Interval: &monitoringpb.TimeInterval{ - StartTime: &now, - EndTime: &now, + StartTime: now, + EndTime: now, }, Value: &monitoringpb.TypedValue{ Value: &monitoringpb.TypedValue_Int64Value{ @@ -88,19 +115,29 @@ func Test_createTimeSeriesValueRequest(t *testing.T) { }}, }, }, - {name: "Bad Queue Name", - args: args{projectID: "test-project-id", metricType: "test-metric-type", queue: "${BUILDKITE_QUEUE:-default}", value: 13, time: now}, + { + name: "Bad Queue Name", + args: args{ + projectID: "test-project-id", + metricType: "test-metric-type", + queue: "${BUILDKITE_QUEUE:-default}", + value: 13, + time: now, + }, want: &monitoringpb.CreateTimeSeriesRequest{ Name: "projects/test-project-id", TimeSeries: []*monitoringpb.TimeSeries{{ Metric: &metric.Metric{ - Type: "test-metric-type", - Labels: map[string]string{queueLabelKey: "${BUILDKITE_QUEUE:-default}"}, + Type: "test-metric-type", + Labels: map[string]string{ + clusterLabelKey: "", + queueLabelKey: "${BUILDKITE_QUEUE:-default}", + }, }, Points: []*monitoringpb.Point{{ Interval: &monitoringpb.TimeInterval{ - StartTime: &now, - EndTime: &now, + StartTime: now, + EndTime: now, }, Value: &monitoringpb.TypedValue{ Value: &monitoringpb.TypedValue_Int64Value{ @@ -114,8 +151,9 @@ func Test_createTimeSeriesValueRequest(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := createTimeSeriesValueRequest(&tt.args.projectID, &tt.args.metricType, tt.args.queue, tt.args.value, &tt.args.time); !reflect.DeepEqual(got, tt.want) { - t.Errorf("createTimeSeriesValueRequest() = %v, want %v", got, tt.want) + got := createTimeSeriesValueRequest(&tt.args.projectID, &tt.args.metricType, tt.args.cluster, tt.args.queue, tt.args.value, tt.args.time) + if diff := cmp.Diff(got, tt.want, protocmp.Transform()); diff != "" { + t.Errorf("createTimeSeriesValueRequest diff (-got +want):\n%s", diff) } }) } diff --git a/backend/statsd.go b/backend/statsd.go index 3c9c1f66..89e4faba 100644 --- a/backend/statsd.go +++ b/backend/statsd.go @@ -1,8 +1,10 @@ package backend import ( + "fmt" + "github.com/DataDog/datadog-go/statsd" - "github.com/buildkite/buildkite-agent-metrics/collector" + "github.com/buildkite/buildkite-agent-metrics/v5/collector" ) // StatsD sends metrics to StatsD (Datadog spec) @@ -12,44 +14,84 @@ type StatsD struct { } func NewStatsDBackend(host string, tagsSupported bool) (*StatsD, error) { - c, err := statsd.NewBuffered(host, 100) + client, err := statsd.NewBuffered(host, 100) if err != nil { return nil, err } // prefix every metric with the app name - c.Namespace = "buildkite." + client.Namespace = "buildkite." return &StatsD{ - client: c, + client: client, tagsSupported: tagsSupported, }, nil } func (cb *StatsD) Collect(r *collector.Result) error { + collectFunc := cb.collectWithoutTags + if cb.tagsSupported { + collectFunc = cb.collectWithTags + } + + if err := collectFunc(r); err != nil { + return err + } + + return cb.client.Flush() +} + +// collectWithTags tags clusters and queues. +func (cb *StatsD) collectWithTags(r *collector.Result) error { + commonTags := make([]string, 0, 2) + prefix := "" + if r.Cluster != "" { + commonTags = append(commonTags, "cluster:"+r.Cluster) + prefix = "clusters." + } + for name, value := range r.Totals { - if err := cb.client.Gauge(name, float64(value), []string{}, 1.0); err != nil { + if err := cb.client.Gauge(prefix+name, float64(value), commonTags, 1.0); err != nil { return err } } for queue, counts := range r.Queues { + tags := append(commonTags, "queue:"+queue) + for name, value := range counts { - var finalName string - tags := []string{} - if cb.tagsSupported { - finalName = "queues." + name - tags = []string{"queue:" + queue} - } else { - finalName = "queues." + queue + "." + name - } - if err := cb.client.Gauge(finalName, float64(value), tags, 1.0); err != nil { + if err := cb.client.Gauge(prefix+"queues."+name, float64(value), tags, 1.0); err != nil { return err } } } - if err := cb.client.Flush(); err != nil { - return err + return cb.client.Flush() +} + +// collectWithoutTags embeds clusters and queues into metric names. +func (cb *StatsD) collectWithoutTags(r *collector.Result) error { + prefix := "" + if r.Cluster != "" { + prefix = fmt.Sprintf("clusters.%s.", r.Cluster) + } + + for name, value := range r.Totals { + if err := cb.client.Gauge(prefix+name, float64(value), nil, 1.0); err != nil { + return err + } + } + + for queue, counts := range r.Queues { + prefix := fmt.Sprintf("queues.%s.", queue) + if r.Cluster != "" { + prefix = fmt.Sprintf("clusters.%s.queues.%s.", r.Cluster, queue) + } + + for name, value := range counts { + if err := cb.client.Gauge(prefix+name, float64(value), nil, 1.0); err != nil { + return err + } + } } - return nil + return cb.client.Flush() } diff --git a/collector/collector.go b/collector/collector.go index 7321a63d..98589ef7 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -47,6 +47,7 @@ type Result struct { Totals map[string]int Queues map[string]map[string]int Org string + Cluster string PollDuration time.Duration } @@ -54,6 +55,10 @@ type organizationResponse struct { Slug string `json:"slug"` } +type clusterResponse struct { + Name string `json:"name"` +} + type metricsAgentsResponse struct { Idle int `json:"idle"` Busy int `json:"busy"` @@ -71,6 +76,7 @@ type queueMetricsResponse struct { Agents metricsAgentsResponse `json:"agents"` Jobs metricsJobsResponse `json:"jobs"` Organization organizationResponse `json:"organization"` + Cluster clusterResponse `json:"cluster"` } type allMetricsAgentsResponse struct { @@ -87,6 +93,7 @@ type allMetricsResponse struct { Agents allMetricsAgentsResponse `json:"agents"` Jobs allMetricsJobsResponse `json:"jobs"` Organization organizationResponse `json:"organization"` + Cluster clusterResponse `json:"cluster"` } func (c *Collector) Collect() (*Result, error) { @@ -100,205 +107,225 @@ func (c *Collector) Collect() (*Result, error) { } if len(c.Queues) == 0 { - log.Println("Collecting agent metrics for all queues") - - endpoint, err := url.Parse(c.Endpoint) - if err != nil { + if err := c.collectAllQueues(httpClient, result); err != nil { return nil, err } + } else { + for _, queue := range c.Queues { + if err := c.collectQueue(httpClient, result, queue); err != nil { + return nil, err + } + } + } - endpoint.Path += "/metrics" + if !c.Quiet { + result.Dump() + } - req, err := http.NewRequest("GET", endpoint.String(), nil) - if err != nil { - return nil, err - } + return result, nil +} - req.Header.Set("User-Agent", c.UserAgent) - req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.Token)) +func (c *Collector) collectAllQueues(httpClient *http.Client, result *Result) error { + log.Println("Collecting agent metrics for all queues") - if c.DebugHttp { - if dump, err := httputil.DumpRequest(req, true); err == nil { - log.Printf("DEBUG request uri=%s\n%s\n", req.URL, dump) - } - } + endpoint, err := url.Parse(c.Endpoint) + if err != nil { + return err + } - res, err := httpClient.Do(req) - if err != nil { - // Authorization error signals token is invalid - return nil, err - } - defer res.Body.Close() + endpoint.Path += "/metrics" - if res.StatusCode == 401 { - return nil, fmt.Errorf("http 401 response received %w", ErrUnauthorized) - } + req, err := http.NewRequest("GET", endpoint.String(), nil) + if err != nil { + return err + } - if c.DebugHttp { - if dump, err := httputil.DumpResponse(res, true); err == nil { - log.Printf("DEBUG response uri=%s\n%s\n", req.URL, dump) - } + req.Header.Set("User-Agent", c.UserAgent) + req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.Token)) + + if c.DebugHttp { + if dump, err := httputil.DumpRequest(req, true); err == nil { + log.Printf("DEBUG request uri=%s\n%s\n", req.URL, dump) } + } - // Handle any errors - if res.StatusCode != http.StatusOK { - // If it's json response, show the error message - if strings.HasPrefix(res.Header.Get("Content-Type"), "application/json") { - var errStruct struct { - Message string `json:"message"` - } - err := json.NewDecoder(res.Body).Decode(&errStruct) - if err == nil { - return nil, errors.New(errStruct.Message) - } else { - log.Printf("Failed to decode error: %v", err) - } - } + res, err := httpClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() - return nil, fmt.Errorf("Request failed with %s (%d)", res.Status, res.StatusCode) - } + if res.StatusCode == 401 { + return fmt.Errorf("http 401 response received %w", ErrUnauthorized) + } - var allMetrics allMetricsResponse + if c.DebugHttp { + if dump, err := httputil.DumpResponse(res, true); err == nil { + log.Printf("DEBUG response uri=%s\n%s\n", req.URL, dump) + } + } - // Check if we get a poll duration header from server - if pollSeconds := res.Header.Get(PollDurationHeader); pollSeconds != "" { - pollSecondsInt, err := strconv.ParseInt(pollSeconds, 10, 64) - if err != nil { - log.Printf("Failed to parse %s header: %v", PollDurationHeader, err) + // Handle any errors + if res.StatusCode != http.StatusOK { + // If it's json response, show the error message + if strings.HasPrefix(res.Header.Get("Content-Type"), "application/json") { + var errStruct struct { + Message string `json:"message"` + } + err := json.NewDecoder(res.Body).Decode(&errStruct) + if err == nil { + return errors.New(errStruct.Message) } else { - result.PollDuration = time.Duration(pollSecondsInt) * time.Second + log.Printf("Failed to decode error: %v", err) } } - err = json.NewDecoder(res.Body).Decode(&allMetrics) + return fmt.Errorf("Request failed with %s (%d)", res.Status, res.StatusCode) + } + + var allMetrics allMetricsResponse + + // Check if we get a poll duration header from server + if pollSeconds := res.Header.Get(PollDurationHeader); pollSeconds != "" { + pollSecondsInt, err := strconv.ParseInt(pollSeconds, 10, 64) if err != nil { - return nil, err + log.Printf("Failed to parse %s header: %v", PollDurationHeader, err) + } else { + result.PollDuration = time.Duration(pollSecondsInt) * time.Second } + } - if allMetrics.Organization.Slug == "" { - return nil, fmt.Errorf("No organization slug was found in the metrics response") - } + err = json.NewDecoder(res.Body).Decode(&allMetrics) + if err != nil { + return err + } - log.Printf("Found organization %q", allMetrics.Organization.Slug) - result.Org = allMetrics.Organization.Slug - - result.Totals[ScheduledJobsCount] = allMetrics.Jobs.Scheduled - result.Totals[RunningJobsCount] = allMetrics.Jobs.Running - result.Totals[UnfinishedJobsCount] = allMetrics.Jobs.Total - result.Totals[WaitingJobsCount] = allMetrics.Jobs.Waiting - result.Totals[IdleAgentCount] = allMetrics.Agents.Idle - result.Totals[BusyAgentCount] = allMetrics.Agents.Busy - result.Totals[TotalAgentCount] = allMetrics.Agents.Total - result.Totals[BusyAgentPercentage] = busyAgentPercentage(allMetrics.Agents.metricsAgentsResponse) - - for queueName, queueJobMetrics := range allMetrics.Jobs.Queues { - if _, ok := result.Queues[queueName]; !ok { - result.Queues[queueName] = map[string]int{} - } - result.Queues[queueName][ScheduledJobsCount] = queueJobMetrics.Scheduled - result.Queues[queueName][RunningJobsCount] = queueJobMetrics.Running - result.Queues[queueName][UnfinishedJobsCount] = queueJobMetrics.Total - result.Queues[queueName][WaitingJobsCount] = queueJobMetrics.Waiting - result.Queues[queueName][BintiRequiredAgentCount] = queueJobMetrics.Scheduled + queueJobMetrics.Running + if allMetrics.Organization.Slug == "" { + return fmt.Errorf("No organization slug was found in the metrics response") + } + + log.Printf("Found organization %q, cluster %q", allMetrics.Organization.Slug, allMetrics.Cluster.Name) + result.Org = allMetrics.Organization.Slug + result.Cluster = allMetrics.Cluster.Name + + result.Totals[ScheduledJobsCount] = allMetrics.Jobs.Scheduled + result.Totals[RunningJobsCount] = allMetrics.Jobs.Running + result.Totals[UnfinishedJobsCount] = allMetrics.Jobs.Total + result.Totals[WaitingJobsCount] = allMetrics.Jobs.Waiting + result.Totals[IdleAgentCount] = allMetrics.Agents.Idle + result.Totals[BusyAgentCount] = allMetrics.Agents.Busy + result.Totals[TotalAgentCount] = allMetrics.Agents.Total + result.Totals[BusyAgentPercentage] = busyAgentPercentage(allMetrics.Agents.metricsAgentsResponse) + + for queueName, queueJobMetrics := range allMetrics.Jobs.Queues { + if _, ok := result.Queues[queueName]; !ok { + result.Queues[queueName] = map[string]int{} } + result.Queues[queueName][ScheduledJobsCount] = queueJobMetrics.Scheduled + result.Queues[queueName][RunningJobsCount] = queueJobMetrics.Running + result.Queues[queueName][UnfinishedJobsCount] = queueJobMetrics.Total + result.Queues[queueName][WaitingJobsCount] = queueJobMetrics.Waiting + result.Queues[queueName][BintiRequiredAgentCount] = queueJobMetrics.Waiting + queueJobMetrics.Running + } - for queueName, queueAgentMetrics := range allMetrics.Agents.Queues { - if _, ok := result.Queues[queueName]; !ok { - result.Queues[queueName] = map[string]int{} - } - result.Queues[queueName][IdleAgentCount] = queueAgentMetrics.Idle - result.Queues[queueName][BusyAgentCount] = queueAgentMetrics.Busy - result.Queues[queueName][TotalAgentCount] = queueAgentMetrics.Total - result.Queues[queueName][BusyAgentPercentage] = busyAgentPercentage(queueAgentMetrics) + for queueName, queueAgentMetrics := range allMetrics.Agents.Queues { + if _, ok := result.Queues[queueName]; !ok { + result.Queues[queueName] = map[string]int{} } - } else { - for _, queue := range c.Queues { - log.Printf("Collecting agent metrics for queue '%s'", queue) + result.Queues[queueName][IdleAgentCount] = queueAgentMetrics.Idle + result.Queues[queueName][BusyAgentCount] = queueAgentMetrics.Busy + result.Queues[queueName][TotalAgentCount] = queueAgentMetrics.Total + result.Queues[queueName][BusyAgentPercentage] = busyAgentPercentage(queueAgentMetrics) + } - endpoint, err := url.Parse(c.Endpoint) - if err != nil { - return nil, err - } + return nil +} - endpoint.Path += "/metrics/queue" - endpoint.RawQuery = url.Values{"name": {queue}}.Encode() +func (c *Collector) collectQueue(httpClient *http.Client, result *Result, queue string) error { + log.Printf("Collecting agent metrics for queue '%s'", queue) - req, err := http.NewRequest("GET", endpoint.String(), nil) - if err != nil { - return nil, err - } + endpoint, err := url.Parse(c.Endpoint) + if err != nil { + return err + } - req.Header.Set("User-Agent", c.UserAgent) - req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.Token)) + endpoint.Path += "/metrics/queue" + endpoint.RawQuery = url.Values{"name": {queue}}.Encode() - if c.DebugHttp { - if dump, err := httputil.DumpRequest(req, true); err == nil { - log.Printf("DEBUG request uri=%s\n%s\n", req.URL, dump) - } - } + req, err := http.NewRequest("GET", endpoint.String(), nil) + if err != nil { + return err + } - res, err := httpClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() + req.Header.Set("User-Agent", c.UserAgent) + req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.Token)) - if c.DebugHttp { - if dump, err := httputil.DumpResponse(res, true); err == nil { - log.Printf("DEBUG response uri=%s\n%s\n", req.URL, dump) - } - } + if c.DebugHttp { + if dump, err := httputil.DumpRequest(req, true); err == nil { + log.Printf("DEBUG request uri=%s\n%s\n", req.URL, dump) + } + } - // Handle any errors - if res.StatusCode != http.StatusOK { - // If it's json response, show the error message - if strings.HasPrefix(res.Header.Get("Content-Type"), "application/json") { - var errStruct struct { - Message string `json:"message"` - } - err := json.NewDecoder(res.Body).Decode(&errStruct) - if err == nil { - return nil, errors.New(errStruct.Message) - } else { - log.Printf("Failed to decode error: %v", err) - } - } - - return nil, fmt.Errorf("Request failed with %s (%d)", res.Status, res.StatusCode) - } + res, err := httpClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() - var queueMetrics queueMetricsResponse - err = json.NewDecoder(res.Body).Decode(&queueMetrics) - if err != nil { - return nil, err - } + if res.StatusCode == 401 { + return fmt.Errorf("http 401 response received %w", ErrUnauthorized) + } - if queueMetrics.Organization.Slug == "" { - return nil, fmt.Errorf("No organization slug was found in the metrics response") - } + if c.DebugHttp { + if dump, err := httputil.DumpResponse(res, true); err == nil { + log.Printf("DEBUG response uri=%s\n%s\n", req.URL, dump) + } + } - log.Printf("Found organization %q", queueMetrics.Organization.Slug) - result.Org = queueMetrics.Organization.Slug - - result.Queues[queue] = map[string]int{ - ScheduledJobsCount: queueMetrics.Jobs.Scheduled, - RunningJobsCount: queueMetrics.Jobs.Running, - UnfinishedJobsCount: queueMetrics.Jobs.Total, - WaitingJobsCount: queueMetrics.Jobs.Waiting, - IdleAgentCount: queueMetrics.Agents.Idle, - BusyAgentCount: queueMetrics.Agents.Busy, - TotalAgentCount: queueMetrics.Agents.Total, - BusyAgentPercentage: busyAgentPercentage(queueMetrics.Agents), - BintiRequiredAgentCount: queueMetrics.Jobs.Scheduled + queueMetrics.Jobs.Running, + // Handle any errors + if res.StatusCode != http.StatusOK { + // If it's json response, show the error message + if strings.HasPrefix(res.Header.Get("Content-Type"), "application/json") { + var errStruct struct { + Message string `json:"message"` + } + err := json.NewDecoder(res.Body).Decode(&errStruct) + if err == nil { + return errors.New(errStruct.Message) + } else { + log.Printf("Failed to decode error: %v", err) } } + + return fmt.Errorf("Request failed with %s (%d)", res.Status, res.StatusCode) } - if !c.Quiet { - result.Dump() + var queueMetrics queueMetricsResponse + err = json.NewDecoder(res.Body).Decode(&queueMetrics) + if err != nil { + return err } - return result, nil + if queueMetrics.Organization.Slug == "" { + return fmt.Errorf("No organization slug was found in the metrics response") + } + + log.Printf("Found organization %q, cluster %q", queueMetrics.Organization.Slug, queueMetrics.Cluster.Name) + result.Org = queueMetrics.Organization.Slug + result.Cluster = queueMetrics.Cluster.Name + + result.Queues[queue] = map[string]int{ + ScheduledJobsCount: queueMetrics.Jobs.Scheduled, + RunningJobsCount: queueMetrics.Jobs.Running, + UnfinishedJobsCount: queueMetrics.Jobs.Total, + WaitingJobsCount: queueMetrics.Jobs.Waiting, + IdleAgentCount: queueMetrics.Agents.Idle, + BusyAgentCount: queueMetrics.Agents.Busy, + TotalAgentCount: queueMetrics.Agents.Total, + BusyAgentPercentage: busyAgentPercentage(queueMetrics.Agents), + BintiRequiredAgentCount: queueMetrics.Jobs.Waiting + queueMetrics.Jobs.Running, + } + return nil } func busyAgentPercentage(agents metricsAgentsResponse) int { diff --git a/collector/collector_test.go b/collector/collector_test.go index 6076a91a..e233aa68 100644 --- a/collector/collector_test.go +++ b/collector/collector_test.go @@ -206,7 +206,7 @@ func TestCollectorWithSomeJobsAndAgentsForAllQueues(t *testing.T) { {"Queue.default", res.Queues["default"], BusyAgentCount, 1}, {"Queue.default", res.Queues["default"], IdleAgentCount, 0}, {"Queue.default", res.Queues["default"], WaitingJobsCount, 0}, - {"Queue.default", res.Queues["default"], BintiRequiredAgentCount, 3}, + {"Queue.default", res.Queues["default"], BintiRequiredAgentCount, 1}, {"Queue.deploy", res.Queues["deploy"], RunningJobsCount, 0}, {"Queue.deploy", res.Queues["deploy"], ScheduledJobsCount, 1}, @@ -219,10 +219,10 @@ func TestCollectorWithSomeJobsAndAgentsForAllQueues(t *testing.T) { {"Queue.default", res.Queues["binti"], TotalAgentCount, 1}, {"Queue.deploy", res.Queues["binti"], BusyAgentCount, 1}, - {"Queue.deploy", res.Queues["binti"], BintiRequiredAgentCount, 2}, + {"Queue.deploy", res.Queues["binti"], BintiRequiredAgentCount, 1}, } - for queue, _ := range res.Queues { + for queue := range res.Queues { switch queue { case "default", "deploy", "binti": continue @@ -251,6 +251,7 @@ func TestCollectorWithSomeJobsAndAgentsForAQueue(t *testing.T) { "jobs": { "scheduled": 3, "running": 1, + "waiting": 1, "total": 4 }, "agents": { @@ -285,10 +286,12 @@ func TestCollectorWithSomeJobsAndAgentsForAQueue(t *testing.T) { {"Queue.deploy", res.Queues["deploy"], RunningJobsCount, 1}, {"Queue.deploy", res.Queues["deploy"], ScheduledJobsCount, 3}, {"Queue.deploy", res.Queues["deploy"], UnfinishedJobsCount, 4}, + {"Queue.deploy", res.Queues["deploy"], WaitingJobsCount, 1}, {"Queue.deploy", res.Queues["deploy"], TotalAgentCount, 1}, {"Queue.deploy", res.Queues["deploy"], BusyAgentCount, 1}, {"Queue.deploy", res.Queues["deploy"], IdleAgentCount, 0}, {"Queue.deploy", res.Queues["deploy"], BusyAgentPercentage, 100}, + {"Queue.deploy", res.Queues["deploy"], BintiRequiredAgentCount, 2}, } for _, tc := range testCases { diff --git a/go.mod b/go.mod index 2f775ea4..3f0b2df0 100644 --- a/go.mod +++ b/go.mod @@ -1,47 +1,48 @@ -module github.com/buildkite/buildkite-agent-metrics +module github.com/buildkite/buildkite-agent-metrics/v5 go 1.20 require ( - cloud.google.com/go/monitoring v1.15.1 + cloud.google.com/go/monitoring v1.17.0 github.com/DataDog/datadog-go v4.8.3+incompatible - github.com/aws/aws-lambda-go v1.41.0 - github.com/aws/aws-sdk-go v1.45.6 + github.com/aws/aws-lambda-go v1.42.0 + github.com/aws/aws-sdk-go v1.48.16 github.com/golang/mock v1.6.0 + github.com/google/go-cmp v0.6.0 github.com/newrelic/go-agent v3.24.1+incompatible - github.com/prometheus/client_golang v1.16.0 - github.com/prometheus/client_model v0.4.0 - google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc - google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc + github.com/prometheus/client_golang v1.17.0 + github.com/prometheus/client_model v0.5.0 + google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b google.golang.org/protobuf v1.31.0 ) require ( - cloud.google.com/go/compute v1.19.3 // indirect + cloud.google.com/go/compute v1.23.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/s2a-go v0.1.4 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.11.0 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.10.1 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.11.1 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.9.0 // indirect + golang.org/x/crypto v0.17.0 // indirect golang.org/x/mod v0.9.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/sync v0.4.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.7.0 // indirect - google.golang.org/api v0.126.0 // indirect + google.golang.org/api v0.149.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/grpc v1.55.0 // indirect + google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/grpc v1.59.0 // indirect ) diff --git a/go.sum b/go.sum index 0d154043..62dafd17 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,33 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= -cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds= -cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= +cloud.google.com/go/compute v1.23.1 h1:V97tBoDaZHb6leicZ1G6DLK2BAaZLJ/7+9BB/En3hR0= +cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/monitoring v1.15.1 h1:65JhLMd+JiYnXr6j5Z63dUYCuOg770p8a/VC+gil/58= -cloud.google.com/go/monitoring v1.15.1/go.mod h1:lADlSAlFdbqQuwwpaImhsJXu1QSdd3ojypXrFSMr2rM= +cloud.google.com/go/monitoring v1.17.0 h1:blrdvF0MkPPivSO041ihul7rFMhXdVp8Uq7F59DKXTU= +cloud.google.com/go/monitoring v1.17.0/go.mod h1:KwSsX5+8PnXv5NJnICZzW2R8pWTis8ypC4zmdRD63Tw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q= github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y= -github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= -github.com/aws/aws-sdk-go v1.45.6 h1:Y2isQQBZsnO15dzUQo9YQRThtHgrV200XCH05BRHVJI= -github.com/aws/aws-sdk-go v1.45.6/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-lambda-go v1.42.0 h1:U4QKkxLp/il15RJGAANxiT9VumQzimsUER7gokqA0+c= +github.com/aws/aws-lambda-go v1.42.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/aws-sdk-go v1.48.16 h1:mcj2/9J/MJ55Dov+ocMevhR8Jv6jW/fAxbrn4a1JFc8= +github.com/aws/aws-sdk-go v1.48.16/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -50,17 +38,14 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -70,16 +55,15 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -88,104 +72,76 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/newrelic/go-agent v3.24.1+incompatible h1:Dvt6dtEafdrNydNsEHbtSMljWUt8pyWmcaO/wVJmuf8= github.com/newrelic/go-agent v3.24.1+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= -github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -193,39 +149,34 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= -google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= +google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -240,8 +191,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lambda/main.go b/lambda/main.go index a2ea4cb7..879c7b2c 100644 --- a/lambda/main.go +++ b/lambda/main.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "log" "os" "strconv" @@ -17,10 +17,10 @@ import ( "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/aws-sdk-go/service/ssm" - "github.com/buildkite/buildkite-agent-metrics/backend" - "github.com/buildkite/buildkite-agent-metrics/collector" - "github.com/buildkite/buildkite-agent-metrics/token" - "github.com/buildkite/buildkite-agent-metrics/version" + "github.com/buildkite/buildkite-agent-metrics/v5/backend" + "github.com/buildkite/buildkite-agent-metrics/v5/collector" + "github.com/buildkite/buildkite-agent-metrics/v5/token" + "github.com/buildkite/buildkite-agent-metrics/v5/version" ) const ( @@ -46,10 +46,8 @@ func main() { } func Handler(ctx context.Context, evt json.RawMessage) (string, error) { - var b backend.Backend - var provider token.Provider - var bkToken string - var err error + // Where we send metrics + var metricsBackend backend.Backend awsRegion := os.Getenv("AWS_REGION") backendOpt := os.Getenv("BUILDKITE_BACKEND") @@ -59,26 +57,36 @@ func Handler(ctx context.Context, evt json.RawMessage) (string, error) { quiet := quietString == "1" || quietString == "true" timeout := os.Getenv("BUILDKITE_AGENT_METRICS_TIMEOUT") + debugEnvVar := os.Getenv("BUILDKITE_AGENT_METRICS_DEBUG") + debug := debugEnvVar == "1" || debugEnvVar == "true" + + debugHTTPEnvVar := os.Getenv("BUILDKITE_AGENT_METRICS_DEBUG_HTTP") + debugHTTP := debugHTTPEnvVar == "1" || debugHTTPEnvVar == "true" + if quiet { - log.SetOutput(ioutil.Discard) + log.SetOutput(io.Discard) } - t := time.Now() + startTime := time.Now() - if !nextPollTime.IsZero() && nextPollTime.After(t) { + if !nextPollTime.IsZero() && nextPollTime.After(startTime) { log.Printf("Skipping polling, next poll time is in %v", - nextPollTime.Sub(t)) + nextPollTime.Sub(startTime)) return "", nil } - provider, err = initTokenProvider(awsRegion) + providers, err := initTokenProvider(awsRegion) if err != nil { return "", err } - bkToken, err = provider.Get() - if err != nil { - return "", err + tokens := make([]string, 0) + for _, provider := range providers { + bkToken, err := provider.Get() + if err != nil { + return "", err + } + tokens = append(tokens, bkToken) } queues := []string{} @@ -98,54 +106,67 @@ func Handler(ctx context.Context, evt json.RawMessage) (string, error) { userAgent := fmt.Sprintf("buildkite-agent-metrics/%s buildkite-agent-metrics-lambda", version.Version) - c := collector.Collector{ - UserAgent: userAgent, - Endpoint: "https://agent.buildkite.com/v3", - Token: bkToken, - Queues: queues, - Quiet: quiet, - Debug: false, - DebugHttp: false, - Timeout: configuredTimeout, + collectors := make([]*collector.Collector, 0, len(tokens)) + for _, token := range tokens { + collectors = append(collectors, &collector.Collector{ + UserAgent: userAgent, + Endpoint: "https://agent.buildkite.com/v3", + Token: token, + Queues: queues, + Quiet: quiet, + Debug: debug, + DebugHttp: debugHTTP, + Timeout: configuredTimeout, + }) } switch strings.ToLower(backendOpt) { case "statsd": statsdHost := os.Getenv("STATSD_HOST") statsdTags := strings.EqualFold(os.Getenv("STATSD_TAGS"), "true") - b, err = backend.NewStatsDBackend(statsdHost, statsdTags) + metricsBackend, err = backend.NewStatsDBackend(statsdHost, statsdTags) if err != nil { return "", err } + case "newrelic": nrAppName := os.Getenv("NEWRELIC_APP_NAME") nrLicenseKey := os.Getenv("NEWRELIC_LICENSE_KEY") - b, err = backend.NewNewRelicBackend(nrAppName, nrLicenseKey) + metricsBackend, err = backend.NewNewRelicBackend(nrAppName, nrLicenseKey) if err != nil { fmt.Printf("Error starting New Relic client: %v\n", err) os.Exit(1) } + default: dimensions, err := backend.ParseCloudWatchDimensions(clwDimensions) if err != nil { return "", err } - b = backend.NewCloudWatchBackend(awsRegion, dimensions) + metricsBackend = backend.NewCloudWatchBackend(awsRegion, dimensions) } - res, err := c.Collect() - if err != nil { - return "", err - } + // minimum res.PollDuration across collectors + var pollDuration time.Duration - res.Dump() + for _, c := range collectors { + res, err := c.Collect() + if err != nil { + return "", err + } - err = b.Collect(res) - if err != nil { - return "", err + if res.PollDuration > pollDuration { + pollDuration = res.PollDuration + } + + res.Dump() + + if err := metricsBackend.Collect(res); err != nil { + return "", err + } } - original, ok := b.(backend.Closer) + original, ok := metricsBackend.(backend.Closer) if ok { err := original.Close() if err != nil { @@ -153,27 +174,34 @@ func Handler(ctx context.Context, evt json.RawMessage) (string, error) { } } - log.Printf("Finished in %s", time.Now().Sub(t)) + log.Printf("Finished in %s", time.Since(startTime)) // Store the next acceptable poll time in global state - nextPollTime = time.Now().Add(res.PollDuration) + nextPollTime = time.Now().Add(pollDuration) return "", nil } -func initTokenProvider(awsRegion string) (token.Provider, error) { - mutuallyExclusiveEnvVars := []string{ +func initTokenProvider(awsRegion string) ([]token.Provider, error) { + err := checkMutuallyExclusiveEnvVars( BKAgentTokenEnvVar, BKAgentTokenSSMKeyEnvVar, BKAgentTokenSecretsManagerSecretIDEnvVar, - } - - if err := checkMutuallyExclusiveEnvVars(mutuallyExclusiveEnvVars...); err != nil { + ) + if err != nil { return nil, err } - if bkToken := os.Getenv(BKAgentTokenEnvVar); bkToken != "" { - return token.NewInMemory(bkToken) + var providers []token.Provider + if bkTokenEnvVar := os.Getenv(BKAgentTokenEnvVar); bkTokenEnvVar != "" { + bkTokens := strings.Split(bkTokenEnvVar, ",") + for _, bkToken := range bkTokens { + provider, err := token.NewInMemory(bkToken) + if err != nil { + return nil, err + } + providers = append(providers, provider) + } } if ssmKey := os.Getenv(BKAgentTokenSSMKeyEnvVar); ssmKey != "" { @@ -182,7 +210,11 @@ func initTokenProvider(awsRegion string) (token.Provider, error) { return nil, err } client := ssm.New(sess) - return token.NewSSM(client, ssmKey) + provider, err := token.NewSSM(client, ssmKey) + if err != nil { + return nil, err + } + providers = append(providers, provider) } if secretsManagerSecretID := os.Getenv(BKAgentTokenSecretsManagerSecretIDEnvVar); secretsManagerSecretID != "" { @@ -193,14 +225,32 @@ func initTokenProvider(awsRegion string) (token.Provider, error) { } client := secretsmanager.New(sess) if jsonKey == "" { - return token.NewSecretsManager(client, secretsManagerSecretID) + secretIDs := strings.Split(secretsManagerSecretID, ",") + for _, secretID := range secretIDs { + secretManager, err := token.NewSecretsManager(client, secretID) + if err != nil { + return nil, err + } + providers = append(providers, secretManager) + } } else { - return token.NewSecretsManager(client, secretsManagerSecretID, token.WithSecretsManagerJSONSecret(jsonKey)) + secretManager, err := token.NewSecretsManager(client, secretsManagerSecretID, token.WithSecretsManagerJSONSecret(jsonKey)) + if err != nil { + return nil, err + } + providers = append(providers, secretManager) } } - return nil, fmt.Errorf("failed to initialize Buildkite token provider: one of the [%s] environment variables "+ - "must be provided", strings.Join(mutuallyExclusiveEnvVars, ",")) + if len(providers) == 0 { + // This should be very unlikely or even impossible (famous last words): + // - There was exactly one of the mutually-exclusive env vars + // - If a token provider above failed to use its value, it should error + // - Otherwise, each if-branch appends to providers, so... + return nil, fmt.Errorf("no Buildkite token providers could be created") + } + + return providers, nil } func checkMutuallyExclusiveEnvVars(varNames ...string) error { @@ -211,8 +261,14 @@ func checkMutuallyExclusiveEnvVars(varNames ...string) error { foundVars = append(foundVars, value) } } - if len(foundVars) > 1 { + switch len(foundVars) { + case 0: + return fmt.Errorf("one of the environment variables [%s] must be provided", strings.Join(varNames, ",")) + + case 1: + return nil // that's what we want + + default: return fmt.Errorf("the environment variables [%s] are mutually exclusive", strings.Join(foundVars, ",")) } - return nil } diff --git a/main.go b/main.go index 1522fdb7..24a94729 100644 --- a/main.go +++ b/main.go @@ -4,22 +4,22 @@ import ( "errors" "flag" "fmt" - "io/ioutil" + "io" "log" "os" "strings" "time" - "github.com/buildkite/buildkite-agent-metrics/backend" - "github.com/buildkite/buildkite-agent-metrics/collector" - "github.com/buildkite/buildkite-agent-metrics/version" + "github.com/buildkite/buildkite-agent-metrics/v5/backend" + "github.com/buildkite/buildkite-agent-metrics/v5/collector" + "github.com/buildkite/buildkite-agent-metrics/v5/version" ) -var bk backend.Backend +// Where we send metrics +var metricsBackend backend.Backend func main() { var ( - token = flag.String("token", "", "A Buildkite Agent Registration Token") interval = flag.Duration("interval", 0, "Update metrics every interval, rather than once") showVersion = flag.Bool("version", false, "Show the version") quiet = flag.Bool("quiet", false, "Only print errors") @@ -30,7 +30,7 @@ func main() { timeout = flag.Int("timeout", 15, "Timeout, in seconds, for HTTP requests to Buildkite API") // backend config - backendOpt = flag.String("backend", "cloudwatch", "Specify the backend to use: cloudwatch, statsd, prometheus, stackdriver") + backendOpt = flag.String("backend", "cloudwatch", "Specify the backend to use: cloudwatch, newrelic, prometheus, stackdriver, statsd") statsdHost = flag.String("statsd-host", "127.0.0.1:8125", "Specify the StatsD server") statsdTags = flag.Bool("statsd-tags", false, "Whether your StatsD server supports tagging like Datadog") prometheusAddr = flag.String("prometheus-addr", ":8080", "Prometheus metrics transport bind address") @@ -42,8 +42,9 @@ func main() { nrLicenseKey = flag.String("newrelic-license-key", "", "New Relic license key for publishing events") ) - // custom config for multiple queues - var queues stringSliceFlag + // custom config for multiple tokens and queues + var tokens, queues stringSliceFlag + flag.Var(&tokens, "token", "Buildkite Agent registration tokens. At least one is required. Multiple cluster tokens can be used to gather metrics for multiple clusters.") flag.Var(&queues, "queue", "Specific queues to process") flag.Parse() @@ -53,15 +54,22 @@ func main() { os.Exit(0) } - if *token == "" { - if bkToken := os.Getenv("BUILDKITE_AGENT_TOKEN"); bkToken != "" { - *token = bkToken - } else { - fmt.Println("Must provide a token") - os.Exit(1) + if len(tokens) == 0 { + envTokens := strings.Split(os.Getenv("BUILDKITE_AGENT_TOKEN"), ",") + for _, t := range envTokens { + t = strings.TrimSpace(t) + if t == "" { + continue + } + tokens = append(tokens, t) } } + if len(tokens) == 0 { + fmt.Println("Must provide at least one token with either --token or BUILDKITE_AGENT_TOKEN") + os.Exit(1) + } + var err error switch strings.ToLower(*backendOpt) { case "cloudwatch": @@ -76,34 +84,44 @@ func main() { fmt.Println(err) os.Exit(1) } - bk = backend.NewCloudWatchBackend(region, dimensions) + metricsBackend = backend.NewCloudWatchBackend(region, dimensions) + case "statsd": - bk, err = backend.NewStatsDBackend(*statsdHost, *statsdTags) + metricsBackend, err = backend.NewStatsDBackend(*statsdHost, *statsdTags) if err != nil { fmt.Printf("Error starting StatsD, err: %v\n", err) os.Exit(1) } + case "prometheus": - bk = backend.NewPrometheusBackend(*prometheusPath, *prometheusAddr) + prom := backend.NewPrometheusBackend() + go prom.Serve(*prometheusPath, *prometheusAddr) + metricsBackend = prom + case "stackdriver": - bk, err = backend.NewStackDriverBackend(*gcpProjectID) + if *gcpProjectID == "" { + *gcpProjectID = os.Getenv(`GCP_PROJECT_ID`) + } + metricsBackend, err = backend.NewStackDriverBackend(*gcpProjectID) if err != nil { fmt.Printf("Error starting Stackdriver backend, err: %v\n", err) os.Exit(1) } + case "newrelic": - bk, err = backend.NewNewRelicBackend(*nrAppName, *nrLicenseKey) + metricsBackend, err = backend.NewNewRelicBackend(*nrAppName, *nrLicenseKey) if err != nil { fmt.Printf("Error starting New Relic client: %v\n", err) os.Exit(1) } + default: - fmt.Println("Must provide a supported backend: cloudwatch, statsd, prometheus, stackdriver, newrelic") + fmt.Println("Must provide a supported backend: cloudwatch, newrelic, prometheus, stackdriver, statsd") os.Exit(1) } if *quiet { - log.SetOutput(ioutil.Discard) + log.SetOutput(io.Discard) } userAgent := fmt.Sprintf("buildkite-agent-metrics/%s buildkite-agent-metrics-cli", version.Version) @@ -111,42 +129,65 @@ func main() { userAgent += fmt.Sprintf(" interval=%s", *interval) } - c := collector.Collector{ - UserAgent: userAgent, - Endpoint: *endpoint, - Token: *token, - Queues: []string(queues), - Quiet: *quiet, - Debug: *debug, - DebugHttp: *debugHttp, - Timeout: *timeout, + // Queues passed as flags take precedence. But if no queues are passed in we + // check env vars. If no env vars are defined we default to ingesting metrics + // for all queues. + // NOTE: `BUILDKITE_QUEUE` is a comma separated string of queues + // i.e. "default,deploy,test" + if len(queues) == 0 { + if q, exists := os.LookupEnv(`BUILDKITE_QUEUE`); exists { + queues = strings.Split(q, ",") + } } - f := func() (time.Duration, error) { - t := time.Now() + collectors := make([]*collector.Collector, 0, len(tokens)) + for _, token := range tokens { + collectors = append(collectors, &collector.Collector{ + UserAgent: userAgent, + Endpoint: *endpoint, + Token: token, + Queues: []string(queues), + Quiet: *quiet, + Debug: *debug, + DebugHttp: *debugHttp, + Timeout: *timeout, + }) + } - result, err := c.Collect() - if err != nil { - fmt.Printf("Error collecting agent metrics, err: %s\n", err) - if errors.Is(err, collector.ErrUnauthorized) { - // Unique exit code to signal HTTP 401 - os.Exit(4) - } - return time.Duration(0), err - } + collectFunc := func() (time.Duration, error) { + start := time.Now() + + // minimum result.PollDuration across collectors + var pollDuration time.Duration - if !*dryRun { - err = bk.Collect(result) + for _, c := range collectors { + result, err := c.Collect() if err != nil { + fmt.Printf("Error collecting agent metrics, err: %s\n", err) + if errors.Is(err, collector.ErrUnauthorized) { + // Unique exit code to signal HTTP 401 + os.Exit(4) + } return time.Duration(0), err } + + if *dryRun { + continue + } + + if err := metricsBackend.Collect(result); err != nil { + return time.Duration(0), err + } + if result.PollDuration > pollDuration { + pollDuration = result.PollDuration + } } - log.Printf("Finished in %s", time.Now().Sub(t)) - return result.PollDuration, nil + log.Printf("Finished in %s", time.Since(start)) + return pollDuration, nil } - minPollDuration, err := f() + minPollDuration, err := collectFunc() if err != nil { fmt.Println(err) } @@ -164,7 +205,7 @@ func main() { log.Printf("Waiting for %v (minimum of %v)", waitTime, minPollDuration) time.Sleep(waitTime) - minPollDuration, err = f() + minPollDuration, err = collectFunc() if err != nil { fmt.Println(err) } diff --git a/token/secretsmanager_test.go b/token/secretsmanager_test.go index 5db7183a..5a9fdb7e 100644 --- a/token/secretsmanager_test.go +++ b/token/secretsmanager_test.go @@ -7,7 +7,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/secretsmanager" - "github.com/buildkite/buildkite-agent-metrics/token/mock" + "github.com/buildkite/buildkite-agent-metrics/v5/token/mock" "github.com/golang/mock/gomock" ) diff --git a/token/ssm_test.go b/token/ssm_test.go index 6f57a0e8..292b239c 100644 --- a/token/ssm_test.go +++ b/token/ssm_test.go @@ -6,7 +6,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ssm" - "github.com/buildkite/buildkite-agent-metrics/token/mock" + "github.com/buildkite/buildkite-agent-metrics/v5/token/mock" "github.com/golang/mock/gomock" ) diff --git a/version/version.go b/version/version.go index 5edc9bcb..8d0160bb 100644 --- a/version/version.go +++ b/version/version.go @@ -1,4 +1,4 @@ package version // Version the library version number -const Version = "5.8.0" +const Version = "5.9.3"