feat: Add stats support for basic operations (#2147)

This commit is contained in:
Nish Krishnan
2022-03-18 15:18:13 -07:00
committed by GitHub
parent 2453e43ab1
commit d1d1539ced
35 changed files with 1642 additions and 147 deletions

View File

@@ -80,6 +80,7 @@ const (
HidePrevPlanComments = "hide-prev-plan-comments"
LogLevelFlag = "log-level"
ParallelPoolSize = "parallel-pool-size"
StatsNamespace = "stats-namespace"
AllowDraftPRs = "allow-draft-prs"
PortFlag = "port"
RepoConfigFlag = "repo-config"
@@ -120,6 +121,7 @@ const (
DefaultGitlabHostname = "gitlab.com"
DefaultLogLevel = "info"
DefaultParallelPoolSize = 15
DefaultStatsNamespace = "atlantis"
DefaultPort = 4141
DefaultTFDownloadURL = "https://releases.hashicorp.com"
DefaultTFEHostname = "app.terraform.io"
@@ -259,6 +261,10 @@ var stringFlags = map[string]stringFlag{
description: "Log level. Either debug, info, warn, or error.",
defaultValue: DefaultLogLevel,
},
StatsNamespace: {
description: "Namespace for aggregating stats.",
defaultValue: DefaultStatsNamespace,
},
RepoConfigFlag: {
description: "Path to a repo config file, used to customize how Atlantis runs on each repo. See runatlantis.io/docs for more details.",
},
@@ -652,6 +658,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) {
if c.ParallelPoolSize == 0 {
c.ParallelPoolSize = DefaultParallelPoolSize
}
if c.StatsNamespace == "" {
c.StatsNamespace = DefaultStatsNamespace
}
if c.Port == 0 {
c.Port = DefaultPort
}

View File

@@ -85,6 +85,7 @@ var testFlags = map[string]interface{}{
GitlabUserFlag: "gitlab-user",
GitlabWebhookSecretFlag: "gitlab-secret",
LogLevelFlag: "debug",
StatsNamespace: "atlantis",
AllowDraftPRs: true,
PortFlag: 8181,
ParallelPoolSize: 100,

15
go.mod
View File

@@ -80,7 +80,7 @@ require (
github.com/huandu/xstrings v1.3.1 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.3.1-0.20200310193758-2437e8417af5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.11.2 // indirect
github.com/kr/pretty v0.2.0 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
@@ -93,7 +93,6 @@ require (
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/onsi/ginkgo v1.14.0 // indirect
github.com/onsi/gomega v1.10.1 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -101,7 +100,7 @@ require (
github.com/shopspring/decimal v1.2.0 // indirect
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.6.1-0.20200528085638-6699a89a232f // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
@@ -109,7 +108,7 @@ require (
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/zclconf/go-cty v1.5.1 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
@@ -128,3 +127,11 @@ require (
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gotest.tools v2.2.0+incompatible // indirect
)
require (
github.com/cactus/go-statsd-client/statsd v0.0.0-20200623234511-94959e3146b2
github.com/twmb/murmur3 v1.1.6 // indirect
github.com/uber-go/tally v3.4.3+incompatible
)
require github.com/onsi/ginkgo v1.14.0 // indirect

19
go.sum
View File

@@ -95,6 +95,8 @@ github.com/bradleyfalzon/ghinstallation/v2 v2.0.4 h1:tXKVfhE7FcSkhkv0UwkLvPDeZ4k
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4/go.mod h1:B40qPqJxWE0jDZgOR1JmaMy+4AY1eBP+IByOvqyAKp0=
github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5 h1:osZyZB7J4kE1tKLeaUjV6+uZVBfS835T0I/RxmwWw1w=
github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5/go.mod h1:hw/JEQBIE+c/BLI4aKM8UU8v+ZqrD3h7HC27kKt8JQU=
github.com/cactus/go-statsd-client/statsd v0.0.0-20200623234511-94959e3146b2 h1:GgJnJEJYymy/lx+1zXOO2TvGPRQJJ9vz4onxnA9gF3k=
github.com/cactus/go-statsd-client/statsd v0.0.0-20200623234511-94959e3146b2/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -314,8 +316,10 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmespath/go-jmespath v0.3.1-0.20200310193758-2437e8417af5 h1:1G6l+WClVmbflmgW0Wsr6a50KeKCQcYKv/vUjtQUHuw=
github.com/jmespath/go-jmespath v0.3.1-0.20200310193758-2437e8417af5/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
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=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -455,8 +459,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.1-0.20200528085638-6699a89a232f h1:qqqIhBDFUBrbMezIyJkKWIpf+E5CdObleGMjW1s19Hg=
github.com/sirupsen/logrus v1.6.1-0.20200528085638-6699a89a232f/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
@@ -485,6 +489,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/uber-go/tally v3.4.3+incompatible h1:Oq25FXV8cWHPRo+EPeNdbN3LfuozC9mDK2/4vZ1k38U=
github.com/uber-go/tally v3.4.3+incompatible/go.mod h1:YDTIBxdXyOU/sCWilKB4bgyufu1cEi0jdVnRdxvjnmU=
github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
@@ -517,8 +525,9 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=

View File

@@ -600,6 +600,12 @@ Values are chosen in this order:
```
File containing x509 private key matching `--ssl-cert-file`.
* ### `--stats-namespace`
```bash
atlantis server --stats-namespace="myatlantis"
```
Namespace for emitting stats/metrics. See (stats.html#Metrics/Stats)
* ### `--tf-download-url`
```bash
atlantis server --tf-download-url="https://releases.company.com"

View File

@@ -1,16 +1,18 @@
# Server Side Repo Config
A Server-Side Repo Config file is used to control per-repo behaviour
# Server Side Config
A Server-Side Config file is used for more groups of server config that can't reasonably be expressed through flags.
One such usecase is to control per-repo behaviour
and what users can do in repo-level `atlantis.yaml` files.
[[toc]]
## Do I Need A Server-Side Repo Config File?
## Do I Need A Server-Side Config File?
You do not need a server-side repo config file unless you want to customize
some aspect of Atlantis on a per-repo basis.
Read through the [use-cases](#use-cases) to determine if you need it.
## Enabling Server Side Repo Config
## Enabling Server Side Config
To use server side repo config create a config file, ex. `repos.yaml`, and pass it to
the `atlantis server` command via the `--repo-config` flag, ex. `--repo-config=path/to/repos.yaml`.
@@ -453,3 +455,17 @@ If you set a workflow with the key `default`, it will override this.
| name | string | none | yes | unique name for the policy set |
| path | string | none | yes | path to the rego policies directory |
| source | string | none | yes | only `local` is supported at this time |
### Metrics
| Key | Type | Default | Required | Description |
|------------------------|-----------------|---------|-----------|------------------------------------------|
| statsd | Statsd(#Statsd) | none | no | Statsd metrics provider |
### Statsd
| Key | Type | Default | Required | Description |
| ------ | ------ | ------- | -------- | -------------------------------------- |
| host | string | none | yes | statsd host ip address |
| port | string | none | yes | statsd port |

View File

@@ -0,0 +1,11 @@
# Metrics/Stats
Atlantis exposes a set of metrics for each of its operations including errors, successes, and latencies.
::: warning NOTE
Only statsd is supported currently, but it should be relatively straightforward to add other providers such as prometheus.
:::
## Configuration
Metrics are configured through the (server-side-repo-config.html#Metrics).

View File

@@ -29,6 +29,7 @@ import (
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud"
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver"
"github.com/runatlantis/atlantis/server/logging"
"github.com/uber-go/tally"
gitlab "github.com/xanzy/go-gitlab"
)
@@ -48,6 +49,7 @@ type VCSEventsController struct {
CommandRunner events.CommandRunner
PullCleaner events.PullCleaner
Logger logging.SimpleLogging
Scope tally.Scope
Parser events.EventParsing
CommentParser events.CommentParsing
ApplyDisabled bool
@@ -136,6 +138,16 @@ func (e *VCSEventsController) Post(w http.ResponseWriter, r *http.Request) {
e.respond(w, logging.Debug, http.StatusBadRequest, "Ignoring request")
}
type HTTPError struct {
err error
code int
}
type HTTPResponse struct {
body string
err HTTPError
}
func (e *VCSEventsController) handleGithubPost(w http.ResponseWriter, r *http.Request) {
// Validate the request against the optional webhook secret.
payload, err := e.GithubRequestValidator.Validate(r, e.GithubWebhookSecret)
@@ -143,20 +155,41 @@ func (e *VCSEventsController) handleGithubPost(w http.ResponseWriter, r *http.Re
e.respond(w, logging.Warn, http.StatusBadRequest, err.Error())
return
}
e.Logger.Debug("request valid")
githubReqID := "X-Github-Delivery=" + r.Header.Get("X-Github-Delivery")
logger := e.Logger.With("gh-request-id", githubReqID)
scope := e.Scope.SubScope("github.event")
logger.Debug("request valid")
event, _ := github.ParseWebHook(github.WebHookType(r), payload)
var resp HTTPResponse
switch event := event.(type) {
case *github.IssueCommentEvent:
e.Logger.Debug("handling as comment event")
e.HandleGithubCommentEvent(w, event, githubReqID)
resp = e.HandleGithubCommentEvent(event, githubReqID, logger)
scope = scope.SubScope(fmt.Sprintf("comment.%s", *event.Action))
case *github.PullRequestEvent:
e.Logger.Debug("handling as pull request event")
e.HandleGithubPullRequestEvent(w, event, githubReqID)
resp = e.HandleGithubPullRequestEvent(logger, event, githubReqID)
scope = scope.SubScope(fmt.Sprintf("pr.%s", *event.Action))
default:
e.respond(w, logging.Debug, http.StatusOK, "Ignoring unsupported event %s", githubReqID)
resp = HTTPResponse{
body: fmt.Sprintf("Ignoring unsupported event %s", githubReqID),
}
}
if resp.err.code != 0 {
logger.Err("error handling gh post code: %d err: %s", resp.err.code, resp.err.err.Error())
scope.Counter(fmt.Sprintf("error_%d", resp.err.code)).Inc(1)
w.WriteHeader(resp.err.code)
fmt.Fprintln(w, resp.err.err.Error())
return
}
scope.Counter(fmt.Sprintf("success_%d", http.StatusOK)).Inc(1)
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, resp.body)
}
func (e *VCSEventsController) handleBitbucketCloudPost(w http.ResponseWriter, r *http.Request) {
@@ -247,21 +280,29 @@ func (e *VCSEventsController) handleAzureDevopsPost(w http.ResponseWriter, r *ht
// HandleGithubCommentEvent handles comment events from GitHub where Atlantis
// commands can come from. It's exported to make testing easier.
func (e *VCSEventsController) HandleGithubCommentEvent(w http.ResponseWriter, event *github.IssueCommentEvent, githubReqID string) {
func (e *VCSEventsController) HandleGithubCommentEvent(event *github.IssueCommentEvent, githubReqID string, logger logging.SimpleLogging) HTTPResponse {
if event.GetAction() != "created" {
e.respond(w, logging.Debug, http.StatusOK, "Ignoring comment event since action was not created %s", githubReqID)
return
return HTTPResponse{
body: fmt.Sprintf("Ignoring comment event since action was not created %s", githubReqID),
}
}
baseRepo, user, pullNum, err := e.Parser.ParseGithubIssueCommentEvent(event)
wrapped := errors.Wrapf(err, "Failed parsing event: %s", githubReqID)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Failed parsing event: %v %s", err, githubReqID)
return
return HTTPResponse{
body: wrapped.Error(),
err: HTTPError{
code: http.StatusBadRequest,
err: wrapped,
},
}
}
// We pass in nil for maybeHeadRepo because the head repo data isn't
// available in the GithubIssueComment event.
e.handleCommentEvent(w, baseRepo, nil, nil, user, pullNum, event.Comment.GetBody(), models.Github)
return e.handleCommentEvent(logger, baseRepo, nil, nil, user, pullNum, event.Comment.GetBody(), models.Github)
}
// HandleBitbucketCloudCommentEvent handles comment events from Bitbucket.
@@ -271,7 +312,18 @@ func (e *VCSEventsController) HandleBitbucketCloudCommentEvent(w http.ResponseWr
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID)
return
}
e.handleCommentEvent(w, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.BitbucketCloud)
resp := e.handleCommentEvent(e.Logger, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.BitbucketCloud)
//TODO: move this to the outer most function similar to github
lvl := logging.Debug
code := http.StatusOK
msg := resp.body
if resp.err.code != 0 {
lvl = logging.Error
code = resp.err.code
msg = resp.err.err.Error()
}
e.respond(w, lvl, code, msg)
}
// HandleBitbucketServerCommentEvent handles comment events from Bitbucket.
@@ -281,7 +333,18 @@ func (e *VCSEventsController) HandleBitbucketServerCommentEvent(w http.ResponseW
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID)
return
}
e.handleCommentEvent(w, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.BitbucketCloud)
resp := e.handleCommentEvent(e.Logger, baseRepo, &headRepo, &pull, user, pull.Num, comment, models.BitbucketCloud)
//TODO: move this to the outer most function similar to github
lvl := logging.Debug
code := http.StatusOK
msg := resp.body
if resp.err.code != 0 {
lvl = logging.Error
code = resp.err.code
msg = resp.err.err.Error()
}
e.respond(w, lvl, code, msg)
}
func (e *VCSEventsController) handleBitbucketCloudPullRequestEvent(w http.ResponseWriter, eventType string, body []byte, reqID string) {
@@ -292,7 +355,18 @@ func (e *VCSEventsController) handleBitbucketCloudPullRequestEvent(w http.Respon
}
pullEventType := e.Parser.GetBitbucketCloudPullEventType(eventType)
e.Logger.Info("identified event as type %q", pullEventType.String())
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
resp := e.handlePullRequestEvent(e.Logger, baseRepo, headRepo, pull, user, pullEventType)
//TODO: move this to the outer most function similar to github
lvl := logging.Debug
code := http.StatusOK
msg := resp.body
if resp.err.code != 0 {
lvl = logging.Error
code = resp.err.code
msg = resp.err.err.Error()
}
e.respond(w, lvl, code, msg)
}
func (e *VCSEventsController) handleBitbucketServerPullRequestEvent(w http.ResponseWriter, eventType string, body []byte, reqID string) {
@@ -303,23 +377,40 @@ func (e *VCSEventsController) handleBitbucketServerPullRequestEvent(w http.Respo
}
pullEventType := e.Parser.GetBitbucketServerPullEventType(eventType)
e.Logger.Info("identified event as type %q", pullEventType.String())
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
resp := e.handlePullRequestEvent(e.Logger, baseRepo, headRepo, pull, user, pullEventType)
//TODO: move this to the outer most function similar to github
lvl := logging.Debug
code := http.StatusOK
msg := resp.body
if resp.err.code != 0 {
lvl = logging.Error
code = resp.err.code
msg = resp.err.err.Error()
}
e.respond(w, lvl, code, msg)
}
// HandleGithubPullRequestEvent will delete any locks associated with the pull
// request if the event is a pull request closed event. It's exported to make
// testing easier.
func (e *VCSEventsController) HandleGithubPullRequestEvent(w http.ResponseWriter, pullEvent *github.PullRequestEvent, githubReqID string) {
func (e *VCSEventsController) HandleGithubPullRequestEvent(logger logging.SimpleLogging, pullEvent *github.PullRequestEvent, githubReqID string) HTTPResponse {
pull, pullEventType, baseRepo, headRepo, user, err := e.Parser.ParseGithubPullEvent(pullEvent)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s", err, githubReqID)
return
wrapped := errors.Wrapf(err, "Error parsing pull data: %s %s", err, githubReqID)
return HTTPResponse{
body: wrapped.Error(),
err: HTTPError{
code: http.StatusBadRequest,
err: wrapped,
},
}
}
e.Logger.Info("identified event as type %q", pullEventType.String())
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
logger.Debug("identified event as type %q", pullEventType.String())
return e.handlePullRequestEvent(logger, baseRepo, headRepo, pull, user, pullEventType)
}
func (e *VCSEventsController) handlePullRequestEvent(w http.ResponseWriter, baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User, eventType models.PullRequestEventType) {
func (e *VCSEventsController) handlePullRequestEvent(logger logging.SimpleLogging, baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User, eventType models.PullRequestEventType) HTTPResponse {
if !e.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) {
// If the repo isn't allowlisted and we receive an opened pull request
// event we comment back on the pull request that the repo isn't
@@ -328,10 +419,16 @@ func (e *VCSEventsController) handlePullRequestEvent(w http.ResponseWriter, base
if eventType == models.OpenedPullEvent {
e.commentNotAllowlisted(baseRepo, pull.Num)
}
e.respond(w, logging.Debug, http.StatusForbidden,
"Ignoring pull request event from non-allowlisted repo \"%s/%s\"",
baseRepo.VCSHost.Hostname, baseRepo.FullName)
return
err := errors.Errorf("Pull request event from non-allowlisted repo \"%s/%s\"", baseRepo.VCSHost.Hostname, baseRepo.FullName)
return HTTPResponse{
body: err.Error(),
err: HTTPError{
code: http.StatusForbidden,
err: err,
},
}
}
switch eventType {
@@ -341,30 +438,37 @@ func (e *VCSEventsController) handlePullRequestEvent(w http.ResponseWriter, base
// Respond with success and then actually execute the command asynchronously.
// We use a goroutine so that this function returns and the connection is
// closed.
fmt.Fprintln(w, "Processing...")
e.Logger.Info("executing autoplan")
if !e.TestingMode {
go e.CommandRunner.RunAutoplanCommand(baseRepo, headRepo, pull, user)
} else {
// When testing we want to wait for everything to complete.
e.CommandRunner.RunAutoplanCommand(baseRepo, headRepo, pull, user)
}
return
return HTTPResponse{
body: "Processing...",
}
case models.ClosedPullEvent:
// If the pull request was closed, we delete locks.
if err := e.PullCleaner.CleanUpPull(baseRepo, pull); err != nil {
e.respond(w, logging.Error, http.StatusInternalServerError, "Error cleaning pull request: %s", err)
return
return HTTPResponse{
body: err.Error(),
err: HTTPError{
code: http.StatusForbidden,
err: err,
},
}
}
logger.Info("deleted locks and workspace for repo %s, pull %d", baseRepo.FullName, pull.Num)
return HTTPResponse{
body: "Pull request cleaned successfully",
}
e.Logger.Info("deleted locks and workspace for repo %s, pull %d", baseRepo.FullName, pull.Num)
fmt.Fprintln(w, "Pull request cleaned successfully")
return
case models.OtherPullEvent:
// Else we ignore the event.
e.respond(w, logging.Debug, http.StatusOK, "Ignoring non-actionable pull request event")
return
return HTTPResponse{
body: "Ignoring non-actionable pull request event",
}
}
return HTTPResponse{}
}
func (e *VCSEventsController) handleGitlabPost(w http.ResponseWriter, r *http.Request) {
@@ -400,10 +504,21 @@ func (e *VCSEventsController) HandleGitlabCommentEvent(w http.ResponseWriter, ev
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing webhook: %s", err)
return
}
e.handleCommentEvent(w, baseRepo, &headRepo, nil, user, event.MergeRequest.IID, event.ObjectAttributes.Note, models.Gitlab)
resp := e.handleCommentEvent(e.Logger, baseRepo, &headRepo, nil, user, event.MergeRequest.IID, event.ObjectAttributes.Note, models.Gitlab)
//TODO: move this to the outer most function similar to github
lvl := logging.Debug
code := http.StatusOK
msg := resp.body
if resp.err.code != 0 {
lvl = logging.Error
code = resp.err.code
msg = resp.err.err.Error()
}
e.respond(w, lvl, code, msg)
}
func (e *VCSEventsController) handleCommentEvent(w http.ResponseWriter, baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, comment string, vcsHost models.VCSHostType) {
func (e *VCSEventsController) handleCommentEvent(logger logging.SimpleLogging, baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, comment string, vcsHost models.VCSHostType) HTTPResponse {
parseResult := e.CommentParser.Parse(comment, vcsHost)
if parseResult.Ignore {
truncated := comment
@@ -411,17 +526,25 @@ func (e *VCSEventsController) handleCommentEvent(w http.ResponseWriter, baseRepo
if len(truncated) > truncateLen {
truncated = comment[:truncateLen] + "..."
}
e.respond(w, logging.Debug, http.StatusOK, "Ignoring non-command comment: %q", truncated)
return
return HTTPResponse{
body: fmt.Sprintf("Ignoring non-command comment: %q", truncated),
}
}
e.Logger.Info("parsed comment as %s", parseResult.Command)
logger.Info("parsed comment as %s", parseResult.Command)
// At this point we know it's a command we're not supposed to ignore, so now
// we check if this repo is allowed to run commands in the first place.
if !e.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) {
e.commentNotAllowlisted(baseRepo, pullNum)
e.respond(w, logging.Warn, http.StatusForbidden, "Repo not allowlisted")
return
err := errors.New("Repo not allowlisted")
return HTTPResponse{
body: err.Error(),
err: HTTPError{
err: err,
code: http.StatusForbidden,
},
}
}
// If the command isn't valid or doesn't require processing, ex.
@@ -430,14 +553,14 @@ func (e *VCSEventsController) handleCommentEvent(w http.ResponseWriter, baseRepo
// variable to comment back on the pull request.
if parseResult.CommentResponse != "" {
if err := e.VCSClient.CreateComment(baseRepo, pullNum, parseResult.CommentResponse, ""); err != nil {
e.Logger.Err("unable to comment on pull request: %s", err)
logger.Err("unable to comment on pull request: %s", err)
}
return HTTPResponse{
body: "Commenting back on pull request",
}
e.respond(w, logging.Info, http.StatusOK, "Commenting back on pull request")
return
}
e.Logger.Debug("executing command")
fmt.Fprintln(w, "Processing...")
logger.Debug("executing command")
if !e.TestingMode {
// Respond with success and then actually execute the command asynchronously.
// We use a goroutine so that this function returns and the connection is
@@ -447,6 +570,10 @@ func (e *VCSEventsController) handleCommentEvent(w http.ResponseWriter, baseRepo
// When testing we want to wait for everything to complete.
e.CommandRunner.RunCommentCommand(baseRepo, maybeHeadRepo, maybePull, user, pullNum, parseResult.Command)
}
return HTTPResponse{
body: "Processing...",
}
}
// HandleGitlabMergeRequestEvent will delete any locks associated with the pull
@@ -459,7 +586,18 @@ func (e *VCSEventsController) HandleGitlabMergeRequestEvent(w http.ResponseWrite
return
}
e.Logger.Info("identified event as type %q", pullEventType.String())
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
resp := e.handlePullRequestEvent(e.Logger, baseRepo, headRepo, pull, user, pullEventType)
//TODO: move this to the outer most function similar to github
lvl := logging.Debug
code := http.StatusOK
msg := resp.body
if resp.err.code != 0 {
lvl = logging.Error
code = resp.err.code
msg = resp.err.err.Error()
}
e.respond(w, lvl, code, msg)
}
// HandleAzureDevopsPullRequestCommentedEvent handles comment events from Azure DevOps where Atlantis
@@ -491,7 +629,18 @@ func (e *VCSEventsController) HandleAzureDevopsPullRequestCommentedEvent(w http.
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull request repository field: %s; %s", err, azuredevopsReqID)
return
}
e.handleCommentEvent(w, baseRepo, nil, nil, user, resource.PullRequest.GetPullRequestID(), string(strippedComment), models.AzureDevops)
resp := e.handleCommentEvent(e.Logger, baseRepo, nil, nil, user, resource.PullRequest.GetPullRequestID(), string(strippedComment), models.AzureDevops)
//TODO: move this to the outer most function similar to github
lvl := logging.Debug
code := http.StatusOK
msg := resp.body
if resp.err.code != 0 {
lvl = logging.Error
code = resp.err.code
msg = resp.err.err.Error()
}
e.respond(w, lvl, code, msg)
}
// HandleAzureDevopsPullRequestEvent will delete any locks associated with the pull
@@ -521,7 +670,18 @@ func (e *VCSEventsController) HandleAzureDevopsPullRequestEvent(w http.ResponseW
return
}
e.Logger.Info("identified event as type %q", pullEventType.String())
e.handlePullRequestEvent(w, baseRepo, headRepo, pull, user, pullEventType)
resp := e.handlePullRequestEvent(e.Logger, baseRepo, headRepo, pull, user, pullEventType)
//TODO: move this to the outer most function similar to github
lvl := logging.Debug
code := http.StatusOK
msg := resp.body
if resp.err.code != 0 {
lvl = logging.Error
code = resp.err.code
msg = resp.err.err.Error()
}
e.respond(w, lvl, code, msg)
}
// supportsHost returns true if h is in e.SupportedVCSHosts and false otherwise.

View File

@@ -36,6 +36,7 @@ import (
"github.com/runatlantis/atlantis/server/events/webhooks"
jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
. "github.com/runatlantis/atlantis/testing"
)
@@ -916,6 +917,7 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl
WorkingDir: workingDir,
PostWorkflowHookRunner: mockPostWorkflowHookRunner,
}
statsScope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
projectCommandBuilder := events.NewProjectCommandBuilder(
userConfig.EnablePolicyChecksFlag,
@@ -930,6 +932,8 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
statsScope,
logger,
)
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFVersion)
@@ -1079,6 +1083,7 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl
GitlabMergeRequestGetter: e2eGitlabGetter,
Logger: logger,
GlobalCfg: globalCfg,
StatsScope: statsScope,
AllowForkPRs: allowForkPRs,
AllowForkPRsFlag: "allow-fork-prs",
CommentCommandRunnerByCmd: commentCommandRunnerByCmd,
@@ -1103,6 +1108,7 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl
LogStreamResourceCleaner: projectCmdOutputHandler,
},
Logger: logger,
Scope: statsScope,
Parser: eventParser,
CommentParser: commentParser,
GithubWebhookSecret: nil,

View File

@@ -17,15 +17,6 @@ import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
. "github.com/petergtz/pegomock"
events_controllers "github.com/runatlantis/atlantis/server/controllers/events"
"github.com/runatlantis/atlantis/server/controllers/events/mocks"
@@ -35,8 +26,17 @@ import (
"github.com/runatlantis/atlantis/server/events/models"
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
. "github.com/runatlantis/atlantis/testing"
gitlab "github.com/xanzy/go-gitlab"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
const githubHeader = "X-Github-Event"
@@ -193,8 +193,11 @@ func TestPost_GitlabCommentNotAllowlisted(t *testing.T) {
t.Log("when the event is a gitlab comment from a repo that isn't allowlisted we comment with an error")
RegisterMockTestingT(t)
vcsClient := vcsmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
Logger: logging.NewNoopLogger(t),
Logger: logger,
Scope: scope,
CommentParser: &events.CommentParser{},
GitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{},
Parser: &events.EventParser{},
@@ -221,8 +224,11 @@ func TestPost_GitlabCommentNotAllowlistedWithSilenceErrors(t *testing.T) {
t.Log("when the event is a gitlab comment from a repo that isn't allowlisted and we are silencing errors, do not comment with an error")
RegisterMockTestingT(t)
vcsClient := vcsmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
Logger: logging.NewNoopLogger(t),
Logger: logger,
Scope: scope,
CommentParser: &events.CommentParser{},
GitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{},
Parser: &events.EventParser{},
@@ -250,8 +256,11 @@ func TestPost_GithubCommentNotAllowlisted(t *testing.T) {
t.Log("when the event is a github comment from a repo that isn't allowlisted we comment with an error")
RegisterMockTestingT(t)
vcsClient := vcsmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
Logger: logging.NewNoopLogger(t),
Logger: logger,
Scope: scope,
GithubRequestValidator: &events_controllers.DefaultGithubRequestValidator{},
CommentParser: &events.CommentParser{},
Parser: &events.EventParser{},
@@ -279,8 +288,11 @@ func TestPost_GithubCommentNotAllowlistedWithSilenceErrors(t *testing.T) {
t.Log("when the event is a github comment from a repo that isn't allowlisted and we are silencing errors, do not comment with an error")
RegisterMockTestingT(t)
vcsClient := vcsmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
Logger: logging.NewNoopLogger(t),
Logger: logger,
Scope: scope,
GithubRequestValidator: &events_controllers.DefaultGithubRequestValidator{},
CommentParser: &events.CommentParser{},
Parser: &events.EventParser{},
@@ -408,7 +420,7 @@ func TestPost_GithubPullRequestNotAllowlisted(t *testing.T) {
When(v.Validate(req, secret)).ThenReturn([]byte(event), nil)
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusForbidden, "Ignoring pull request event from non-allowlisted repo")
ResponseContains(t, w, http.StatusForbidden, "Pull request event from non-allowlisted repo")
}
func TestPost_GitlabMergeRequestNotAllowlisted(t *testing.T) {
@@ -427,7 +439,7 @@ func TestPost_GitlabMergeRequestNotAllowlisted(t *testing.T) {
w := httptest.NewRecorder()
e.Post(w, req)
ResponseContains(t, w, http.StatusForbidden, "Ignoring pull request event from non-allowlisted repo")
ResponseContains(t, w, http.StatusForbidden, "Pull request event from non-allowlisted repo")
}
func TestPost_GithubPullRequestUnsupportedAction(t *testing.T) {
@@ -476,9 +488,12 @@ func TestPost_AzureDevopsPullRequestIgnoreEvent(t *testing.T) {
vcsmock := vcsmocks.NewMockClient()
repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*")
Ok(t, err)
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
TestingMode: true,
Logger: logging.NewNoopLogger(t),
Logger: logger,
Scope: scope,
ApplyDisabled: false,
AzureDevopsWebhookBasicUser: user,
AzureDevopsWebhookBasicPassword: secret,
@@ -632,6 +647,8 @@ func TestPost_BBServerPullClosed(t *testing.T) {
pullCleaner := emocks.NewMockPullCleaner()
allowlist, err := events.NewRepoAllowlistChecker("*")
Ok(t, err)
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
ec := &events_controllers.VCSEventsController{
PullCleaner: pullCleaner,
Parser: &events.EventParser{
@@ -642,7 +659,8 @@ func TestPost_BBServerPullClosed(t *testing.T) {
RepoAllowlistChecker: allowlist,
SupportedVCSHosts: []models.VCSHostType{models.BitbucketServer},
VCSClient: nil,
Logger: logging.NewNoopLogger(t),
Logger: logger,
Scope: scope,
}
// Build HTTP request.
@@ -760,9 +778,12 @@ func setup(t *testing.T) (events_controllers.VCSEventsController, *mocks.MockGit
vcsmock := vcsmocks.NewMockClient()
repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*")
Ok(t, err)
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "null")
e := events_controllers.VCSEventsController{
TestingMode: true,
Logger: logging.NewNoopLogger(t),
Logger: logger,
Scope: scope,
GithubRequestValidator: v,
Parser: p,
CommentParser: cp,

View File

@@ -10,6 +10,8 @@ import (
"github.com/runatlantis/atlantis/server/controllers/websocket"
"github.com/runatlantis/atlantis/server/core/db"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
"github.com/uber-go/tally"
)
type JobIDKeyGenerator struct{}
@@ -32,14 +34,15 @@ type JobsController struct {
Db *db.BoltDB
WsMux *websocket.Multiplexor
KeyGenerator JobIDKeyGenerator
StatsScope tally.Scope
}
func (j *JobsController) GetProjectJobs(w http.ResponseWriter, r *http.Request) {
func (j *JobsController) getProjectJobs(w http.ResponseWriter, r *http.Request) error {
jobID, err := j.KeyGenerator.Generate(r)
if err != nil {
j.respond(w, logging.Error, http.StatusBadRequest, err.Error())
return
return err
}
viewData := templates.ProjectJobData{
@@ -48,17 +51,39 @@ func (j *JobsController) GetProjectJobs(w http.ResponseWriter, r *http.Request)
CleanedBasePath: j.AtlantisURL.Path,
}
if err = j.ProjectJobsTemplate.Execute(w, viewData); err != nil {
return j.ProjectJobsTemplate.Execute(w, viewData)
}
func (j *JobsController) GetProjectJobs(w http.ResponseWriter, r *http.Request) {
errorCounter := j.StatsScope.SubScope("getprojectjobs").Counter(metrics.ExecutionErrorMetric)
err := j.getProjectJobs(w, r)
if err != nil {
j.Logger.Err(err.Error())
errorCounter.Inc(1)
}
}
func (j *JobsController) GetProjectJobsWS(w http.ResponseWriter, r *http.Request) {
func (j *JobsController) getProjectJobsWS(w http.ResponseWriter, r *http.Request) error {
err := j.WsMux.Handle(w, r)
if err != nil {
j.respond(w, logging.Error, http.StatusBadRequest, err.Error())
return
j.respond(w, logging.Error, http.StatusInternalServerError, err.Error())
return err
}
return nil
}
func (j *JobsController) GetProjectJobsWS(w http.ResponseWriter, r *http.Request) {
jobsMetric := j.StatsScope.SubScope("getprojectjobs")
errorCounter := jobsMetric.Counter(metrics.ExecutionErrorMetric)
executionTime := jobsMetric.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
err := j.getProjectJobsWS(w, r)
if err != nil {
errorCounter.Inc(1)
}
}

View File

@@ -15,6 +15,7 @@ type GlobalCfg struct {
Repos []Repo `yaml:"repos" json:"repos"`
Workflows map[string]Workflow `yaml:"workflows" json:"workflows"`
PolicySets PolicySets `yaml:"policies" json:"policies"`
Metrics Metrics `yaml:"metrics" json:"metrics"`
}
// Repo is the raw schema for repos in the server-side repo config.
@@ -34,7 +35,9 @@ type Repo struct {
func (g GlobalCfg) Validate() error {
err := validation.ValidateStruct(&g,
validation.Field(&g.Repos),
validation.Field(&g.Workflows))
validation.Field(&g.Workflows),
validation.Field(&g.Metrics),
)
if err != nil {
return err
}
@@ -129,6 +132,7 @@ func (g GlobalCfg) ToValid(defaultCfg valid.GlobalCfg) valid.GlobalCfg {
Repos: repos,
Workflows: workflows,
PolicySets: g.PolicySets.ToValid(),
Metrics: g.Metrics.ToValid(),
}
}

View File

@@ -0,0 +1,44 @@
package raw
import (
validation "github.com/go-ozzo/ozzo-validation"
"github.com/go-ozzo/ozzo-validation/is"
"github.com/runatlantis/atlantis/server/core/config/valid"
)
type Metrics struct {
Statsd *Statsd `yaml:"statsd" json:"statsd"`
}
type Statsd struct {
Port string `yaml:"port" json:"port"`
Host string `yaml:"host" json:"host"`
}
func (s *Statsd) Validate() error {
return validation.ValidateStruct(s,
validation.Field(&s.Host, validation.Required),
validation.Field(&s.Port, validation.Required),
validation.Field(&s.Host, is.IP),
validation.Field(&s.Port, is.Int))
}
func (m Metrics) Validate() error {
return validation.ValidateStruct(&m,
validation.Field(&m.Statsd),
)
}
func (m Metrics) ToValid() valid.Metrics {
// we've already validated at this point
if m.Statsd != nil {
return valid.Metrics{
Statsd: &valid.Statsd{
Host: m.Statsd.Host,
Port: m.Statsd.Port,
},
}
}
return valid.Metrics{}
}

View File

@@ -0,0 +1,116 @@
package raw_test
import (
"encoding/json"
"testing"
"github.com/runatlantis/atlantis/server/core/config/raw"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)
func TestMetrics_Unmarshal(t *testing.T) {
t.Run("yaml", func(t *testing.T) {
rawYaml := `
statsd:
host: 127.0.0.1
port: 8125
`
var result raw.Metrics
err := yaml.UnmarshalStrict([]byte(rawYaml), &result)
assert.NoError(t, err)
})
t.Run("json", func(t *testing.T) {
rawJSON := `
{
"statsd": {
"host": "127.0.0.1",
"port": "8125"
}
}
`
var result raw.Metrics
err := json.Unmarshal([]byte(rawJSON), &result)
assert.NoError(t, err)
})
}
func TestMetrics_Validate_Success(t *testing.T) {
cases := []struct {
description string
subject raw.Metrics
}{
{
description: "success",
subject: raw.Metrics{
Statsd: &raw.Statsd{
Host: "127.0.0.1",
Port: "8125",
},
},
},
{
description: "missing stats",
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
assert.NoError(t, c.subject.Validate())
})
}
}
func TestMetrics_Validate_Error(t *testing.T) {
cases := []struct {
description string
subject raw.Metrics
}{
{
description: "missing host",
subject: raw.Metrics{
Statsd: &raw.Statsd{
Port: "8125",
},
},
},
{
description: "missing port",
subject: raw.Metrics{
Statsd: &raw.Statsd{
Host: "127.0.0.1",
},
},
},
{
description: "invalid port",
subject: raw.Metrics{
Statsd: &raw.Statsd{
Host: "127.0.0.1",
Port: "string",
},
},
},
{
description: "invalid host",
subject: raw.Metrics{
Statsd: &raw.Statsd{
Host: "127.0.1",
Port: "8125",
},
},
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
assert.Error(t, c.subject.Validate())
})
}
}

View File

@@ -35,6 +35,16 @@ type GlobalCfg struct {
Repos []Repo
Workflows map[string]Workflow
PolicySets PolicySets
Metrics Metrics
}
type Metrics struct {
Statsd *Statsd
}
type Statsd struct {
Port string
Host string
}
// Repo is the final parsed version of server-side repo config.

View File

@@ -11,6 +11,7 @@ import (
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/models/fixtures"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
)
func TestApplyCommandRunner_IsLocked(t *testing.T) {
@@ -46,6 +47,8 @@ func TestApplyCommandRunner_IsLocked(t *testing.T) {
t.Run(c.Description, func(t *testing.T) {
vcsClient := setup(t)
scopeNull, _, _ := metrics.NewLoggingScope(logger, "atlantis")
pull := &github.PullRequest{
State: github.String("open"),
}
@@ -56,6 +59,7 @@ func TestApplyCommandRunner_IsLocked(t *testing.T) {
ctx := &events.CommandContext{
User: fixtures.User,
Log: logging.NewNoopLogger(t),
Scope: scopeNull,
Pull: modelPull,
HeadRepo: fixtures.GithubRepo,
Trigger: events.Comment,

View File

@@ -15,6 +15,7 @@ package events
import (
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
"github.com/uber-go/tally"
)
// CommandTrigger represents the how the command was triggered
@@ -37,6 +38,7 @@ type CommandContext struct {
// See https://help.github.com/articles/about-pull-request-merges/.
HeadRepo models.Repo
Pull models.PullRequest
Scope tally.Scope
// User is the user that triggered this command.
User models.User
Log logging.SimpleLogging

View File

@@ -24,7 +24,9 @@ import (
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
"github.com/runatlantis/atlantis/server/recovery"
"github.com/uber-go/tally"
gitlab "github.com/xanzy/go-gitlab"
)
@@ -97,6 +99,7 @@ type DefaultCommandRunner struct {
EventParser EventParsing
Logger logging.SimpleLogging
GlobalCfg valid.GlobalCfg
StatsScope tally.Scope
// AllowForkPRs controls whether we operate on pull requests from forks.
AllowForkPRs bool
// ParallelPoolSize controls the size of the wait group used to run
@@ -138,9 +141,14 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo
log.Err("Unable to fetch pull status, this is likely a bug.", err)
}
scope := c.StatsScope.SubScope("autoplan")
timer := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer timer.Stop()
ctx := &CommandContext{
User: user,
Log: log,
Scope: scope,
Pull: pull,
HeadRepo: headRepo,
PullStatus: status,
@@ -213,6 +221,14 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
log := c.buildLogger(baseRepo.FullName, pullNum)
defer c.logPanics(baseRepo, pullNum, log)
scope := c.StatsScope.SubScope("comment")
if cmd != nil {
scope = scope.SubScope(cmd.Name.String())
}
timer := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer timer.Stop()
// Check if the user who commented has the permissions to execute the 'plan' or 'apply' commands
ok, err := c.checkUserPermissions(baseRepo, user, cmd)
if err != nil {
@@ -242,6 +258,7 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
PullStatus: status,
HeadRepo: headRepo,
Trigger: Comment,
Scope: scope,
}
if !c.validateCtxAndComment(ctx) {

View File

@@ -24,6 +24,7 @@ import (
"github.com/runatlantis/atlantis/server/core/db"
"github.com/runatlantis/atlantis/server/events/vcs"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
"github.com/google/go-github/v31/github"
. "github.com/petergtz/pegomock"
@@ -192,6 +193,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
When(postWorkflowHooksCommandRunner.RunPostHooks(matchers.AnyPtrToEventsCommandContext())).ThenReturn(nil)
globalCfg := valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{})
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
ch = events.DefaultCommandRunner{
VCSClient: vcsClient,
@@ -201,6 +203,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
GitlabMergeRequestGetter: gitlabGetter,
AzureDevopsPullGetter: azuredevopsGetter,
Logger: logger,
StatsScope: scope,
GlobalCfg: globalCfg,
AllowForkPRs: false,
AllowForkPRsFlag: "allow-fork-prs-flag",

View File

@@ -0,0 +1,76 @@
package events
import (
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
)
type InstrumentedProjectCommandBuilder struct {
ProjectCommandBuilder
Logger logging.SimpleLogging
}
func (b *InstrumentedProjectCommandBuilder) BuildApplyCommands(ctx *CommandContext, comment *CommentCommand) ([]models.ProjectCommandContext, error) {
scope := ctx.Scope.SubScope("builder")
timer := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer timer.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
projectCmds, err := b.ProjectCommandBuilder.BuildApplyCommands(ctx, comment)
if err != nil {
executionError.Inc(1)
b.Logger.Err("Error building apply commands: %s", err)
} else {
executionSuccess.Inc(1)
}
return projectCmds, err
}
func (b *InstrumentedProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) {
scope := ctx.Scope.SubScope("builder")
timer := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer timer.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
projectCmds, err := b.ProjectCommandBuilder.BuildAutoplanCommands(ctx)
if err != nil {
executionError.Inc(1)
b.Logger.Err("Error building auto plan commands: %s", err)
} else {
executionSuccess.Inc(1)
}
return projectCmds, err
}
func (b *InstrumentedProjectCommandBuilder) BuildPlanCommands(ctx *CommandContext, comment *CommentCommand) ([]models.ProjectCommandContext, error) {
scope := ctx.Scope.SubScope("builder")
timer := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer timer.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
projectCmds, err := b.ProjectCommandBuilder.BuildPlanCommands(ctx, comment)
if err != nil {
executionError.Inc(1)
b.Logger.Err("Error building plan commands: %s", err)
} else {
executionSuccess.Inc(1)
}
return projectCmds, err
}

View File

@@ -0,0 +1,58 @@
package events
import (
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/metrics"
)
type InstrumentedProjectCommandRunner struct {
ProjectCommandRunner
}
func (p *InstrumentedProjectCommandRunner) Plan(ctx models.ProjectCommandContext) models.ProjectResult {
return RunAndEmitStats("plan", ctx, p.ProjectCommandRunner.Plan)
}
func (p *InstrumentedProjectCommandRunner) PolicyCheck(ctx models.ProjectCommandContext) models.ProjectResult {
return RunAndEmitStats("policy check", ctx, p.ProjectCommandRunner.PolicyCheck)
}
func (p *InstrumentedProjectCommandRunner) Apply(ctx models.ProjectCommandContext) models.ProjectResult {
return RunAndEmitStats("apply", ctx, p.ProjectCommandRunner.Apply)
}
func RunAndEmitStats(commandName string, ctx models.ProjectCommandContext, execute func(ctx models.ProjectCommandContext) models.ProjectResult) models.ProjectResult {
// ensures we are differentiating between project level command and overall command
ctx.SetScope("project")
scope := ctx.Scope
logger := ctx.Log
executionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
executionFailure := scope.Counter(metrics.ExecutionFailureMetric)
result := execute(ctx)
if result.Error != nil {
executionError.Inc(1)
logger.Err("Error running %s operation: %s", commandName, result.Error.Error())
return result
}
if result.Failure != "" {
executionFailure.Inc(1)
logger.Err("Failure running %s operation: %s", commandName, result.Failure)
return result
}
logger.Info("%s success. output available at: %s", commandName, ctx.Pull.URL)
executionSuccess.Inc(1)
return result
}

View File

@@ -0,0 +1,53 @@
package events
import (
"strconv"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
"github.com/uber-go/tally"
)
type InstrumentedPullClosedExecutor struct {
scope tally.Scope
log logging.SimpleLogging
cleaner PullCleaner
}
func NewInstrumentedPullClosedExecutor(
scope tally.Scope, log logging.SimpleLogging, cleaner PullCleaner,
) PullCleaner {
return &InstrumentedPullClosedExecutor{
scope: scope.SubScope("pullclosed.cleanup"),
log: log,
cleaner: cleaner,
}
}
func (e *InstrumentedPullClosedExecutor) CleanUpPull(repo models.Repo, pull models.PullRequest) error {
log := e.log.With(
"repository", repo.FullName,
"pull-num", strconv.Itoa(pull.Num),
)
executionSuccess := e.scope.Counter(metrics.ExecutionSuccessMetric)
executionError := e.scope.Counter(metrics.ExecutionErrorMetric)
executionTime := e.scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
log.Info("Initiating cleanup of pull data.")
err := e.cleaner.CleanUpPull(repo, pull)
if err != nil {
executionError.Inc(1)
log.Err("error during cleanup of pull data", err)
return err
}
executionSuccess.Inc(1)
return nil
}

View File

@@ -26,6 +26,7 @@ import (
"github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/logging"
"github.com/uber-go/tally"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/config/valid"
@@ -375,6 +376,8 @@ type ProjectCommandContext struct {
HeadRepo Repo
// Log is a logger that's been set up for this context.
Log logging.SimpleLogging
// Scope is the scope for reporting stats setup for this context
Scope tally.Scope
// PullReqStatus holds state about the PR that requires additional computation outside models.PullRequest
PullReqStatus PullReqStatus
// CurrentProjectPlanStatus is the status of the current project prior to this command.
@@ -415,6 +418,12 @@ type ProjectCommandContext struct {
JobID string
}
// SetScope sets the scope of the stats object field. Note: we deliberately set this on the value
// instead of a pointer since we want scopes to mirror our function stack
func (p ProjectCommandContext) SetScope(scope string) {
p.Scope = p.Scope.SubScope(scope) //nolint
}
// GetShowResultFileName returns the filename (not the path) to store the tf show result
func (p ProjectCommandContext) GetShowResultFileName() string {
if p.ProjectName == "" {

View File

@@ -5,6 +5,8 @@ import (
"os"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/logging"
"github.com/uber-go/tally"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/config"
@@ -29,6 +31,43 @@ const (
DefaultDeleteSourceBranchOnMerge = false
)
func NewInstrumentedProjectCommandBuilder(
policyChecksSupported bool,
parserValidator *config.ParserValidator,
projectFinder ProjectFinder,
vcsClient vcs.Client,
workingDir WorkingDir,
workingDirLocker WorkingDirLocker,
globalCfg valid.GlobalCfg,
pendingPlanFinder *DefaultPendingPlanFinder,
commentBuilder CommentBuilder,
skipCloneNoChanges bool,
EnableRegExpCmd bool,
AutoplanFileList string,
scope tally.Scope,
logger logging.SimpleLogging,
) *InstrumentedProjectCommandBuilder {
return &InstrumentedProjectCommandBuilder{
ProjectCommandBuilder: NewProjectCommandBuilder(
policyChecksSupported,
parserValidator,
projectFinder,
vcsClient,
workingDir,
workingDirLocker,
globalCfg,
pendingPlanFinder,
commentBuilder,
skipCloneNoChanges,
EnableRegExpCmd,
AutoplanFileList,
scope,
logger,
),
Logger: logger,
}
}
func NewProjectCommandBuilder(
policyChecksSupported bool,
parserValidator *config.ParserValidator,
@@ -42,8 +81,10 @@ func NewProjectCommandBuilder(
skipCloneNoChanges bool,
EnableRegExpCmd bool,
AutoplanFileList string,
scope tally.Scope,
logger logging.SimpleLogging,
) *DefaultProjectCommandBuilder {
projectCommandBuilder := &DefaultProjectCommandBuilder{
return &DefaultProjectCommandBuilder{
ParserValidator: parserValidator,
ProjectFinder: projectFinder,
VCSClient: vcsClient,
@@ -57,10 +98,9 @@ func NewProjectCommandBuilder(
ProjectCommandContextBuilder: NewProjectCommandContextBuilder(
policyChecksSupported,
commentBuilder,
scope,
),
}
return projectCommandBuilder
}
type ProjectPlanCommandBuilder interface {

View File

@@ -14,12 +14,14 @@ import (
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
"github.com/runatlantis/atlantis/server/logging"
logging_matchers "github.com/runatlantis/atlantis/server/logging/mocks/matchers"
"github.com/runatlantis/atlantis/server/metrics"
. "github.com/runatlantis/atlantis/testing"
)
// Test different permutations of global and repo config.
func TestBuildProjectCmdCtx(t *testing.T) {
logger := logging.NewNoopLogger(t)
statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis")
emptyPolicySets := valid.PolicySets{
Version: nil,
PolicySets: []valid.PolicySet{},
@@ -66,6 +68,7 @@ workflows:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logger,
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -118,6 +121,7 @@ projects:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logger,
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -172,6 +176,7 @@ projects:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logger,
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -234,6 +239,7 @@ projects:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logger,
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -383,6 +389,7 @@ workflows:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logger,
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -441,6 +448,7 @@ projects:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logger,
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -502,6 +510,7 @@ workflows:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logger,
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -547,6 +556,7 @@ projects:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logger,
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -615,13 +625,16 @@ projects:
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
statsScope,
logger,
)
// We run a test for each type of command.
for _, cmd := range []models.CommandName{models.PlanCommand, models.ApplyCommand} {
t.Run(cmd.String(), func(t *testing.T) {
ctxs, err := builder.buildProjectCommandCtx(&CommandContext{
Log: logger,
Log: logger,
Scope: statsScope,
Pull: models.PullRequest{
BaseRepo: baseRepo,
},
@@ -674,6 +687,7 @@ projects:
}
func TestBuildProjectCmdCtx_WithRegExpCmdEnabled(t *testing.T) {
statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis")
emptyPolicySets := valid.PolicySets{
Version: nil,
PolicySets: []valid.PolicySet{},
@@ -746,6 +760,7 @@ projects:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logging.NewNoopLogger(t),
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -796,6 +811,9 @@ projects:
Ok(t, os.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600))
}
logger := logging.NewNoopLogger(t)
statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis")
builder := NewProjectCommandBuilder(
false,
parser,
@@ -809,6 +827,8 @@ projects:
false,
true,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
statsScope,
logger,
)
// We run a test for each type of command, again specific projects
@@ -818,7 +838,8 @@ projects:
Pull: models.PullRequest{
BaseRepo: baseRepo,
},
Log: logging.NewNoopLogger(t),
Log: logging.NewNoopLogger(t),
Scope: statsScope,
PullRequestStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -870,6 +891,7 @@ projects:
func TestBuildProjectCmdCtx_WithPolicCheckEnabled(t *testing.T) {
logger := logging.NewNoopLogger(t)
statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis")
emptyPolicySets := valid.PolicySets{
Version: nil,
PolicySets: []valid.PolicySet{},
@@ -906,6 +928,7 @@ repos:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logger,
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -963,6 +986,7 @@ workflows:
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: logger,
Scope: statsScope,
PullReqStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -1018,6 +1042,7 @@ workflows:
if c.repoCfg != "" {
Ok(t, os.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600))
}
statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis")
builder := NewProjectCommandBuilder(
true,
@@ -1032,12 +1057,15 @@ workflows:
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
statsScope,
logger,
)
cmd := models.PolicyCheckCommand
t.Run(cmd.String(), func(t *testing.T) {
ctxs, err := builder.buildProjectCommandCtx(&CommandContext{
Log: logger,
Log: logger,
Scope: statsScope,
Pull: models.PullRequest{
BaseRepo: baseRepo,
},

View File

@@ -16,6 +16,7 @@ import (
"github.com/runatlantis/atlantis/server/events/models"
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
. "github.com/runatlantis/atlantis/testing"
)
@@ -118,6 +119,7 @@ projects:
}
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
@@ -156,13 +158,16 @@ projects:
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
scope,
logger,
)
ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{
PullRequestStatus: models.PullReqStatus{
Mergeable: true,
},
Log: logger,
Log: logger,
Scope: scope,
})
Ok(t, err)
Equals(t, len(c.exp), len(ctxs))
@@ -379,6 +384,7 @@ projects:
}
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
for _, c := range cases {
// NOTE: we're testing both plan and apply here.
@@ -420,16 +426,19 @@ projects:
false,
true,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
scope,
logger,
)
var actCtxs []models.ProjectCommandContext
var err error
if cmdName == models.PlanCommand {
actCtxs, err = builder.BuildPlanCommands(&events.CommandContext{
Log: logger,
Log: logger,
Scope: scope,
}, &c.Cmd)
} else {
actCtxs, err = builder.BuildApplyCommands(&events.CommandContext{Log: logger}, &c.Cmd)
actCtxs, err = builder.BuildApplyCommands(&events.CommandContext{Log: logger, Scope: scope}, &c.Cmd)
}
if c.ExpErr != "" {
@@ -535,6 +544,7 @@ projects:
}
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
for name, c := range cases {
t.Run(name, func(t *testing.T) {
RegisterMockTestingT(t)
@@ -571,11 +581,14 @@ projects:
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
scope,
logger,
)
ctxs, err := builder.BuildPlanCommands(
&events.CommandContext{
Log: logger,
Log: logger,
Scope: scope,
},
&events.CommentCommand{
RepoRelDir: "",
@@ -644,6 +657,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {
ApprovedReq: false,
UnDivergedReq: false,
}
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
builder := events.NewProjectCommandBuilder(
false,
@@ -658,11 +672,14 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
scope,
logger,
)
ctxs, err := builder.BuildApplyCommands(
&events.CommandContext{
Log: logger,
Log: logger,
Scope: scope,
},
&events.CommentCommand{
RepoRelDir: "",
@@ -724,6 +741,8 @@ projects:
ApprovedReq: false,
UnDivergedReq: false,
}
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
builder := events.NewProjectCommandBuilder(
false,
@@ -738,13 +757,16 @@ projects:
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
scope,
logger,
)
ctx := &events.CommandContext{
HeadRepo: models.Repo{},
Pull: models.PullRequest{},
User: models.User{},
Log: logging.NewNoopLogger(t),
Log: logger,
Scope: scope,
}
_, err = builder.BuildPlanCommands(ctx, &events.CommentCommand{
RepoRelDir: ".",
@@ -778,6 +800,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {
}
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
for _, c := range cases {
t.Run(strings.Join(c.ExtraArgs, " "), func(t *testing.T) {
@@ -813,12 +836,15 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
scope,
logger,
)
var actCtxs []models.ProjectCommandContext
var err error
actCtxs, err = builder.BuildPlanCommands(&events.CommandContext{
Log: logger,
Log: logger,
Scope: scope,
}, &events.CommentCommand{
RepoRelDir: ".",
Flags: c.ExtraArgs,
@@ -949,6 +975,7 @@ projects:
}
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
@@ -992,11 +1019,14 @@ projects:
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
scope,
logger,
)
actCtxs, err := builder.BuildPlanCommands(
&events.CommandContext{
Log: logger,
Log: logger,
Scope: scope,
},
&events.CommentCommand{
RepoRelDir: "",
@@ -1041,6 +1071,7 @@ projects:
ApprovedReq: false,
UnDivergedReq: false,
}
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
builder := events.NewProjectCommandBuilder(
false,
@@ -1055,6 +1086,8 @@ projects:
true,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
scope,
logger,
)
var actCtxs []models.ProjectCommandContext
@@ -1064,6 +1097,7 @@ projects:
Pull: models.PullRequest{},
User: models.User{},
Log: logger,
Scope: scope,
PullRequestStatus: models.PullReqStatus{
Mergeable: true,
},
@@ -1081,6 +1115,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman
defer cleanup()
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
workingDir := mocks.NewMockWorkingDir()
When(workingDir.Clone(matchers.AnyPtrToLoggingSimpleLogger(), matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmpDir, false, nil)
@@ -1109,13 +1144,16 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
scope,
logger,
)
ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{
PullRequestStatus: models.PullReqStatus{
Mergeable: true,
},
Log: logger,
Log: logger,
Scope: scope,
})
Ok(t, err)
@@ -1166,6 +1204,7 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) {
ThenReturn(tmpDir, nil)
logger := logging.NewNoopLogger(t)
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
globalCfgArgs := valid.GlobalCfgArgs{
AllowRepoCfg: false,
@@ -1187,11 +1226,14 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) {
false,
false,
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
scope,
logger,
)
ctxs, err := builder.BuildVersionCommands(
&events.CommandContext{
Log: logger,
Log: logger,
Scope: scope,
},
&events.CommentCommand{
RepoRelDir: "",

View File

@@ -9,9 +9,10 @@ import (
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/uber-go/tally"
)
func NewProjectCommandContextBuilder(policyCheckEnabled bool, commentBuilder CommentBuilder) ProjectCommandContextBuilder {
func NewProjectCommandContextBuilder(policyCheckEnabled bool, commentBuilder CommentBuilder, scope tally.Scope) ProjectCommandContextBuilder {
projectCommandContextBuilder := &DefaultProjectCommandContextBuilder{
CommentBuilder: commentBuilder,
}
@@ -23,7 +24,10 @@ func NewProjectCommandContextBuilder(policyCheckEnabled bool, commentBuilder Com
}
}
return projectCommandContextBuilder
return &CommandScopedStatsProjectCommandContextBuilder{
ProjectCommandContextBuilder: projectCommandContextBuilder,
ProjectCounter: scope.Counter("projects"),
}
}
type ProjectCommandContextBuilder interface {
@@ -38,6 +42,43 @@ type ProjectCommandContextBuilder interface {
) []models.ProjectCommandContext
}
// CommandScopedStatsProjectCommandContextBuilder ensures that project command context contains a scoped stats
// object relevant to the command it applies to.
type CommandScopedStatsProjectCommandContextBuilder struct {
ProjectCommandContextBuilder
// Conciously making this global since it gets flushed periodically anyways
ProjectCounter tally.Counter
}
// BuildProjectContext builds the context and injects the appropriate command level scope after the fact.
func (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext(
ctx *CommandContext,
cmdName models.CommandName,
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose bool,
) (projectCmds []models.ProjectCommandContext) {
cb.ProjectCounter.Inc(1)
cmds := cb.ProjectCommandContextBuilder.BuildProjectContext(
ctx, cmdName, prjCfg, commentFlags, repoDir, automerge, deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose,
)
projectCmds = []models.ProjectCommandContext{}
for _, cmd := range cmds {
// specifically use the command name in the context instead of the arg
// since we can return multiple commands worth of contexts for a given command name arg
// to effectively pipeline them.
cmd.SetScope(cmd.CommandName.String())
projectCmds = append(projectCmds, cmd)
}
return
}
type DefaultProjectCommandContextBuilder struct {
CommentBuilder CommentBuilder
}
@@ -86,6 +127,7 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(
parallelApply,
parallelPlan,
verbose,
ctx.Scope,
ctx.PullRequestStatus,
)
@@ -147,6 +189,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
parallelApply,
parallelPlan,
verbose,
ctx.Scope,
ctx.PullRequestStatus,
))
}
@@ -170,6 +213,7 @@ func newProjectCommandContext(ctx *CommandContext,
parallelApplyEnabled bool,
parallelPlanEnabled bool,
verbose bool,
scope tally.Scope,
pullStatus models.PullReqStatus,
) models.ProjectCommandContext {
@@ -205,6 +249,7 @@ func newProjectCommandContext(ctx *CommandContext,
Steps: steps,
HeadRepo: ctx.HeadRepo,
Log: ctx.Log,
Scope: scope,
ProjectPlanStatus: projectPlanStatus,
Pull: ctx.Pull,
ProjectName: projCfg.Name,

View File

@@ -0,0 +1,237 @@
package vcs
import (
"fmt"
"strconv"
"github.com/google/go-github/v31/github"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
"github.com/uber-go/tally"
)
// NewInstrumentedGithubClient creates a client proxy responsible for gathering stats and logging
func NewInstrumentedGithubClient(client *GithubClient, statsScope tally.Scope, logger logging.SimpleLogging) IGithubClient {
scope := statsScope.SubScope("github")
instrumentedGHClient := &InstrumentedClient{
Client: client,
StatsScope: scope,
Logger: logger,
}
return &InstrumentedGithubClient{
InstrumentedClient: instrumentedGHClient,
PullRequestGetter: client,
StatsScope: scope,
Logger: logger,
}
}
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_github_pull_request_getter.go GithubPullRequestGetter
type GithubPullRequestGetter interface {
GetPullRequest(repo models.Repo, pullNum int) (*github.PullRequest, error)
}
// IGithubClient exists to bridge the gap between GithubPullRequestGetter and Client interface to allow
// for a single instrumented client
type IGithubClient interface {
Client
GithubPullRequestGetter
}
// InstrumentedGithubClient should delegate to the underlying InstrumentedClient for vcs provider-agnostic
// methods and implement soley any github specific interfaces.
type InstrumentedGithubClient struct {
*InstrumentedClient
PullRequestGetter GithubPullRequestGetter
StatsScope tally.Scope
Logger logging.SimpleLogging
}
func (c *InstrumentedGithubClient) GetPullRequest(repo models.Repo, pullNum int) (*github.PullRequest, error) {
scope := c.StatsScope.SubScope("get_pull_request")
logger := c.Logger.WithHistory([]interface{}{
"repository", fmt.Sprintf("%s/%s", repo.Owner, repo.Name),
"pull-num", strconv.Itoa(pullNum),
}...)
executionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
pull, err := c.PullRequestGetter.GetPullRequest(repo, pullNum)
if err != nil {
executionError.Inc(1)
logger.Err("Unable to get pull number for repo, error: %s", err.Error())
} else {
executionSuccess.Inc(1)
}
return pull, err
}
type InstrumentedClient struct {
Client
StatsScope tally.Scope
Logger logging.SimpleLogging
}
func (c *InstrumentedClient) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([]string, error) {
scope := c.StatsScope.SubScope("get_modified_files")
logger := c.Logger.WithHistory(fmtLogSrc(repo, pull.Num)...)
executionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
files, err := c.Client.GetModifiedFiles(repo, pull)
if err != nil {
executionError.Inc(1)
logger.Err("Unable to get modified files, error: %s", err.Error())
} else {
executionSuccess.Inc(1)
}
return files, err
}
func (c *InstrumentedClient) CreateComment(repo models.Repo, pullNum int, comment string, command string) error {
scope := c.StatsScope.SubScope("create_comment")
logger := c.Logger.WithHistory(fmtLogSrc(repo, pullNum)...)
executionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
if err := c.Client.CreateComment(repo, pullNum, comment, command); err != nil {
executionError.Inc(1)
logger.Err("Unable to create comment for command %s, error: %s", command, err.Error())
return err
}
executionSuccess.Inc(1)
return nil
}
func (c *InstrumentedClient) HidePrevCommandComments(repo models.Repo, pullNum int, command string) error {
scope := c.StatsScope.SubScope("hide_prev_plan_comments")
logger := c.Logger.WithHistory(fmtLogSrc(repo, pullNum)...)
executionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
if err := c.Client.HidePrevCommandComments(repo, pullNum, command); err != nil {
executionError.Inc(1)
logger.Err("Unable to hide previous %s comments, error: %s", command, err.Error())
return err
}
executionSuccess.Inc(1)
return nil
}
func (c *InstrumentedClient) PullIsApproved(repo models.Repo, pull models.PullRequest) (models.ApprovalStatus, error) {
scope := c.StatsScope.SubScope("pull_is_approved")
logger := c.Logger.WithHistory(fmtLogSrc(repo, pull.Num)...)
executionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
approved, err := c.Client.PullIsApproved(repo, pull)
if err != nil {
executionError.Inc(1)
logger.Err("Unable to check pull approval status, error: %s", err.Error())
} else {
executionSuccess.Inc(1)
}
return approved, err
}
func (c *InstrumentedClient) PullIsMergeable(repo models.Repo, pull models.PullRequest) (bool, error) {
scope := c.StatsScope.SubScope("pull_is_mergeable")
logger := c.Logger.WithHistory(fmtLogSrc(repo, pull.Num)...)
executionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
mergeable, err := c.Client.PullIsMergeable(repo, pull)
if err != nil {
executionError.Inc(1)
logger.Err("Unable to check pull mergeable status, error: %s", err.Error())
} else {
executionSuccess.Inc(1)
}
return mergeable, err
}
func (c *InstrumentedClient) UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error {
scope := c.StatsScope.SubScope("update_status")
logger := c.Logger.WithHistory(fmtLogSrc(repo, pull.Num)...)
executionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
if err := c.Client.UpdateStatus(repo, pull, state, src, description, url); err != nil {
executionError.Inc(1)
logger.Err("Unable to update status at url: %s, error: %s", url, err.Error())
return err
}
executionSuccess.Inc(1)
return nil
}
func (c *InstrumentedClient) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error {
scope := c.StatsScope.SubScope("merge_pull")
logger := c.Logger.WithHistory("pull-num", pull.Num)
executionTime := scope.Timer(metrics.ExecutionTimeMetric).Start()
defer executionTime.Stop()
executionSuccess := scope.Counter(metrics.ExecutionSuccessMetric)
executionError := scope.Counter(metrics.ExecutionErrorMetric)
if err := c.Client.MergePull(pull, pullOptions); err != nil {
executionError.Inc(1)
logger.Err("Unable to merge pull, error: %s", err.Error())
}
executionSuccess.Inc(1)
return nil
}
// taken from other parts of the code, would be great to have this in a shared spot
func fmtLogSrc(repo models.Repo, pullNum int) []interface{} {
return []interface{}{
"repository", repo.FullName,
"pull-num", strconv.Itoa(pullNum),
}
}

8
server/metrics/common.go Normal file
View File

@@ -0,0 +1,8 @@
package metrics
const (
ExecutionTimeMetric = "execution_time"
ExecutionSuccessMetric = "execution_success"
ExecutionErrorMetric = "execution_error"
ExecutionFailureMetric = "execution_failure"
)

93
server/metrics/debug.go Normal file
View File

@@ -0,0 +1,93 @@
package metrics
import (
"time"
"github.com/runatlantis/atlantis/server/logging"
"github.com/uber-go/tally"
)
// newLoggingReporter returns a tally reporter that logs to the provided logger at debug level. This is useful for
// local development where the usual sinks are not available.
func newLoggingReporter(logger logging.SimpleLogging) tally.StatsReporter {
return &debugReporter{log: logger}
}
type debugReporter struct {
log logging.SimpleLogging
}
// Capabilities interface.
func (r *debugReporter) Reporting() bool {
return true
}
func (r *debugReporter) Tagging() bool {
return true
}
func (r *debugReporter) Capabilities() tally.Capabilities {
return r
}
// Reporter interface.
func (r *debugReporter) Flush() {
// Silence.
}
func (r *debugReporter) ReportCounter(name string, tags map[string]string, value int64) {
log := r.log.With("name", name, "value", value, "tags", tags, "type", "counter")
log.Debug("counter")
}
func (r *debugReporter) ReportGauge(name string, tags map[string]string, value float64) {
log := r.log.With("name", name, "value", value, "tags", tags, "type", "gauge")
log.Debug("gauge")
}
func (r *debugReporter) ReportTimer(name string, tags map[string]string, interval time.Duration) {
log := r.log.With("name", name, "value", interval, "tags", tags, "type", "timer")
log.Debug("timer")
}
func (r *debugReporter) ReportHistogramValueSamples(
name string,
tags map[string]string,
buckets tally.Buckets,
bucketLowerBound,
bucketUpperBound float64,
samples int64,
) {
log := r.log.With(
"name", name,
"buckets", buckets.AsValues(),
"bucketLowerBound", bucketLowerBound,
"bucketUpperBound", bucketUpperBound,
"samples", samples,
"tags", tags,
"type", "valueHistogram",
)
log.Debug("histogram")
}
func (r *debugReporter) ReportHistogramDurationSamples(
name string,
tags map[string]string,
buckets tally.Buckets,
bucketLowerBound,
bucketUpperBound time.Duration,
samples int64,
) {
log := r.log.With(
"name", name,
"buckets", buckets.AsValues(),
"bucketLowerBound", bucketLowerBound,
"bucketUpperBound", bucketUpperBound,
"samples", samples,
"tags", tags,
"type", "durationHistogram",
)
log.Debug("histogram")
}

63
server/metrics/scope.go Normal file
View File

@@ -0,0 +1,63 @@
package metrics
import (
"io"
"strings"
"time"
"github.com/cactus/go-statsd-client/statsd"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/logging"
"github.com/uber-go/tally"
tallystatsd "github.com/uber-go/tally/statsd"
)
func NewLoggingScope(logger logging.SimpleLogging, statsNamespace string) (tally.Scope, io.Closer, error) {
reporter, err := newReporter(valid.Metrics{}, logger)
if err != nil {
return nil, nil, errors.Wrap(err, "initializing stats reporter")
}
scope, closer := tally.NewRootScope(tally.ScopeOptions{
Prefix: statsNamespace,
Reporter: reporter,
}, time.Second)
return scope, closer, nil
}
func NewScope(cfg valid.Metrics, logger logging.SimpleLogging, statsNamespace string) (tally.Scope, io.Closer, error) {
reporter, err := newReporter(cfg, logger)
if err != nil {
return nil, nil, errors.Wrap(err, "initializing stats reporter")
}
scope, closer := tally.NewRootScope(tally.ScopeOptions{
Prefix: statsNamespace,
Reporter: reporter,
}, time.Second)
return scope, closer, nil
}
func newReporter(cfg valid.Metrics, logger logging.SimpleLogging) (tally.StatsReporter, error) {
if cfg.Statsd == nil {
// return logging reporter and proceed
return newLoggingReporter(logger), nil
}
statsdCfg := cfg.Statsd
client, err := statsd.NewClientWithConfig(&statsd.ClientConfig{
Address: strings.Join([]string{statsdCfg.Host, statsdCfg.Port}, ":"),
})
if err != nil {
return nil, errors.Wrap(err, "initializing statsd client")
}
return tallystatsd.NewReporter(client, tallystatsd.Options{}), nil
}

View File

@@ -0,0 +1,100 @@
package scheduled
import (
"context"
"github.com/runatlantis/atlantis/server/logging"
"github.com/uber-go/tally"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
type ExecutorService struct {
log logging.SimpleLogging
// jobs
runtimeStatsPublisher JobDefinition
}
func NewExecutorService(
statsScope tally.Scope,
log logging.SimpleLogging,
) *ExecutorService {
scheduledScope := statsScope.SubScope("scheduled")
runtimeStatsPublisher := NewRuntimeStats(scheduledScope)
runtimeStatsPublisherJob := JobDefinition{
Job: runtimeStatsPublisher,
Period: 10 * time.Second,
}
return &ExecutorService{
log: log,
runtimeStatsPublisher: runtimeStatsPublisherJob,
}
}
type JobDefinition struct {
Job Job
Period time.Duration
}
func (s *ExecutorService) Run() {
s.log.Info("Scheduled Executor Service started")
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
s.runScheduledJob(ctx, &wg, s.runtimeStatsPublisher)
interrupt := make(chan os.Signal, 1)
// Stop on SIGINTs and SIGTERMs.
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
<-interrupt
s.log.Warn("Received interrupt. Attempting to Shut down scheduled executor service")
cancel()
wg.Wait()
s.log.Warn("All jobs completed, exiting.")
}
func (s *ExecutorService) runScheduledJob(ctx context.Context, wg *sync.WaitGroup, jd JobDefinition) {
ticker := time.NewTicker(jd.Period)
wg.Add(1)
go func() {
defer wg.Done()
defer ticker.Stop()
// Ensure we recover from any panics to keep the jobs isolated.
// Keep the recovery outside the select to ensure that we don't infinitely panic.
defer func() {
if r := recover(); r != nil {
s.log.Err("Recovered from panic: %v", r)
}
}()
for {
select {
case <-ctx.Done():
s.log.Warn("Received interrupt, cancelling job")
return
case <-ticker.C:
jd.Job.Run()
}
}
}()
}
type Job interface {
Run()
}

View File

@@ -0,0 +1,128 @@
package scheduled
import (
"runtime"
"github.com/uber-go/tally"
)
type RuntimeStatCollector struct {
runtimeMetrics runtimeMetrics
}
type runtimeMetrics struct {
cpuGoroutines tally.Gauge
cpuCgoCalls tally.Gauge
memoryAlloc tally.Gauge
memoryTotal tally.Gauge
memorySys tally.Gauge
memoryLookups tally.Gauge
memoryMalloc tally.Gauge
memoryFrees tally.Gauge
memoryHeapAlloc tally.Gauge
memoryHeapSys tally.Gauge
memoryHeapIdle tally.Gauge
memoryHeapInuse tally.Gauge
memoryHeapReleased tally.Gauge
memoryHeapObjects tally.Gauge
memoryStackInuse tally.Gauge
memoryStackSys tally.Gauge
memoryStackMSpanInuse tally.Gauge
memoryStackMSpanSys tally.Gauge
memoryStackMCacheInuse tally.Gauge
memoryStackMCacheSys tally.Gauge
memoryOtherSys tally.Gauge
memoryGCSys tally.Gauge
memoryGCNext tally.Gauge
memoryGCLast tally.Gauge
memoryGCPauseTotal tally.Gauge
memoryGCCount tally.Gauge
}
func NewRuntimeStats(scope tally.Scope) *RuntimeStatCollector {
runtimeScope := scope.SubScope("runtime")
runtimeMetrics := runtimeMetrics{
// cpu
cpuGoroutines: runtimeScope.Gauge("cpu.goroutines"),
cpuCgoCalls: runtimeScope.Gauge("cpu.cgo_calls"),
// memory
memoryAlloc: runtimeScope.Gauge("memory.alloc"),
memoryTotal: runtimeScope.Gauge("memory.total"),
memorySys: runtimeScope.Gauge("memory.sys"),
memoryLookups: runtimeScope.Gauge("memory.lookups"),
memoryMalloc: runtimeScope.Gauge("memory.malloc"),
memoryFrees: runtimeScope.Gauge("memory.frees"),
// heap
memoryHeapAlloc: runtimeScope.Gauge("memory.heap.alloc"),
memoryHeapSys: runtimeScope.Gauge("memory.heap.sys"),
memoryHeapIdle: runtimeScope.Gauge("memory.heap.idle"),
memoryHeapInuse: runtimeScope.Gauge("memory.heap.inuse"),
memoryHeapReleased: runtimeScope.Gauge("memory.heap.released"),
memoryHeapObjects: runtimeScope.Gauge("memory.heap.objects"),
// stack
memoryStackInuse: runtimeScope.Gauge("memory.stack.inuse"),
memoryStackSys: runtimeScope.Gauge("memory.stack.sys"),
memoryStackMSpanInuse: runtimeScope.Gauge("memory.stack.mspan_inuse"),
memoryStackMSpanSys: runtimeScope.Gauge("memory.stack.sys"),
memoryStackMCacheInuse: runtimeScope.Gauge("memory.stack.mcache_inuse"),
memoryStackMCacheSys: runtimeScope.Gauge("memory.stack.mcache_sys"),
memoryOtherSys: runtimeScope.Gauge("memory.othersys"),
// GC
memoryGCSys: runtimeScope.Gauge("memory.gc.sys"),
memoryGCNext: runtimeScope.Gauge("memory.gc.next"),
memoryGCLast: runtimeScope.Gauge("memory.gc.last"),
memoryGCPauseTotal: runtimeScope.Gauge("memory.gc.pause_total"),
memoryGCCount: runtimeScope.Gauge("memory.gc.count"),
}
return &RuntimeStatCollector{
runtimeMetrics: runtimeMetrics,
}
}
func (r *RuntimeStatCollector) Run() {
// cpu stats
r.runtimeMetrics.cpuGoroutines.Update(float64(runtime.NumGoroutine()))
r.runtimeMetrics.cpuCgoCalls.Update(float64(runtime.NumCgoCall()))
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
// general
r.runtimeMetrics.memoryAlloc.Update(float64(memStats.Alloc))
r.runtimeMetrics.memoryTotal.Update(float64(memStats.TotalAlloc))
r.runtimeMetrics.memorySys.Update(float64(memStats.Sys))
r.runtimeMetrics.memoryLookups.Update(float64(memStats.Lookups))
r.runtimeMetrics.memoryMalloc.Update(float64(memStats.Mallocs))
r.runtimeMetrics.memoryFrees.Update(float64(memStats.Frees))
// heap
r.runtimeMetrics.memoryHeapAlloc.Update(float64(memStats.HeapAlloc))
r.runtimeMetrics.memoryHeapSys.Update(float64(memStats.HeapSys))
r.runtimeMetrics.memoryHeapIdle.Update(float64(memStats.HeapIdle))
r.runtimeMetrics.memoryHeapInuse.Update(float64(memStats.HeapInuse))
r.runtimeMetrics.memoryHeapReleased.Update(float64(memStats.HeapReleased))
r.runtimeMetrics.memoryHeapObjects.Update(float64(memStats.HeapObjects))
// stack
r.runtimeMetrics.memoryStackInuse.Update(float64(memStats.StackInuse))
r.runtimeMetrics.memoryStackSys.Update(float64(memStats.StackSys))
r.runtimeMetrics.memoryStackMSpanInuse.Update(float64(memStats.MSpanInuse))
r.runtimeMetrics.memoryStackMSpanSys.Update(float64(memStats.MSpanSys))
r.runtimeMetrics.memoryStackMCacheInuse.Update(float64(memStats.MCacheInuse))
r.runtimeMetrics.memoryStackMCacheSys.Update(float64(memStats.MCacheSys))
r.runtimeMetrics.memoryOtherSys.Update(float64(memStats.OtherSys))
// GC
r.runtimeMetrics.memoryGCSys.Update(float64(memStats.GCSys))
r.runtimeMetrics.memoryGCNext.Update(float64(memStats.NextGC))
r.runtimeMetrics.memoryGCLast.Update(float64(memStats.LastGC))
r.runtimeMetrics.memoryGCPauseTotal.Update(float64(memStats.PauseTotalNs))
r.runtimeMetrics.memoryGCCount.Update(float64(memStats.NumGC))
}

View File

@@ -19,6 +19,7 @@ import (
"context"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/url"
@@ -31,9 +32,13 @@ import (
"time"
"github.com/mitchellh/go-homedir"
cfg "github.com/runatlantis/atlantis/server/core/config"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/db"
"github.com/runatlantis/atlantis/server/jobs"
"github.com/runatlantis/atlantis/server/metrics"
"github.com/runatlantis/atlantis/server/scheduled"
"github.com/uber-go/tally"
assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/gorilla/mux"
@@ -42,7 +47,6 @@ import (
events_controllers "github.com/runatlantis/atlantis/server/controllers/events"
"github.com/runatlantis/atlantis/server/controllers/templates"
"github.com/runatlantis/atlantis/server/controllers/websocket"
cfgParser "github.com/runatlantis/atlantis/server/core/config"
"github.com/runatlantis/atlantis/server/core/locking"
"github.com/runatlantis/atlantis/server/core/runtime"
"github.com/runatlantis/atlantis/server/core/runtime/policy"
@@ -88,6 +92,8 @@ type Server struct {
PreWorkflowHooksCommandRunner *events.DefaultPreWorkflowHooksCommandRunner
CommandRunner *events.DefaultCommandRunner
Logger logging.SimpleLogging
StatsScope tally.Scope
StatsCloser io.Closer
Locker locking.Locker
ApplyLocker locking.ApplyLocker
VCSEventsController *events_controllers.VCSEventsController
@@ -106,6 +112,7 @@ type Server struct {
WebUsername string
WebPassword string
ProjectCmdOutputHandler jobs.ProjectCommandOutputHandler
ScheduledExecutorService *scheduled.ExecutorService
}
// Config holds config for server that isn't passed in by the user.
@@ -144,7 +151,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
}
var supportedVCSHosts []models.VCSHostType
var githubClient *vcs.GithubClient
var githubClient vcs.IGithubClient
var githubAppEnabled bool
var githubCredentials vcs.GithubCredentials
var gitlabClient *vcs.GitlabClient
@@ -158,6 +165,34 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
policyChecksEnabled = true
}
validator := &cfg.ParserValidator{}
globalCfg := valid.NewGlobalCfgFromArgs(
valid.GlobalCfgArgs{
AllowRepoCfg: userConfig.AllowRepoConfig,
MergeableReq: userConfig.RequireMergeable,
ApprovedReq: userConfig.RequireApproval,
UnDivergedReq: userConfig.RequireUnDiverged,
PolicyCheckEnabled: userConfig.EnablePolicyChecksFlag,
})
if userConfig.RepoConfig != "" {
globalCfg, err = validator.ParseGlobalCfg(userConfig.RepoConfig, globalCfg)
if err != nil {
return nil, errors.Wrapf(err, "parsing %s file", userConfig.RepoConfig)
}
} else if userConfig.RepoConfigJSON != "" {
globalCfg, err = validator.ParseGlobalCfgJSON(userConfig.RepoConfigJSON, globalCfg)
if err != nil {
return nil, errors.Wrapf(err, "parsing --%s", config.RepoConfigJSONFlag)
}
}
statsScope, closer, err := metrics.NewScope(globalCfg.Metrics, logger, userConfig.StatsNamespace)
if err != nil {
return nil, errors.Wrapf(err, "instantiating metrics scope")
}
if userConfig.GithubUser != "" || userConfig.GithubAppID != 0 {
supportedVCSHosts = append(supportedVCSHosts, models.Github)
if userConfig.GithubUser != "" {
@@ -188,10 +223,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
}
var err error
githubClient, err = vcs.NewGithubClient(userConfig.GithubHostname, githubCredentials, logger)
rawGithubClient, err := vcs.NewGithubClient(userConfig.GithubHostname, githubCredentials, logger)
if err != nil {
return nil, err
}
githubClient = vcs.NewInstrumentedGithubClient(rawGithubClient, statsScope, logger)
}
if userConfig.GitlabUser != "" {
supportedVCSHosts = append(supportedVCSHosts, models.Gitlab)
@@ -392,37 +429,19 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
DB: boltdb,
}
validator := &cfgParser.ParserValidator{}
globalCfg := valid.NewGlobalCfgFromArgs(
valid.GlobalCfgArgs{
AllowRepoCfg: userConfig.AllowRepoConfig,
MergeableReq: userConfig.RequireMergeable,
ApprovedReq: userConfig.RequireApproval,
UnDivergedReq: userConfig.RequireUnDiverged,
PolicyCheckEnabled: userConfig.EnablePolicyChecksFlag,
})
if userConfig.RepoConfig != "" {
globalCfg, err = validator.ParseGlobalCfg(userConfig.RepoConfig, globalCfg)
if err != nil {
return nil, errors.Wrapf(err, "parsing %s file", userConfig.RepoConfig)
}
} else if userConfig.RepoConfigJSON != "" {
globalCfg, err = validator.ParseGlobalCfgJSON(userConfig.RepoConfigJSON, globalCfg)
if err != nil {
return nil, errors.Wrapf(err, "parsing --%s", config.RepoConfigJSONFlag)
}
}
pullClosedExecutor := &events.PullClosedExecutor{
VCSClient: vcsClient,
Locker: lockingClient,
WorkingDir: workingDir,
Logger: logger,
DB: boltdb,
LogStreamResourceCleaner: projectCmdOutputHandler,
PullClosedTemplate: &events.PullClosedEventTemplate{},
}
pullClosedExecutor := events.NewInstrumentedPullClosedExecutor(
statsScope,
logger,
&events.PullClosedExecutor{
Locker: lockingClient,
WorkingDir: workingDir,
Logger: logger,
DB: boltdb,
PullClosedTemplate: &events.PullClosedEventTemplate{},
LogStreamResourceCleaner: projectCmdOutputHandler,
VCSClient: vcsClient,
},
)
eventParser := &events.EventParser{
GithubUser: userConfig.GithubUser,
GithubToken: userConfig.GithubToken,
@@ -468,7 +487,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
WorkingDir: workingDir,
PostWorkflowHookRunner: runtime.DefaultPostWorkflowHookRunner{},
}
projectCommandBuilder := events.NewProjectCommandBuilder(
projectCommandBuilder := events.NewInstrumentedProjectCommandBuilder(
policyChecksEnabled,
validator,
&events.DefaultProjectFinder{},
@@ -481,6 +500,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
userConfig.SkipCloneNoChanges,
userConfig.EnableRegExpCmd,
userConfig.AutoplanFileList,
statsScope,
logger,
)
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfVersion)
@@ -557,12 +578,15 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
ProjectCommandRunner: projectCommandRunner,
JobURLSetter: jobs.NewJobURLSetter(router, commitStatusUpdater),
}
instrumentedProjectCmdRunner := &events.InstrumentedProjectCommandRunner{
ProjectCommandRunner: projectOutputWrapper,
}
policyCheckCommandRunner := events.NewPolicyCheckCommandRunner(
dbUpdater,
pullUpdater,
commitStatusUpdater,
projectOutputWrapper,
instrumentedProjectCmdRunner,
userConfig.ParallelPoolSize,
userConfig.SilenceVCSStatusNoProjects,
)
@@ -575,7 +599,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
workingDir,
commitStatusUpdater,
projectCommandBuilder,
projectOutputWrapper,
instrumentedProjectCmdRunner,
dbUpdater,
pullUpdater,
policyCheckCommandRunner,
@@ -592,7 +616,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
applyLockingClient,
commitStatusUpdater,
projectCommandBuilder,
projectOutputWrapper,
instrumentedProjectCmdRunner,
autoMerger,
pullUpdater,
dbUpdater,
@@ -606,7 +630,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
approvePoliciesCommandRunner := events.NewApprovePoliciesCommandRunner(
commitStatusUpdater,
projectCommandBuilder,
projectOutputWrapper,
instrumentedProjectCmdRunner,
pullUpdater,
dbUpdater,
userConfig.SilenceNoProjects,
@@ -649,6 +673,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
EventParser: eventParser,
Logger: logger,
GlobalCfg: globalCfg,
StatsScope: statsScope.SubScope("cmd"),
AllowForkPRs: userConfig.AllowForkPRs,
AllowForkPRsFlag: config.AllowForkPRsFlag,
SilenceForkPRErrors: userConfig.SilenceForkPRErrors,
@@ -693,6 +718,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
Db: boltdb,
WsMux: wsMux,
KeyGenerator: controllers.JobIDKeyGenerator{},
StatsScope: statsScope.SubScope("api"),
}
eventsController := &events_controllers.VCSEventsController{
@@ -701,6 +727,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
Parser: eventParser,
CommentParser: commentParser,
Logger: logger,
Scope: statsScope,
ApplyDisabled: userConfig.DisableApply,
GithubWebhookSecret: []byte(userConfig.GithubWebhookSecret),
GithubRequestValidator: &events_controllers.DefaultGithubRequestValidator{},
@@ -722,6 +749,11 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
GithubHostname: userConfig.GithubHostname,
GithubOrg: userConfig.GithubOrg,
}
scheduledExecutorService := scheduled.NewExecutorService(
statsScope,
logger,
)
return &Server{
AtlantisVersion: config.AtlantisVersion,
AtlantisURL: parsedURL,
@@ -731,6 +763,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner,
CommandRunner: commandRunner,
Logger: logger,
StatsScope: statsScope,
StatsCloser: closer,
Locker: lockingClient,
ApplyLocker: applyLockingClient,
VCSEventsController: eventsController,
@@ -749,6 +783,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
WebAuthentication: userConfig.WebBasicAuth,
WebUsername: userConfig.WebUsername,
WebPassword: userConfig.WebPassword,
ScheduledExecutorService: scheduledExecutorService,
}, nil
}
@@ -786,6 +821,8 @@ func (s *Server) Start() error {
// Stop on SIGINTs and SIGTERMs.
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
go s.ScheduledExecutorService.Run()
go func() {
s.ProjectCmdOutputHandler.Handle()
}()
@@ -809,6 +846,12 @@ func (s *Server) Start() error {
s.Logger.Warn("Received interrupt. Waiting for in-progress operations to complete")
s.waitForDrain()
// flush stats before shutdown
if err := s.StatsCloser.Close(); err != nil {
s.Logger.Err(err.Error())
}
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) // nolint: vet
if err := server.Shutdown(ctx); err != nil {
return cli.NewExitError(fmt.Sprintf("while shutting down: %s", err), 1)

View File

@@ -49,6 +49,7 @@ type UserConfig struct {
HidePrevPlanComments bool `mapstructure:"hide-prev-plan-comments"`
LogLevel string `mapstructure:"log-level"`
ParallelPoolSize int `mapstructure:"parallel-pool-size"`
StatsNamespace string `mapstructure:"stats-namespace"`
PlanDrafts bool `mapstructure:"allow-draft-prs"`
Port int `mapstructure:"port"`
RepoConfig string `mapstructure:"repo-config"`