Compare commits

..

No commits in common. "master" and "v0.2.0" have entirely different histories.

17 changed files with 78 additions and 149 deletions

View File

@ -1,43 +0,0 @@
---
kind: pipeline
type: docker
name: build
steps:
- name: lint
image: code.pztrn.name/containers/mirror/golangci/golangci-lint:v1.46.2
environment:
CGO_ENABLED: 0
commands:
- golangci-lint run
- name: test
image: code.pztrn.name/containers/mirror/golang:1.18.3-alpine
environment:
CGO_ENABLED: 0
commands:
- go test ./...
- name: build master image
image: code.pztrn.name/containers/mirror/plugins/docker:20.13.0
when:
branch: ["master"]
settings:
registry: code.pztrn.name
username: drone
password:
from_secret: drone_secret
repo: code.pztrn.name/apps/metricator
auto_tag: true
- name: build tagged image
image: code.pztrn.name/containers/mirror/plugins/docker:20.13.0
when:
event: ["tag"]
settings:
registry: code.pztrn.name
username: drone
password:
from_secret: drone_secret
repo: code.pztrn.name/apps/metricator
auto_tag: true

2
.gitignore vendored
View File

@ -1,5 +1,3 @@
._bin ._bin
.vscode .vscode
metricator.yaml metricator.yaml
.idea
*DS_Store*

View File

@ -9,15 +9,11 @@ linters:
- gomnd - gomnd
# Why? WHY? WHY _test??? # Why? WHY? WHY _test???
- testpackage - testpackage
# Structs will contain some context.Context.
- containedctx
# Deprecated
- exhaustivestruct
linters-settings: linters-settings:
lll: lll:
line-length: 120 line-length: 120
cyclop: gocyclo:
max-complexity: 40 min-complexity: 40
gocognit: gocognit:
min-complexity: 40 min-complexity: 40
funlen: funlen:

View File

@ -1,4 +1,4 @@
FROM code.pztrn.name/containers/mirror/golang:1.18.3-alpine AS build FROM registry.gitlab.pztrn.name/containers/mirror/golang:1.15.5-alpine AS build
WORKDIR /go/src/gitlab.pztrn.name/pztrn/metricator WORKDIR /go/src/gitlab.pztrn.name/pztrn/metricator
COPY . . COPY . .
@ -13,7 +13,7 @@ RUN apk add bash git make
RUN make metricatord-build RUN make metricatord-build
RUN make metricator-client-build RUN make metricator-client-build
FROM code.pztrn.name/containers/mirror/golang:1.18.3-alpine FROM registry.gitlab.pztrn.name/containers/mirror/golang:1.15.5-alpine
LABEL maintainer="Stanislav N. <pztrn@pztrn.name>" LABEL maintainer="Stanislav N. <pztrn@pztrn.name>"
COPY --from=build /go/src/gitlab.pztrn.name/pztrn/metricator/._bin/metricatord /usr/local/bin/metricatord COPY --from=build /go/src/gitlab.pztrn.name/pztrn/metricator/._bin/metricatord /usr/local/bin/metricatord

View File

