You've already forked postgres_exporter
mirror of
https://github.com/prometheus-community/postgres_exporter.git
synced 2025-08-09 15:42:47 +03:00
385 lines
8.4 KiB
Go
385 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/shlex"
|
|
"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
|
|
}
|
|
|
|
// Severity of linter message.
|
|
type Severity string
|
|
|
|
// Linter message severity levels.
|
|
const ( // nolint: deadcode
|
|
Error Severity = "error"
|
|
Warning Severity = "warning"
|
|
)
|
|
|
|
type Issue struct {
|
|
Linter string `json:"linter"`
|
|
Severity Severity `json:"severity"`
|
|
Path string `json:"path"`
|
|
Line int `json:"line"`
|
|
Col int `json:"col"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func (i *Issue) String() string {
|
|
buf := new(bytes.Buffer)
|
|
err := formatTemplate.Execute(buf, i)
|
|
kingpin.FatalIfError(err, "Invalid output format")
|
|
return buf.String()
|
|
}
|
|
|
|
type linterState struct {
|
|
*Linter
|
|
paths []string
|
|
issues chan *Issue
|
|
vars Vars
|
|
exclude *regexp.Regexp
|
|
include *regexp.Regexp
|
|
deadline <-chan time.Time
|
|
}
|
|
|
|
func (l *linterState) Partitions() ([][]string, error) {
|
|
command := l.vars.Replace(l.Command)
|
|
cmdArgs, err := parseCommand(command)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parts, err := l.Linter.PartitionStrategy(cmdArgs, l.paths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parts, nil
|
|
}
|
|
|
|
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)
|
|
processedIssues := filterIssuesViaDirectives(
|
|
newDirectiveParser(),
|
|
maybeSortIssues(maybeAggregateIssues(incomingIssues)))
|
|
|
|
vars := Vars{
|
|
"duplthreshold": fmt.Sprintf("%d", config.DuplThreshold),
|
|
"mincyclo": fmt.Sprintf("%d", config.Cyclo),
|
|
"maxlinelength": fmt.Sprintf("%d", config.LineLength),
|
|
"min_confidence": fmt.Sprintf("%f", config.MinConfidence),
|
|
"min_occurrences": fmt.Sprintf("%d", config.MinOccurrences),
|
|
"min_const_length": fmt.Sprintf("%d", config.MinConstLength),
|
|
"tests": "",
|
|
}
|
|
if config.Test {
|
|
vars["tests"] = "-t"
|
|
}
|
|
|
|
wg := &sync.WaitGroup{}
|
|
for _, linter := range linters {
|
|
deadline := time.After(config.Deadline.Duration())
|
|
state := &linterState{
|
|
Linter: linter,
|
|
issues: incomingIssues,
|
|
paths: paths,
|
|
vars: vars,
|
|
exclude: exclude,
|
|
include: include,
|
|
deadline: deadline,
|
|
}
|
|
|
|
partitions, err := state.Partitions()
|
|
if err != nil {
|
|
errch <- err
|
|
continue
|
|
}
|
|
for _, args := range partitions {
|
|
wg.Add(1)
|
|
// 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(args []string) {
|
|
concurrencych <- true
|
|
err := executeLinter(state, args)
|
|
if err != nil {
|
|
errch <- err
|
|
}
|
|
<-concurrencych
|
|
wg.Done()
|
|
}(append(args))
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(incomingIssues)
|
|
close(errch)
|
|
}()
|
|
return processedIssues, errch
|
|
}
|
|
|
|
func executeLinter(state *linterState, args []string) error {
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("missing linter command")
|
|
}
|
|
|
|
start := time.Now()
|
|
debug("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 {
|
|
debug("warning: %s returned %s: %s", command, err, buf.String())
|
|
}
|
|
|
|
processOutput(state, buf.Bytes())
|
|
elapsed := time.Since(start)
|
|
debug("%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(state *linterState, out []byte) {
|
|
re := state.regex
|
|
all := re.FindAllSubmatchIndex(out, -1)
|
|
debug("%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 := &Issue{Line: 1, Linter: state.Linter.Name}
|
|
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 = relativePath(cwd, part)
|
|
|
|
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)
|
|
} else {
|
|
issue.Severity = Warning
|
|
}
|
|
if state.exclude != nil && state.exclude.MatchString(issue.String()) {
|
|
continue
|
|
}
|
|
if state.include != nil && !state.include.MatchString(issue.String()) {
|
|
continue
|
|
}
|
|
state.issues <- issue
|
|
}
|
|
}
|
|
|
|
func relativePath(root, path string) string {
|
|
fallback := path
|
|
root = resolvePath(root)
|
|
path = resolvePath(path)
|
|
var err error
|
|
path, err = filepath.Rel(root, path)
|
|
if err != nil {
|
|
warning("failed to make %s a relative path: %s", fallback, err)
|
|
return fallback
|
|
}
|
|
return path
|
|
}
|
|
|
|
func resolvePath(path string) string {
|
|
var err error
|
|
fallback := path
|
|
if !filepath.IsAbs(path) {
|
|
path, err = filepath.Abs(path)
|
|
if err != nil {
|
|
warning("failed to make %s an absolute path: %s", fallback, err)
|
|
return fallback
|
|
}
|
|
}
|
|
path, err = filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
warning("failed to resolve symlinks in %s: %s", fallback, err)
|
|
return fallback
|
|
}
|
|
return path
|
|
}
|
|
|
|
type sortedIssues struct {
|
|
issues []*Issue
|
|
order []string
|
|
}
|
|
|
|
func (s *sortedIssues) Len() int { return len(s.issues) }
|
|
func (s *sortedIssues) Swap(i, j int) { s.issues[i], s.issues[j] = s.issues[j], s.issues[i] }
|
|
|
|
// nolint: gocyclo
|
|
func (s *sortedIssues) Less(i, j int) bool {
|
|
l, r := s.issues[i], s.issues[j]
|
|
for _, key := range s.order {
|
|
switch key {
|
|
case "path":
|
|
if l.Path > r.Path {
|
|
return false
|
|
}
|
|
case "line":
|
|
if l.Line > r.Line {
|
|
return false
|
|
}
|
|
case "column":
|
|
if l.Col > r.Col {
|
|
return false
|
|
}
|
|
case "severity":
|
|
if l.Severity > r.Severity {
|
|
return false
|
|
}
|
|
case "message":
|
|
if l.Message > r.Message {
|
|
return false
|
|
}
|
|
case "linter":
|
|
if l.Linter > r.Linter {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func maybeSortIssues(issues chan *Issue) chan *Issue {
|
|
if reflect.DeepEqual([]string{"none"}, config.Sort) {
|
|
return issues
|
|
}
|
|
out := make(chan *Issue, 1000000)
|
|
sorted := &sortedIssues{
|
|
issues: []*Issue{},
|
|
order: config.Sort,
|
|
}
|
|
go func() {
|
|
for issue := range issues {
|
|
sorted.issues = append(sorted.issues, issue)
|
|
}
|
|
sort.Sort(sorted)
|
|
for _, issue := range sorted.issues {
|
|
out <- issue
|
|
}
|
|
close(out)
|
|
}()
|
|
return out
|
|
}
|