Compare commits
17 Commits
Author | SHA1 | Date |
---|---|---|
Stanislav Nikitin | 3ebb5b657c | |
Stanislav Nikitin | dc1287dae8 | |
Stanislav Nikitin | 9d5d5c262e | |
Stanislav Nikitin | 26ce90bb84 | |
Stanislav Nikitin | 16be332d38 | |
Stanislav Nikitin | e2a928fac4 | |
Andrey Shcherbinin | a252681b26 | |
Stanislav Nikitin | fb34d0d53e | |
Stanislav Nikitin | abe6734a46 | |
Stanislav Nikitin | c32d99ea65 | |
Stanislav Nikitin | edfbd5a90d | |
Stanislav Nikitin | 10d761f07d | |
Stanislav Nikitin | ddf3ff9240 | |
Stanislav Nikitin | 25d8b2776c | |
Stanislav Nikitin | cdbb3b7089 | |
Stanislav Nikitin | 614526b16d | |
Stanislav Nikitin | f1418a7a31 |
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
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
|
|
@ -1,3 +1,5 @@
|
|||
._bin
|
||||
.vscode
|
||||
metricator.yaml
|
||||
.idea
|
||||
*DS_Store*
|
||||
|
|
|
@ -3,23 +3,30 @@ run:
|
|||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
# Because globals might exist, but according to our codestyle they
|
||||
# should be lowercased and considered as unexported.
|
||||
- gochecknoglobals
|
||||
# While it might be useful it'll create more problems that will solve.
|
||||
- gocritic
|
||||
# Complains about main() lengths, which isn't an issue.
|
||||
- funlen
|
||||
# Magic numbers might be everywhere. Disabled for now.
|
||||
- gomnd
|
||||
# ToDos everywhere
|
||||
- godox
|
||||
# Why? WHY? WHY _test???
|
||||
- testpackage
|
||||
# Structs will contain some context.Context.
|
||||
- containedctx
|
||||
# Deprecated
|
||||
- exhaustivestruct
|
||||
linters-settings:
|
||||
lll:
|
||||
line-length: 120
|
||||
gocyclo:
|
||||
min-complexity: 40
|
||||
cyclop:
|
||||
max-complexity: 40
|
||||
gocognit:
|
||||
min-complexity: 40
|
||||
funlen:
|
||||
lines: 200
|
||||
statements: 100
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# There will be some ToDos.
|
||||
- linters:
|
||||
- godox
|
||||
text: "TODO"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM registry.gitlab.pztrn.name/containers/mirror/golang:1.15.5-alpine AS build
|
||||
FROM code.pztrn.name/containers/mirror/golang:1.18.3-alpine AS build
|
||||
|
||||
WORKDIR /go/src/gitlab.pztrn.name/pztrn/metricator
|
||||
COPY . .
|
||||
|
@ -11,11 +11,13 @@ ARG CI_COMMIT_TAG
|
|||
ENV CGO_ENABLED=0
|
||||
RUN apk add bash git make
|
||||
RUN make metricatord-build
|
||||
RUN make metricator-client-build
|
||||
|
||||
FROM registry.gitlab.pztrn.name/containers/mirror/golang:1.15.5-alpine
|
||||
FROM code.pztrn.name/containers/mirror/golang:1.18.3-alpine
|
||||
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/metricator-client /usr/local/bin/metricator-client
|
||||
|
||||
RUN apk add tzdata
|
||||
|
||||
|
|
11
Makefile
11
Makefile
|
@ -14,11 +14,20 @@ help: Makefile
|
|||
check-build-dir:
|
||||
@if [ ! -d "._bin" ]; then mkdir ._bin; fi
|
||||
|
||||
## metricator-client-build: builds metricator client and places into ${PWD}/._bin.
|
||||
metricator-client-build: check-build-dir
|
||||
@if [ -f ./._bin/metricator-client ]; then rm ./._bin/metricator-client; fi
|
||||
@scripts/build.sh metricator-client
|
||||
|
||||
## metricatord-build: builds metricator daemon and places into ${PWD}/._bin.
|
||||
metricatord-build: check-build-dir
|
||||
@rm ./._bin/metricatord || true
|
||||
@if [ -f ./._bin/metricatord ]; then rm ./._bin/metricatord; fi
|
||||
@scripts/build.sh metricatord
|
||||
|
||||
## metricator-client-run: starts metricator client. Use ARGS to supply args.
|
||||
metricator-client-run: metricator-client-build
|
||||
@./._bin/metricator-client -config ${CONFIG} $(ARGS)
|
||||
|
||||
## metricatord-run: starts metricator daemon.
|
||||
metricatord-run: metricatord-build
|
||||
./._bin/metricatord -config ${CONFIG}
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"go.dev.pztrn.name/metricator/internal/common"
|
||||
"go.dev.pztrn.name/metricator/internal/configuration"
|
||||
"go.dev.pztrn.name/metricator/internal/logger"
|
||||
"go.dev.pztrn.name/metricator/pkg/client"
|
||||
"go.dev.pztrn.name/metricator/pkg/schema"
|
||||
)
|
||||
|
||||
// nolint:gochecknoglobals
|
||||
var (
|
||||
application = flag.String("application", "", "Application to query.")
|
||||
appsList = flag.Bool("apps-list", false, "Show application's list registered at Metricator.")
|
||||
metricatorHost = flag.String("metricator-host", "", "IP address or domain on which Metricator is available")
|
||||
metricatorTimeout = flag.Int("metricator-timeout", 5, "Timeout for requests sent to Metricator.")
|
||||
metricsList = flag.Bool("metrics-list", false, "Show metrics list. Requires 'application' parameter.")
|
||||
metric = flag.String("metric", "", "Metric data to retrieve. Requires 'application' parameter.")
|
||||
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() {
|
||||
config := configuration.NewConfig()
|
||||
|
||||
// Parse configuration.
|
||||
flag.Parse()
|
||||
|
||||
err := config.Parse()
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to parse configuration:", err.Error())
|
||||
}
|
||||
|
||||
logger := logger.NewLogger(config.Logger)
|
||||
|
||||
logger.Debugf("Starting Metricator client, version %s from branch %s (build #%s, commit hash %s)\n",
|
||||
common.Version,
|
||||
common.Branch,
|
||||
common.Build,
|
||||
common.CommitHash,
|
||||
)
|
||||
|
||||
// Check configuration.
|
||||
// We cannot work at all if host isn't defined.
|
||||
if *metricatorHost == "" {
|
||||
logger.Infoln("Host isn't defined.")
|
||||
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// If nothing is requested - show error message.
|
||||
if !*appsList && !*metricsList && *metric == "" {
|
||||
logger.Infoln("No action specified.")
|
||||
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// When asking to metrics list we need application to be defined.
|
||||
if *metricsList && *application == "" {
|
||||
logger.Infoln("Getting metrics list requires 'application' parameter to be filled.")
|
||||
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// When asking for specific metric we need application to be defined.
|
||||
if *metric != "" && *application == "" {
|
||||
logger.Infoln("Getting metric data requires 'application' parameter to be filled.")
|
||||
|
||||
flag.PrintDefaults()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
clientConfig := &client.Config{
|
||||
Host: *metricatorHost,
|
||||
Timeout: *metricatorTimeout,
|
||||
}
|
||||
|
||||
clnt := client.NewClient(clientConfig, logger)
|
||||
|
||||
var data interface{}
|
||||
|
||||
switch {
|
||||
case *appsList:
|
||||
data = clnt.GetAppsList()
|
||||
case *metricsList:
|
||||
data = clnt.GetMetricsList(*application)
|
||||
case *metric != "":
|
||||
data = clnt.GetMetric(*application, *metric)
|
||||
}
|
||||
|
||||
switch *output {
|
||||
case "json":
|
||||
dataAsBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
logger.Infoln("Failed to marshal data from Metricator:", err.Error())
|
||||
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
fmt.Println(string(dataAsBytes))
|
||||
case "plain-by-lines":
|
||||
// For plain mode if we request metric - we should just print it and exit.
|
||||
if *metric != "" {
|
||||
fmt.Println(data)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
dataToPrint := []string{}
|
||||
|
||||
switch {
|
||||
case *appsList:
|
||||
appsListData, ok := data.(schema.AppsList)
|
||||
if !ok {
|
||||
logger.Infoln("Failed to cast parsed data into schema.AppsList!")
|
||||
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
for _, app := range appsListData {
|
||||
dataToPrint = append(dataToPrint, app)
|
||||
}
|
||||
case *metric != "":
|
||||
metricData, ok := data.(string)
|
||||
if !ok {
|
||||
logger.Infoln("Failed to cast parsed data into string!")
|
||||
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
dataToPrint = append(dataToPrint, metricData)
|
||||
case *metricsList:
|
||||
metricsData, ok := data.(schema.Metrics)
|
||||
if !ok {
|
||||
logger.Infoln("Failed to cast parsed data into schema.Metrics!")
|
||||
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
for _, metric := range metricsData {
|
||||
dataToPrint = append(dataToPrint, metric.Name)
|
||||
}
|
||||
}
|
||||
|
||||
for _, line := range dataToPrint {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,8 +29,7 @@ func main() {
|
|||
// Parse configuration.
|
||||
flag.Parse()
|
||||
|
||||
err := config.Parse()
|
||||
if err != nil {
|
||||
if err := config.Parse(); err != nil {
|
||||
log.Fatalln("Failed to parse configuration:", err.Error())
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# Metricator Client
|
||||
|
||||
Metricator client was created to help with Metricator daemon communication. It is able to produce different output based on selected format.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `-application` | string | Name of application to query. |
|
||||
| `-apps-list` | bool | Request type: applications list. |
|
||||
| `-config` | string | Path to configuration file. **MANDATORY** |
|
||||
| `-metric` | string | Request type: single metric. Name of metric to request. |
|
||||
| `-metricator-host` | URL | URL to Metricator daemon. (e.g. `http://127.0.0.1:34421`). **MANDATORY** |
|
||||
| `-metricator-timeout` | integer | Timeout in seconds for Metricator Client's HTTP requests. By default - 5 seconds. |
|
||||
| `-metrics-list` | bool | Request type: list of metrics. **Requires `-application` parameter to be filled.** |
|
||||
| `-output` | string | Type of output to produce (see below). |
|
||||
|
||||
## Request types
|
||||
|
||||
One of following parameters should be defined:
|
||||
|
||||
* `-apps-list` to get listing of applications that is registered at Metricator daemon.
|
||||
* `-metrics-list` with `-application` parameters to get list of metrics for application which Metricator know.
|
||||
* `-metric` with name of metric and `-application` (with a name of application) parameters to get specific metric for application which Metricator know.
|
||||
|
||||
See Examples section below.
|
||||
|
||||
## Outputs
|
||||
|
||||
Currently Metricator client is able to produce JSON and "Plain By Line" outputs. Their meanings:
|
||||
|
||||
* When `-output=json` is specified (or `-output` wasn't specified at all) Metricator Client will just dump response from Metricator Daemon.
|
||||
* When `-output=plain-by-line` is specified Metricator Client will transform received data into line-by-line output, e.g. every application name on separate line, every metric name on separate line, etc.
|
||||
|
||||
## Examples
|
||||
|
||||
* Get list of applications registered at Metricator daemon line-by-line (for later use with metrics autodiscovery helper):
|
||||
|
||||
```shell
|
||||
metricator-client -config=./metricator.yaml -metricator-host http://127.0.0.1:34421 -output plain-by-line -apps-list
|
||||
```
|
||||
|
||||
* Get list of metrics for application `test` in JSON format:
|
||||
|
||||
```shell
|
||||
metricator-client -config ./metricator.yaml -metricator-host http://127.0.0.1:34421 -application test -metrics-list
|
||||
```
|
||||
|
||||
* Get specific metric for application `test`:
|
||||
|
||||
```shell
|
||||
metricator-client -config ./metricator.yaml -metricator-host http://127.0.0.1:34421 -application test -metric mymegametric
|
||||
```
|
|
@ -31,3 +31,4 @@ dnsdist_frontend_responses/frontend:127.0.0.1:53/proto:UDP/thread:0
|
|||
* [Installation](INSTALL.md)
|
||||
* [Configuration](CONFIGURE.md)
|
||||
* [API](API.md)
|
||||
* [Client](CLIENT.md)
|
||||
|
|
|
@ -28,16 +28,18 @@ type Application struct {
|
|||
|
||||
// NewApplication creates new application.
|
||||
func NewApplication(ctx context.Context, name string, config *Config, logger *logger.Logger) *Application {
|
||||
a := &Application{
|
||||
// Some variables are initialized in initialize() function.
|
||||
// nolint:exhaustruct
|
||||
app := &Application{
|
||||
config: config,
|
||||
ctx: ctx,
|
||||
doneChan: make(chan struct{}),
|
||||
logger: logger,
|
||||
name: name,
|
||||
}
|
||||
a.initialize()
|
||||
app.initialize()
|
||||
|
||||
return a
|
||||
return app
|
||||
}
|
||||
|
||||
// GetDoneChan returns a channel which should be used to block execution until
|
||||
|
|
|
@ -11,5 +11,6 @@ type Config struct {
|
|||
Endpoint string `yaml:"endpoint"`
|
||||
// TimeBetweenRequests is a minimal amount of time which should pass
|
||||
// between requests.
|
||||
// nolint:tagliatelle
|
||||
TimeBetweenRequests time.Duration `yaml:"time_between_requests"`
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
@ -11,6 +10,9 @@ func (a *Application) fetch() {
|
|||
// Do not do anything if fetching is running.
|
||||
// ToDo: maybe another approach?
|
||||
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
|
||||
a.fetchIsRunningMutex.RUnlock()
|
||||
|
||||
|
@ -44,15 +46,13 @@ func (a *Application) fetch() {
|
|||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
data, err := a.parse(resp.Body)
|
||||
if err != nil {
|
||||
a.logger.Infoln("Failed to read response body for", a.name, "metrics:", err.Error())
|
||||
a.logger.Infoln("Failed to parse response body for", a.name, "metrics:", err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
data := a.parse(string(body))
|
||||
|
||||
a.storage.Put(data)
|
||||
|
||||
a.fetchIsRunningMutex.Lock()
|
||||
|
@ -64,7 +64,7 @@ func (a *Application) fetch() {
|
|||
func (a *Application) startFetcher() {
|
||||
fetchTicker := time.NewTicker(a.config.TimeBetweenRequests)
|
||||
|
||||
// nolint:exhaustivestruct
|
||||
// nolint:exhaustruct
|
||||
a.httpClient = &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"go.dev.pztrn.name/metricator/internal/models"
|
||||
"go.dev.pztrn.name/metricator/pkg/schema"
|
||||
)
|
||||
|
||||
// Parses passed body and returns a map suitable for pushing into storage.
|
||||
func (a *Application) parse(body string) map[string]models.Metric {
|
||||
data := make(map[string]models.Metric)
|
||||
// Parses io.Reader passed and returns a map suitable for pushing into storage.
|
||||
func (a *Application) parse(r io.Reader) (map[string]schema.Metric, error) {
|
||||
data := make(map[string]schema.Metric)
|
||||
|
||||
// ToDo: switch to bytes buffer and maybe do not read body in caller?
|
||||
splittedBody := strings.Split(body, "\n")
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Skip empty lines.
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, line := range splittedBody {
|
||||
// Prometheus line contains metric name and metric parameters defined
|
||||
// in "{}".
|
||||
var (
|
||||
|
@ -21,11 +29,6 @@ func (a *Application) parse(body string) map[string]models.Metric {
|
|||
params []string
|
||||
)
|
||||
|
||||
// Skip empty lines.
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
a.logger.Debugln("Analyzing line:", line)
|
||||
|
||||
name = a.getMetricName(line)
|
||||
|
@ -35,7 +38,7 @@ func (a *Application) parse(body string) map[string]models.Metric {
|
|||
if !found {
|
||||
a.logger.Debugln("Metric wasn't yet created, creating new structure")
|
||||
|
||||
metric = models.NewMetric(name, "", "", nil)
|
||||
metric = schema.NewMetric(name, "", "", nil)
|
||||
}
|
||||
|
||||
a.logger.Debugf("Got metric to use: %+v\n", metric)
|
||||
|
@ -80,19 +83,22 @@ func (a *Application) parse(body string) map[string]models.Metric {
|
|||
newMetric.Params = params
|
||||
|
||||
metric = newMetric
|
||||
data[metric.Name] = metric
|
||||
}
|
||||
|
||||
metric.Value = a.getMetricValue(line)
|
||||
|
||||
a.logger.Debugf("Got metric: %+v\n", metric)
|
||||
|
||||
data[name] = metric
|
||||
data[metric.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)
|
||||
|
||||
return data
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Gets metric description from passed line.
|
||||
|
@ -137,8 +143,8 @@ func (a *Application) getParametersForPrometheusMetric(line string) []string {
|
|||
paramNameFinished, paramValueStarted, paramValueFinished bool
|
||||
)
|
||||
|
||||
for _, r := range valuesString {
|
||||
if paramValueFinished && string(r) == "," {
|
||||
for _, runeChar := range valuesString {
|
||||
if paramValueFinished && string(runeChar) == "," {
|
||||
params = append(params, paramName+":"+paramValue)
|
||||
paramName, paramValue = "", ""
|
||||
paramNameFinished, paramValueStarted, paramValueFinished = false, false, false
|
||||
|
@ -150,8 +156,8 @@ func (a *Application) getParametersForPrometheusMetric(line string) []string {
|
|||
// "deeply nested"? I think not. So:
|
||||
// nolint:nestif
|
||||
if !paramNameFinished {
|
||||
if string(r) != "=" {
|
||||
paramName += string(r)
|
||||
if string(runeChar) != "=" {
|
||||
paramName += string(runeChar)
|
||||
|
||||
continue
|
||||
} else {
|
||||
|
@ -160,19 +166,19 @@ func (a *Application) getParametersForPrometheusMetric(line string) []string {
|
|||
continue
|
||||
}
|
||||
} else {
|
||||
if string(r) == "\"" && !paramValueStarted {
|
||||
if string(runeChar) == "\"" && !paramValueStarted {
|
||||
paramValueStarted = true
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if paramValueStarted && string(r) != "\"" {
|
||||
paramValue += string(r)
|
||||
if paramValueStarted && string(runeChar) != "\"" {
|
||||
paramValue += string(runeChar)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if paramValueStarted && string(r) == "\"" {
|
||||
if paramValueStarted && string(runeChar) == "\"" {
|
||||
paramValueFinished = true
|
||||
|
||||
continue
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package common
|
||||
|
||||
// nolint:gochecknoglobals
|
||||
var (
|
||||
Branch string
|
||||
Build string
|
||||
|
|
|
@ -21,16 +21,18 @@ var (
|
|||
|
||||
// Config is an application's configuration.
|
||||
type Config struct {
|
||||
configPath string
|
||||
// Applications describes configuration for remote application's endpoints.
|
||||
// Key is an application's name.
|
||||
Applications map[string]*application.Config `yaml:"applications"`
|
||||
// Logger is a logging configuration.
|
||||
Logger *logger.Config `yaml:"logger"`
|
||||
Logger *logger.Config `yaml:"logger"`
|
||||
configPath string
|
||||
}
|
||||
|
||||
// NewConfig returns new configuration.
|
||||
func NewConfig() *Config {
|
||||
// Fields are initialized when parsing YAML file.
|
||||
// nolint:exhaustruct
|
||||
c := &Config{}
|
||||
c.initialize()
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"go.dev.pztrn.name/metricator/internal/models"
|
||||
)
|
||||
|
||||
// nolint:gochecknoglobals
|
||||
var (
|
||||
errInvalidAPIVersion = errors.New("invalid API version")
|
||||
errInvalidApplication = errors.New("invalid application")
|
||||
|
@ -47,14 +48,14 @@ func (h *handler) getAppsList() ([]byte, error) {
|
|||
|
||||
// Gets request information from URL. Returns a structure with filled request
|
||||
// info and error if it occurs.
|
||||
func (h *handler) getRequestInfo(r *http.Request) (*models.RequestInfo, error) {
|
||||
func (h *handler) getRequestInfo(req *http.Request) (*models.RequestInfo, error) {
|
||||
// Request isn't for API or isn't versioned.
|
||||
if !strings.HasPrefix(r.URL.Path, "/api/v") {
|
||||
if !strings.HasPrefix(req.URL.Path, "/api/v") {
|
||||
return nil, errInvalidPath
|
||||
}
|
||||
|
||||
// Note: first element will always be empty!
|
||||
pathSplitted := strings.Split(r.URL.Path, "/")
|
||||
pathSplitted := strings.Split(req.URL.Path, "/")
|
||||
|
||||
// Request is for API but not enough items in URL was passed.
|
||||
if len(pathSplitted) < 4 {
|
||||
|
@ -118,20 +119,20 @@ func (h *handler) register(appName string, hndl common.HTTPHandlerFunc) {
|
|||
}
|
||||
|
||||
// ServeHTTP handles every HTTP request.
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
|
||||
startTime := time.Now()
|
||||
|
||||
defer func() {
|
||||
requestDuration := time.Since(startTime)
|
||||
|
||||
log.Printf("[HTTP Request] from %s to %s, duration %.4fs\n", r.RemoteAddr, r.URL.Path, requestDuration.Seconds())
|
||||
log.Printf("[HTTP Request] from %s to %s, duration %.4fs\n", req.RemoteAddr, req.URL.Path, requestDuration.Seconds())
|
||||
}()
|
||||
|
||||
// Validate request and extract needed info.
|
||||
rInfo, err := h.getRequestInfo(r)
|
||||
rInfo, err := h.getRequestInfo(req)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("400 bad request - " + err.Error()))
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = writer.Write([]byte("400 bad request - " + err.Error()))
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -143,14 +144,14 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
case "apps_list":
|
||||
appsList, err := h.getAppsList()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("400 bad request - " + err.Error()))
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = writer.Write([]byte("400 bad request - " + err.Error()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(appsList)
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write(appsList)
|
||||
|
||||
return
|
||||
case "info":
|
||||
|
@ -166,17 +167,18 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
Version: common.Version,
|
||||
}
|
||||
|
||||
// nolint:errchkjson
|
||||
infoBytes, _ := json.Marshal(infoData)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(infoBytes)
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write(infoBytes)
|
||||
|
||||
return
|
||||
case "metrics":
|
||||
handler, found := h.handlers[rInfo.Application]
|
||||
if !found {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("400 bad request - " + errInvalidApplication.Error()))
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = writer.Write([]byte("400 bad request - " + errInvalidApplication.Error()))
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -184,16 +186,16 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
// Get data from handler.
|
||||
data := handler(rInfo)
|
||||
if data == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("400 bad request - " + errNoData.Error()))
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = writer.Write([]byte("400 bad request - " + errNoData.Error()))
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(data))
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write([]byte(data))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("400 bad request - " + errInvalidPath.Error()))
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = writer.Write([]byte("400 bad request - " + errInvalidPath.Error()))
|
||||
}
|
||||
|
|
|
@ -25,15 +25,16 @@ type HTTPServer struct {
|
|||
// NewHTTPServer creates HTTP server and executes preliminary initialization
|
||||
// (HTTP server structure initialized but it doesn't start).
|
||||
func NewHTTPServer(ctx context.Context, cfg *configuration.Config, logger *logger.Logger) (*HTTPServer, chan struct{}) {
|
||||
h := &HTTPServer{
|
||||
// nolint:exhaustruct
|
||||
httpServer := &HTTPServer{
|
||||
config: cfg,
|
||||
ctx: ctx,
|
||||
doneChan: make(chan struct{}),
|
||||
logger: logger,
|
||||
}
|
||||
h.initialize()
|
||||
httpServer.initialize()
|
||||
|
||||
return h, h.doneChan
|
||||
return httpServer, httpServer.doneChan
|
||||
}
|
||||
|
||||
// Returns request's context based on main context of application.
|
||||
|
@ -50,7 +51,7 @@ func (h *HTTPServer) initialize() {
|
|||
handlers: make(map[string]common.HTTPHandlerFunc),
|
||||
}
|
||||
// We do not need to specify all possible parameters for HTTP server, so:
|
||||
// nolint:exhaustivestruct
|
||||
// nolint:exhaustruct
|
||||
h.server = &http.Server{
|
||||
// ToDo: make it all configurable.
|
||||
Addr: ":34421",
|
||||
|
|
|
@ -9,6 +9,10 @@ type Logger struct {
|
|||
|
||||
// NewLogger creates new logging wrapper and returns it to caller.
|
||||
func NewLogger(config *Config) *Logger {
|
||||
if config == nil {
|
||||
config = &Config{Debug: false}
|
||||
}
|
||||
|
||||
l := &Logger{config: config}
|
||||
|
||||
return l
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
package storage
|
||||
|
||||
import "go.dev.pztrn.name/metricator/internal/models"
|
||||
import "go.dev.pztrn.name/metricator/pkg/schema"
|
||||
|
||||
// GenericStorage describes interface every other storage should embed
|
||||
// and conform to as it contains essential things like context handling.
|
||||
type GenericStorage interface {
|
||||
// Get returns data from storage by key.
|
||||
Get(string) (models.Metric, error)
|
||||
Get(string) (schema.Metric, error)
|
||||
// GetAsSlice returns all data from storage as slice.
|
||||
GetAsSlice() []models.Metric
|
||||
GetAsSlice() []schema.Metric
|
||||
// GetDoneChan returns a channel which should be used to block execution
|
||||
// until storage's routines are completed.
|
||||
GetDoneChan() chan struct{}
|
||||
// Put puts passed data into storage.
|
||||
Put(map[string]models.Metric)
|
||||
Put(map[string]schema.Metric)
|
||||
// Start starts asynchronous things if needed.
|
||||
Start()
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"sync"
|
||||
|
||||
"go.dev.pztrn.name/metricator/internal/logger"
|
||||
"go.dev.pztrn.name/metricator/internal/models"
|
||||
"go.dev.pztrn.name/metricator/pkg/schema"
|
||||
)
|
||||
|
||||
// ErrMetricNotFound appears if requested metric wasn't found in storage.
|
||||
|
@ -14,29 +14,31 @@ var ErrMetricNotFound = errors.New("metric not found")
|
|||
|
||||
// Storage is an in-memory storage.
|
||||
type Storage struct {
|
||||
dataMutex sync.RWMutex
|
||||
ctx context.Context
|
||||
doneChan chan struct{}
|
||||
logger *logger.Logger
|
||||
data map[string]models.Metric
|
||||
data map[string]schema.Metric
|
||||
name string
|
||||
dataMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewStorage creates new in-memory storage to use.
|
||||
func NewStorage(ctx context.Context, name string, logger *logger.Logger) (*Storage, chan struct{}) {
|
||||
s := &Storage{
|
||||
// nolint:exhaustruct
|
||||
storage := &Storage{
|
||||
ctx: ctx,
|
||||
doneChan: make(chan struct{}),
|
||||
logger: logger,
|
||||
name: name,
|
||||
data: make(map[string]schema.Metric),
|
||||
}
|
||||
s.initialize()
|
||||
storage.initialize()
|
||||
|
||||
return s, s.doneChan
|
||||
return storage, storage.doneChan
|
||||
}
|
||||
|
||||
// Get returns data from storage by key.
|
||||
func (s *Storage) Get(key string) (models.Metric, error) {
|
||||
func (s *Storage) Get(key string) (schema.Metric, error) {
|
||||
s.logger.Debugln("Retrieving data for", key, "key from storage...")
|
||||
|
||||
s.dataMutex.RLock()
|
||||
|
@ -46,7 +48,7 @@ func (s *Storage) Get(key string) (models.Metric, error) {
|
|||
if !found {
|
||||
s.logger.Infoln("Key", key, "not found in storage!")
|
||||
|
||||
return models.NewMetric("", "", "", nil), ErrMetricNotFound
|
||||
return schema.NewMetric("", "", "", nil), ErrMetricNotFound
|
||||
}
|
||||
|
||||
s.logger.Debugf("Key %s found: %+v\n", key, data)
|
||||
|
@ -55,10 +57,10 @@ func (s *Storage) Get(key string) (models.Metric, error) {
|
|||
}
|
||||
|
||||
// GetAsSlice returns all data from storage as slice.
|
||||
func (s *Storage) GetAsSlice() []models.Metric {
|
||||
func (s *Storage) GetAsSlice() []schema.Metric {
|
||||
s.logger.Debugln("Returning all stored metrics as slice...")
|
||||
|
||||
metrics := make([]models.Metric, 0, len(s.data))
|
||||
metrics := make([]schema.Metric, 0, len(s.data))
|
||||
|
||||
for _, metric := range s.data {
|
||||
metrics = append(metrics, metric)
|
||||
|
@ -75,11 +77,11 @@ func (s *Storage) GetDoneChan() chan struct{} {
|
|||
|
||||
// Initializes internal things.
|
||||
func (s *Storage) initialize() {
|
||||
s.data = make(map[string]models.Metric)
|
||||
s.data = make(map[string]schema.Metric)
|
||||
}
|
||||
|
||||
// Put puts passed data into storage.
|
||||
func (s *Storage) Put(data map[string]models.Metric) {
|
||||
func (s *Storage) Put(data map[string]schema.Metric) {
|
||||
s.dataMutex.Lock()
|
||||
defer s.dataMutex.Unlock()
|
||||
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.dev.pztrn.name/metricator/internal/logger"
|
||||
"go.dev.pztrn.name/metricator/pkg/schema"
|
||||
)
|
||||
|
||||
// Client is a Metricator client that is ready to be used in other applications
|
||||
// or libraries.
|
||||
type Client struct {
|
||||
config *Config
|
||||
logger *logger.Logger
|
||||
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates new Metricator client.
|
||||
func NewClient(config *Config, logger *logger.Logger) *Client {
|
||||
// nolint:exhaustruct
|
||||
client := &Client{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
client.initialize()
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// Executes request and parses it's contents.
|
||||
func (c *Client) executeAndParse(req *http.Request, dest interface{}) error {
|
||||
c.logger.Debugf("Executing HTTP request to %s%s", c.config.Host, req.URL.RequestURI())
|
||||
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*time.Duration(c.config.Timeout))
|
||||
defer cancelFunc()
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
response, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
c.logger.Infoln("Failed to execute request to Metricator:", err.Error())
|
||||
|
||||
return fmt.Errorf("metricator client: %w", err)
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
respData, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
c.logger.Infoln("Failed to read response body:", err.Error())
|
||||
|
||||
return fmt.Errorf("metricator client: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(respData, dest)
|
||||
if err != nil {
|
||||
c.logger.Infoln("Failed to parse response:", err.Error())
|
||||
|
||||
return fmt.Errorf("metricator client: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAppsList returns a slice with applications that was registered at Metricator.
|
||||
func (c *Client) GetAppsList() schema.AppsList {
|
||||
address := fmt.Sprintf("%s/api/v1/apps_list", c.config.Host)
|
||||
|
||||
// Request's context sets in c.executeAndParse, so:
|
||||
// nolint:noctx
|
||||
req, err := http.NewRequest("GET", address, nil)
|
||||
if err != nil {
|
||||
c.logger.Infoln("Failed to create HTTP request:", err.Error())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
appsList := make(schema.AppsList, 0)
|
||||
|
||||
err = c.executeAndParse(req, &appsList)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return appsList
|
||||
}
|
||||
|
||||
// GetMetric returns value for metric.
|
||||
func (c *Client) GetMetric(appName, metricName string) interface{} {
|
||||
address := fmt.Sprintf("%s/api/v1/metrics/%s/%s", c.config.Host, appName, metricName)
|
||||
|
||||
// Request's context sets in c.executeAndParse, so:
|
||||
// nolint:noctx
|
||||
req, err := http.NewRequest("GET", address, nil)
|
||||
if err != nil {
|
||||
c.logger.Infoln("Failed to create HTTP request:", err.Error())
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
var data interface{}
|
||||
|
||||
err = c.executeAndParse(req, &data)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// GetMetricsList returns a slice with metrics names for passed application.
|
||||
func (c *Client) GetMetricsList(appName string) schema.Metrics {
|
||||
address := fmt.Sprintf("%s/api/v1/metrics/%s", c.config.Host, appName)
|
||||
|
||||
// Request's context sets in c.executeAndParse, so:
|
||||
// nolint:noctx
|
||||
req, err := http.NewRequest("GET", address, nil)
|
||||
if err != nil {
|
||||
c.logger.Infoln("Failed to create HTTP request:", err.Error())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
data := make(schema.Metrics, 0)
|
||||
|
||||
err = c.executeAndParse(req, &data)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Initializes internal states and storages.
|
||||
func (c *Client) initialize() {
|
||||
// We do not need to set everything for client actually, so:
|
||||
// nolint:exhaustruct
|
||||
c.httpClient = &http.Client{
|
||||
Timeout: time.Second * time.Duration(c.config.Timeout),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package client
|
||||
|
||||
// Config is a Metricator client configuration.
|
||||
type Config struct {
|
||||
// Host is a host where Metricator is available for requests.
|
||||
Host string
|
||||
// Timeout specifies HTTP client timeout.
|
||||
Timeout int
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package schema
|
||||
|
||||
// AppsList represents applications list structure from Metricator's API.
|
||||
type AppsList []string
|
||||
|
||||
// IsEmpty returns true if returned applications list is empty.
|
||||
func (a AppsList) IsEmpty() bool {
|
||||
return len(a) == 0
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package models
|
||||
package schema
|
||||
|
||||
// Metric is a generic metric structure.
|
||||
// Metric is a generic metric structure. Used in HTTP responses and data storage.
|
||||
type Metric struct {
|
||||
// BaseName is a metric's base name, used for constructing name.
|
||||
BaseName string
|
||||
|
@ -18,7 +18,7 @@ type Metric struct {
|
|||
|
||||
// NewMetric creates new structure for storing single metric data.
|
||||
func NewMetric(name, mType, description string, params []string) Metric {
|
||||
m := Metric{
|
||||
metric := Metric{
|
||||
BaseName: name,
|
||||
Name: name,
|
||||
Description: description,
|
||||
|
@ -27,7 +27,7 @@ func NewMetric(name, mType, description string, params []string) Metric {
|
|||
Value: "",
|
||||
}
|
||||
|
||||
return m
|
||||
return metric
|
||||
}
|
||||
|
||||
// GetValue returns metric's value.
|
|
@ -0,0 +1,9 @@
|
|||
package schema
|
||||
|
||||
// Metrics is a metrics collection response.
|
||||
type Metrics []*Metric
|
||||
|
||||
// IsEmpty returns true if returned applications list is empty.
|
||||
func (m Metrics) IsEmpty() bool {
|
||||
return len(m) == 0
|
||||
}
|
|
@ -2,7 +2,13 @@
|
|||
|
||||
# Metricator build script.
|
||||
|
||||
source ./scripts/shell_helpers/get_git_data.sh
|
||||
GO=${GO:=$(which go)}
|
||||
|
||||
if [ -d .git ]; then
|
||||
source ./scripts/shell_helpers/get_git_data.sh
|
||||
else
|
||||
source ./scripts/shell_helpers/get_release_data.sh
|
||||
fi
|
||||
|
||||
WHATTOBUILD=$1
|
||||
|
||||
|
@ -13,5 +19,7 @@ LINKERFLAGS="\
|
|||
-X go.dev.pztrn.name/metricator/internal/common.Version=${VERSION}"
|
||||
|
||||
|
||||
echo "Using $(go version) at ${GO}"
|
||||
|
||||
cd cmd/${WHATTOBUILD}
|
||||
go build -tags netgo -ldflags "${LINKERFLAGS} -w -extldflags '-static'" -o ../../._bin/${WHATTOBUILD}
|
||||
${GO} build -tags netgo -ldflags "${LINKERFLAGS} -w -extldflags '-static'" -o ../../._bin/${WHATTOBUILD}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Gets git data.
|
||||
# Should be sourced where neccessary.
|
||||
|
||||
export BRANCHNAME=${BRANCHNAME:=$(git branch --no-color --show-current)}
|
||||
export BUILDID=${BUILDID:=$(git rev-list HEAD --count)}
|
||||
export COMMITHASH=${COMMITHASH:=$(git rev-parse --verify HEAD)}
|
||||
export VERSION=${VERSION:="0.1.0-dev"}
|
||||
export VERSION=${VERSION:=$(cat VERSION)}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# Overrides some variables for release.
|
||||
|
||||
export VERSION=$(cat VERSION)
|
||||
export BRANCHNAME="release/${VERSION}"
|
||||
export COMMITHASH="none"
|
||||
export BUILDID="0"
|
|
@ -1,7 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Showing git data in console.
|
||||
source ./scripts/shell_helpers/get_git_data.sh
|
||||
if [ -d .git ]; then
|
||||
source ./scripts/shell_helpers/get_git_data.sh
|
||||
else
|
||||
source ./scripts/shell_helpers/get_release_data.sh
|
||||
fi
|
||||
|
||||
echo "* Branch: ${BRANCHNAME}"
|
||||
echo "* Build ID: ${BUILDID}"
|
||||
|
|
Loading…
Reference in New Issue