@ -25,9 +25,6 @@ var (
output = flag.String("output", "json", "Output format. Can be 'json' or 'plain-by-line'.") output = flag.String("output", "json", "Output format. Can be 'json' or 'plain-by-line'.")
) )
// This function uses fmt.Println to print lines without timestamps to make it easy
// to parse output, so:
// nolint:forbidigo
func main() { func main() {
config := configuration.NewConfig() config := configuration.NewConfig()
@ -86,17 +83,17 @@ func main() {
Timeout: *metricatorTimeout, Timeout: *metricatorTimeout,
} }
clnt := client.NewClient(clientConfig, logger) c := client.NewClient(clientConfig, logger)
var data interface{} var data interface{}
switch { switch {
case *appsList: case *appsList:
data = clnt.GetAppsList() data = c.GetAppsList()
case *metricsList: case *metricsList:
data = clnt.GetMetricsList(*application) data = c.GetMetricsList(*application)
case *metric != "": case *metric != "":
data = clnt.GetMetric(*application, *metric) data = c.GetMetric(*application, *metric)
} }
switch *output { switch *output {

View File

@ -29,7 +29,8 @@ func main() {
// Parse configuration. // Parse configuration.
flag.Parse() flag.Parse()
if err := config.Parse(); err != nil { err := config.Parse()
if err != nil {
log.Fatalln("Failed to parse configuration:", err.Error()) log.Fatalln("Failed to parse configuration:", err.Error())
} }

View File

@ -28,18 +28,16 @@ type Application struct {
// NewApplication creates new application. // NewApplication creates new application.
func NewApplication(ctx context.Context, name string, config *Config, logger *logger.Logger) *Application { func NewApplication(ctx context.Context, name string, config *Config, logger *logger.Logger) *Application {
// Some variables are initialized in initialize() function. a := &Application{
// nolint:exhaustruct
app := &Application{
config: config, config: config,
ctx: ctx, ctx: ctx,
doneChan: make(chan struct{}), doneChan: make(chan struct{}),
logger: logger, logger: logger,
name: name, name: name,
} }
app.initialize() a.initialize()
return app return a
} }
// GetDoneChan returns a channel which should be used to block execution until // GetDoneChan returns a channel which should be used to block execution until

View File

@ -11,6 +11,5 @@ type Config struct {
Endpoint string `yaml:"endpoint"` Endpoint string `yaml:"endpoint"`
// TimeBetweenRequests is a minimal amount of time which should pass // TimeBetweenRequests is a minimal amount of time which should pass
// between requests. // between requests.
// nolint:tagliatelle
TimeBetweenRequests time.Duration `yaml:"time_between_requests"` TimeBetweenRequests time.Duration `yaml:"time_between_requests"`
} }

View File

@ -1,6 +1,7 @@
package application package application
import ( import (
"io/ioutil"
"net/http" "net/http"
"time" "time"
) )
@ -10,9 +11,6 @@ func (a *Application) fetch() {
// Do not do anything if fetching is running. // Do not do anything if fetching is running.
// ToDo: maybe another approach? // ToDo: maybe another approach?
a.fetchIsRunningMutex.RLock() a.fetchIsRunningMutex.RLock()
// This is an optimization to avoid excessive waiting when using Lock().
// Most of time application will wait between fetches.
// nolint:ifshort
isFetching := a.fetchIsRunning isFetching := a.fetchIsRunning
a.fetchIsRunningMutex.RUnlock() a.fetchIsRunningMutex.RUnlock()
@ -46,13 +44,15 @@ func (a *Application) fetch() {
defer resp.Body.Close() defer resp.Body.Close()
data, err := a.parse(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
a.logger.Infoln("Failed to parse response body for", a.name, "metrics:", err.Error()) a.logger.Infoln("Failed to read response body for", a.name, "metrics:", err.Error())
return return
} }
data := a.parse(string(body))
a.storage.Put(data) a.storage.Put(data)
a.fetchIsRunningMutex.Lock() a.fetchIsRunningMutex.Lock()
@ -64,7 +64,7 @@ func (a *Application) fetch() {
func (a *Application) startFetcher() { func (a *Application) startFetcher() {
fetchTicker := time.NewTicker(a.config.TimeBetweenRequests) fetchTicker := time.NewTicker(a.config.TimeBetweenRequests)
// nolint:exhaustruct // nolint:exhaustivestruct
a.httpClient = &http.Client{ a.httpClient = &http.Client{
Timeout: time.Second * 5, Timeout: time.Second * 5,
} }

View File

@ -1,27 +1,19 @@
package application package application
import ( import (
"bufio"
"fmt"
"io"
"strings" "strings"
"go.dev.pztrn.name/metricator/pkg/schema" "go.dev.pztrn.name/metricator/pkg/schema"
) )
// Parses io.Reader passed and returns a map suitable for pushing into storage. // Parses passed body and returns a map suitable for pushing into storage.
func (a *Application) parse(r io.Reader) (map[string]schema.Metric, error) { func (a *Application) parse(body string) map[string]schema.Metric {
data := make(map[string]schema.Metric) data := make(map[string]schema.Metric)
scanner := bufio.NewScanner(r) // ToDo: switch to bytes buffer and maybe do not read body in caller?
for scanner.Scan() { splittedBody := strings.Split(body, "\n")
line := scanner.Text()
// Skip empty lines.
if line == "" {
continue
}
for _, line := range splittedBody {
// Prometheus line contains metric name and metric parameters defined // Prometheus line contains metric name and metric parameters defined
// in "{}". // in "{}".
var ( var (
@ -29,6 +21,11 @@ func (a *Application) parse(r io.Reader) (map[string]schema.Metric, error) {
params []string params []string
) )
// Skip empty lines.
if line == "" {
continue
}
a.logger.Debugln("Analyzing line:", line) a.logger.Debugln("Analyzing line:", line)
name = a.getMetricName(line) name = a.getMetricName(line)
@ -83,22 +80,19 @@ func (a *Application) parse(r io.Reader) (map[string]schema.Metric, error) {
newMetric.Params = params newMetric.Params = params
metric = newMetric metric = newMetric
data[metric.Name] = metric
} }
metric.Value = a.getMetricValue(line) metric.Value = a.getMetricValue(line)
a.logger.Debugf("Got metric: %+v\n", metric) a.logger.Debugf("Got metric: %+v\n", metric)
data[metric.Name] = metric data[name] = metric
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("wasn't able to scan input: %w", err)
} }
a.logger.Debugf("Data parsed: %+v\n", data) a.logger.Debugf("Data parsed: %+v\n", data)
return data, nil return data
} }
// Gets metric description from passed line. // Gets metric description from passed line.
@ -143,8 +137,8 @@ func (a *Application) getParametersForPrometheusMetric(line string) []string {
paramNameFinished, paramValueStarted, paramValueFinished bool paramNameFinished, paramValueStarted, paramValueFinished bool
) )
for _, runeChar := range valuesString { for _, r := range valuesString {
if paramValueFinished && string(runeChar) == "," { if paramValueFinished && string(r) == "," {
params = append(params, paramName+":"+paramValue) params = append(params, paramName+":"+paramValue)
paramName, paramValue = "", "" paramName, paramValue = "", ""
paramNameFinished, paramValueStarted, paramValueFinished = false, false, false paramNameFinished, paramValueStarted, paramValueFinished = false, false, false
@ -156,8 +150,8 @@ func (a *Application) getParametersForPrometheusMetric(line string) []string {
// "deeply nested"? I think not. So: // "deeply nested"? I think not. So:
// nolint:nestif // nolint:nestif
if !paramNameFinished { if !paramNameFinished {
if string(runeChar) != "=" { if string(r) != "=" {
paramName += string(runeChar) paramName += string(r)
continue continue
} else { } else {
@ -166,19 +160,19 @@ func (a *Application) getParametersForPrometheusMetric(line string) []string {
continue continue
} }
} else { } else {
if string(runeChar) == "\"" && !paramValueStarted { if string(r) == "\"" && !paramValueStarted {
paramValueStarted = true paramValueStarted = true
continue continue
} }
if paramValueStarted && string(runeChar) != "\"" { if paramValueStarted && string(r) != "\"" {
paramValue += string(runeChar) paramValue += string(r)
continue continue
} }
if paramValueStarted && string(runeChar) == "\"" { if paramValueStarted && string(r) == "\"" {
paramValueFinished = true paramValueFinished = true
continue continue

View File

@ -21,18 +21,16 @@ var (
// Config is an application's configuration. // Config is an application's configuration.
type Config struct { type Config struct {
configPath string
// Applications describes configuration for remote application's endpoints. // Applications describes configuration for remote application's endpoints.
// Key is an application's name. // Key is an application's name.
Applications map[string]*application.Config `yaml:"applications"` Applications map[string]*application.Config `yaml:"applications"`
// Logger is a logging configuration. // Logger is a logging configuration.
Logger *logger.Config `yaml:"logger"` Logger *logger.Config `yaml:"logger"`
configPath string
} }
// NewConfig returns new configuration. // NewConfig returns new configuration.
func NewConfig() *Config { func NewConfig() *Config {
// Fields are initialized when parsing YAML file.
// nolint:exhaustruct
c := &Config{} c := &Config{}
c.initialize() c.initialize()

View File

@ -48,14 +48,14 @@ func (h *handler) getAppsList() ([]byte, error) {
// Gets request information from URL. Returns a structure with filled request // Gets request information from URL. Returns a structure with filled request
// info and error if it occurs. // info and error if it occurs.
func (h *handler) getRequestInfo(req *http.Request) (*models.RequestInfo, error) { func (h *handler) getRequestInfo(r *http.Request) (*models.RequestInfo, error) {
// Request isn't for API or isn't versioned. // Request isn't for API or isn't versioned.
if !strings.HasPrefix(req.URL.Path, "/api/v") { if !strings.HasPrefix(r.URL.Path, "/api/v") {
return nil, errInvalidPath return nil, errInvalidPath
} }
// Note: first element will always be empty! // Note: first element will always be empty!
pathSplitted := strings.Split(req.URL.Path, "/") pathSplitted := strings.Split(r.URL.Path, "/")
// Request is for API but not enough items in URL was passed. // Request is for API but not enough items in URL was passed.
if len(pathSplitted) < 4 { if len(pathSplitted) < 4 {
@ -119,20 +119,20 @@ func (h *handler) register(appName string, hndl common.HTTPHandlerFunc) {
} }
// ServeHTTP handles every HTTP request. // ServeHTTP handles every HTTP request.
func (h *handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) { func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
startTime := time.Now() startTime := time.Now()
defer func() { defer func() {
requestDuration := time.Since(startTime) requestDuration := time.Since(startTime)
log.Printf("[HTTP Request] from %s to %s, duration %.4fs\n", req.RemoteAddr, req.URL.Path, requestDuration.Seconds()) log.Printf("[HTTP Request] from %s to %s, duration %.4fs\n", r.RemoteAddr, r.URL.Path, requestDuration.Seconds())
}() }()
// Validate request and extract needed info. // Validate request and extract needed info.
rInfo, err := h.getRequestInfo(req) rInfo, err := h.getRequestInfo(r)
if err != nil { if err != nil {
writer.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("400 bad request - " + err.Error())) _, _ = w.Write([]byte("400 bad request - " + err.Error()))
return return
} }
@ -144,14 +144,14 @@ func (h *handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
case "apps_list": case "apps_list":
appsList, err := h.getAppsList() appsList, err := h.getAppsList()
if err != nil { if err != nil {
writer.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("400 bad request - " + err.Error())) _, _ = w.Write([]byte("400 bad request - " + err.Error()))
return return
} }
writer.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_, _ = writer.Write(appsList) _, _ = w.Write(appsList)
return return
case "info": case "info":
@ -167,18 +167,17 @@ func (h *handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
Version: common.Version, Version: common.Version,
} }
// nolint:errchkjson
infoBytes, _ := json.Marshal(infoData) infoBytes, _ := json.Marshal(infoData)
writer.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_, _ = writer.Write(infoBytes) _, _ = w.Write(infoBytes)
return return
case "metrics": case "metrics":
handler, found := h.handlers[rInfo.Application] handler, found := h.handlers[rInfo.Application]
if !found { if !found {
writer.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("400 bad request - " + errInvalidApplication.Error())) _, _ = w.Write([]byte("400 bad request - " + errInvalidApplication.Error()))
return return
} }
@ -186,16 +185,16 @@ func (h *handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
// Get data from handler. // Get data from handler.
data := handler(rInfo) data := handler(rInfo)
if data == "" { if data == "" {
writer.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("400 bad request - " + errNoData.Error())) _, _ = w.Write([]byte("400 bad request - " + errNoData.Error()))
} }
writer.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte(data)) _, _ = w.Write([]byte(data))
return return
} }
writer.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
_, _ = writer.Write([]byte("400 bad request - " + errInvalidPath.Error())) _, _ = w.Write([]byte("400 bad request - " + errInvalidPath.Error()))
} }

View File

@ -25,16 +25,15 @@ type HTTPServer struct {
// NewHTTPServer creates HTTP server and executes preliminary initialization // NewHTTPServer creates HTTP server and executes preliminary initialization
// (HTTP server structure initialized but it doesn't start). // (HTTP server structure initialized but it doesn't start).
func NewHTTPServer(ctx context.Context, cfg *configuration.Config, logger *logger.Logger) (*HTTPServer, chan struct{}) { func NewHTTPServer(ctx context.Context, cfg *configuration.Config, logger *logger.Logger) (*HTTPServer, chan struct{}) {
// nolint:exhaustruct h := &HTTPServer{
httpServer := &HTTPServer{
config: cfg, config: cfg,
ctx: ctx, ctx: ctx,
doneChan: make(chan struct{}), doneChan: make(chan struct{}),
logger: logger, logger: logger,
} }
httpServer.initialize() h.initialize()
return httpServer, httpServer.doneChan return h, h.doneChan
} }
// Returns request's context based on main context of application. // Returns request's context based on main context of application.
@ -51,7 +50,7 @@ func (h *HTTPServer) initialize() {
handlers: make(map[string]common.HTTPHandlerFunc), handlers: make(map[string]common.HTTPHandlerFunc),
} }
// We do not need to specify all possible parameters for HTTP server, so: // We do not need to specify all possible parameters for HTTP server, so:
// nolint:exhaustruct // nolint:exhaustivestruct
h.server = &http.Server{ h.server = &http.Server{
// ToDo: make it all configurable. // ToDo: make it all configurable.
Addr: ":34421", Addr: ":34421",

View File

@ -9,10 +9,6 @@ type Logger struct {
// NewLogger creates new logging wrapper and returns it to caller. // NewLogger creates new logging wrapper and returns it to caller.
func NewLogger(config *Config) *Logger { func NewLogger(config *Config) *Logger {
if config == nil {
config = &Config{Debug: false}
}
l := &Logger{config: config} l := &Logger{config: config}
return l return l

View File

@ -14,27 +14,25 @@ var ErrMetricNotFound = errors.New("metric not found")
// Storage is an in-memory storage. // Storage is an in-memory storage.
type Storage struct { type Storage struct {
dataMutex sync.RWMutex
ctx context.Context ctx context.Context
doneChan chan struct{} doneChan chan struct{}
logger *logger.Logger logger *logger.Logger
data map[string]schema.Metric data map[string]schema.Metric
name string name string
dataMutex sync.RWMutex
} }
// NewStorage creates new in-memory storage to use. // NewStorage creates new in-memory storage to use.
func NewStorage(ctx context.Context, name string, logger *logger.Logger) (*Storage, chan struct{}) { func NewStorage(ctx context.Context, name string, logger *logger.Logger) (*Storage, chan struct{}) {
// nolint:exhaustruct s := &Storage{
storage := &Storage{
ctx: ctx, ctx: ctx,
doneChan: make(chan struct{}), doneChan: make(chan struct{}),
logger: logger, logger: logger,
name: name, name: name,
data: make(map[string]schema.Metric),
} }
storage.initialize() s.initialize()
return storage, storage.doneChan return s, s.doneChan
} }
// Get returns data from storage by key. // Get returns data from storage by key.

View File

@ -23,14 +23,13 @@ type Client struct {
// NewClient creates new Metricator client. // NewClient creates new Metricator client.
func NewClient(config *Config, logger *logger.Logger) *Client { func NewClient(config *Config, logger *logger.Logger) *Client {
// nolint:exhaustruct c := &Client{
client := &Client{
config: config, config: config,
logger: logger, logger: logger,
} }
client.initialize() c.initialize()
return client return c
} }
// Executes request and parses it's contents. // Executes request and parses it's contents.
@ -140,7 +139,7 @@ func (c *Client) GetMetricsList(appName string) schema.Metrics {
// Initializes internal states and storages. // Initializes internal states and storages.
func (c *Client) initialize() { func (c *Client) initialize() {
// We do not need to set everything for client actually, so: // We do not need to set everything for client actually, so:
// nolint:exhaustruct // nolint:exhaustivestruct
c.httpClient = &http.Client{ c.httpClient = &http.Client{
Timeout: time.Second * time.Duration(c.config.Timeout), Timeout: time.Second * time.Duration(c.config.Timeout),
} }

View File

@ -18,7 +18,7 @@ type Metric struct {
// NewMetric creates new structure for storing single metric data. // NewMetric creates new structure for storing single metric data.
func NewMetric(name, mType, description string, params []string) Metric { func NewMetric(name, mType, description string, params []string) Metric {
metric := Metric{ m := Metric{
BaseName: name, BaseName: name,
Name: name, Name: name,
Description: description, Description: description,
@ -27,7 +27,7 @@ func NewMetric(name, mType, description string, params []string) Metric {
Value: "", Value: "",
} }
return metric return m
} }
// GetValue returns metric's value. // GetValue returns metric's value.