You've already forked postgres_exporter
mirror of
https://github.com/prometheus-community/postgres_exporter.git
synced 2025-08-08 04:42:07 +03:00
This commit implements a massive refactor of the repository, and moves the build system over to use Mage (magefile.org) which should allow seamless building across multiple platforms.
291 lines
6.7 KiB
Go
291 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/shlex"
|
|
kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
|
|
)
|
|
|
|
type Vars map[string]string
|
|
|
|
func (v Vars) Copy() Vars {
|
|
out := Vars{}
|
|
for k, v := range v {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (v Vars) Replace(s string) string {
|
|
for k, v := range v {
|
|
prefix := regexp.MustCompile(fmt.Sprintf("{%s=([^}]*)}", k))
|
|
if v != "" {
|
|
s = prefix.ReplaceAllString(s, "$1")
|
|
} else {
|
|
s = prefix.ReplaceAllString(s, "")
|
|
}
|
|
s = strings.Replace(s, fmt.Sprintf("{%s}", k), v, -1)
|
|
}
|
|
return s
|
|
}
|
|
|
|
type linterState struct {
|
|
*Linter
|
|
issues chan *Issue
|
|
vars Vars
|
|
exclude *regexp.Regexp
|
|
include *regexp.Regexp
|
|
deadline <-chan time.Time
|
|
}
|
|
|
|
func (l *linterState) Partitions(paths []string) ([][]string, error) {
|
|
cmdArgs, err := parseCommand(l.command())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parts, err := l.Linter.PartitionStrategy(cmdArgs, paths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parts, nil
|
|
}
|
|
|
|
func (l *linterState) command() string {
|
|
return l.vars.Replace(l.Command)
|
|
}
|
|
|
|
func runLinters(linters map[string]*Linter, paths []string, concurrency int, exclude, include *regexp.Regexp) (chan *Issue, chan error) {
|
|
errch := make(chan error, len(linters))
|
|
concurrencych := make(chan bool, concurrency)
|
|
incomingIssues := make(chan *Issue, 1000000)
|
|
|
|
directiveParser := newDirectiveParser()
|
|
if config.WarnUnmatchedDirective {
|
|
directiveParser.LoadFiles(paths)
|
|
}
|
|
|
|
processedIssues := maybeSortIssues(filterIssuesViaDirectives(
|
|
directiveParser, maybeAggregateIssues(incomingIssues)))
|
|
|
|
vars := Vars{
|
|
"duplthreshold": fmt.Sprintf("%d", config.DuplThreshold),
|
|
"mincyclo": fmt.Sprintf("%d", config.Cyclo),
|
|
"maxlinelength": fmt.Sprintf("%d", config.LineLength),
|
|
"misspelllocale": fmt.Sprintf("%s", config.MisspellLocale),
|
|
"min_confidence": fmt.Sprintf("%f", config.MinConfidence),
|
|
"min_occurrences": fmt.Sprintf("%d", config.MinOccurrences),
|
|
"min_const_length": fmt.Sprintf("%d", config.MinConstLength),
|
|
"tests": "",
|
|
"not_tests": "true",
|
|
}
|
|
if config.Test {
|
|
vars["tests"] = "true"
|
|
vars["not_tests"] = ""
|
|
}
|
|
|
|
wg := &sync.WaitGroup{}
|
|
id := 1
|
|
for _, linter := range linters {
|
|
deadline := time.After(config.Deadline.Duration())
|
|
state := &linterState{
|
|
Linter: linter,
|
|
issues: incomingIssues,
|
|
vars: vars,
|
|
exclude: exclude,
|
|
include: include,
|
|
deadline: deadline,
|
|
}
|
|
|
|
partitions, err := state.Partitions(paths)
|
|
if err != nil {
|
|
errch <- err
|
|
continue
|
|
}
|
|
for _, args := range partitions {
|
|
wg.Add(1)
|
|
concurrencych <- true
|
|
// Call the goroutine with a copy of the args array so that the
|
|
// contents of the array are not modified by the next iteration of
|
|
// the above for loop
|
|
go func(id int, args []string) {
|
|
err := executeLinter(id, state, args)
|
|
if err != nil {
|
|
errch <- err
|
|
}
|
|
<-concurrencych
|
|
wg.Done()
|
|
}(id, args)
|
|
id++
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(incomingIssues)
|
|
close(errch)
|
|
}()
|
|
return processedIssues, errch
|
|
}
|
|
|
|
func executeLinter(id int, state *linterState, args []string) error {
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("missing linter command")
|
|
}
|
|
|
|
start := time.Now()
|
|
dbg := namespacedDebug(fmt.Sprintf("[%s.%d]: ", state.Name, id))
|
|
dbg("executing %s", strings.Join(args, " "))
|
|
buf := bytes.NewBuffer(nil)
|
|
command := args[0]
|
|
cmd := exec.Command(command, args[1:]...) // nolint: gas
|
|
cmd.Stdout = buf
|
|
cmd.Stderr = buf
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute linter %s: %s", command, err)
|
|
}
|
|
|
|
done := make(chan bool)
|
|
go func() {
|
|
err = cmd.Wait()
|
|
done <- true
|
|
}()
|
|
|
|
// Wait for process to complete or deadline to expire.
|
|
select {
|
|
case <-done:
|
|
|
|
case <-state.deadline:
|
|
err = fmt.Errorf("deadline exceeded by linter %s (try increasing --deadline)",
|
|
state.Name)
|
|
kerr := cmd.Process.Kill()
|
|
if kerr != nil {
|
|
warning("failed to kill %s: %s", state.Name, kerr)
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err != nil {
|
|
dbg("warning: %s returned %s: %s", command, err, buf.String())
|
|
}
|
|
|
|
processOutput(dbg, state, buf.Bytes())
|
|
elapsed := time.Since(start)
|
|
dbg("%s linter took %s", state.Name, elapsed)
|
|
return nil
|
|
}
|
|
|
|
func parseCommand(command string) ([]string, error) {
|
|
args, err := shlex.Split(command)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(args) == 0 {
|
|
return nil, fmt.Errorf("invalid command %q", command)
|
|
}
|
|
exe, err := exec.LookPath(args[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append([]string{exe}, args[1:]...), nil
|
|
}
|
|
|
|
// nolint: gocyclo
|
|
func processOutput(dbg debugFunction, state *linterState, out []byte) {
|
|
re := state.regex
|
|
all := re.FindAllSubmatchIndex(out, -1)
|
|
dbg("%s hits %d: %s", state.Name, len(all), state.Pattern)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
warning("failed to get working directory %s", err)
|
|
}
|
|
|
|
// Create a local copy of vars so they can be modified by the linter output
|
|
vars := state.vars.Copy()
|
|
|
|
for _, indices := range all {
|
|
group := [][]byte{}
|
|
for i := 0; i < len(indices); i += 2 {
|
|
var fragment []byte
|
|
if indices[i] != -1 {
|
|
fragment = out[indices[i]:indices[i+1]]
|
|
}
|
|
group = append(group, fragment)
|
|
}
|
|
|
|
issue, err := NewIssue(state.Linter.Name, config.formatTemplate)
|
|
kingpin.FatalIfError(err, "Invalid output format")
|
|
|
|
for i, name := range re.SubexpNames() {
|
|
if group[i] == nil {
|
|
continue
|
|
}
|
|
part := string(group[i])
|
|
if name != "" {
|
|
vars[name] = part
|
|
}
|
|
switch name {
|
|
case "path":
|
|
issue.Path, err = newIssuePathFromAbsPath(cwd, part)
|
|
if err != nil {
|
|
warning("failed to make %s a relative path: %s", part, err)
|
|
}
|
|
case "line":
|
|
n, err := strconv.ParseInt(part, 10, 32)
|
|
kingpin.FatalIfError(err, "line matched invalid integer")
|
|
issue.Line = int(n)
|
|
|
|
case "col":
|
|
n, err := strconv.ParseInt(part, 10, 32)
|
|
kingpin.FatalIfError(err, "col matched invalid integer")
|
|
issue.Col = int(n)
|
|
|
|
case "message":
|
|
issue.Message = part
|
|
|
|
case "":
|
|
}
|
|
}
|
|
// TODO: set messageOveride and severity on the Linter instead of reading
|
|
// them directly from the static config
|
|
if m, ok := config.MessageOverride[state.Name]; ok {
|
|
issue.Message = vars.Replace(m)
|
|
}
|
|
if sev, ok := config.Severity[state.Name]; ok {
|
|
issue.Severity = Severity(sev)
|
|
}
|
|
if state.exclude != nil && state.exclude.MatchString(issue.String()) {
|
|
continue
|
|
}
|
|
if state.include != nil && !state.include.MatchString(issue.String()) {
|
|
continue
|
|
}
|
|
state.issues <- issue
|
|
}
|
|
}
|
|
|
|
func maybeSortIssues(issues chan *Issue) chan *Issue {
|
|
if reflect.DeepEqual([]string{"none"}, config.Sort) {
|
|
return issues
|
|
}
|
|
return SortIssueChan(issues, config.Sort)
|
|
}
|
|
|
|
func maybeAggregateIssues(issues chan *Issue) chan *Issue {
|
|
if !config.Aggregate {
|
|
return issues
|
|
}
|
|
return AggregateIssueChan(issues)
|
|
}
|