Initial commit.
This commit is contained in:
commit
cdf9997cfe
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
._bin
|
||||||
|
.vscode
|
26
Makefile
Normal file
26
Makefile
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# This is a Metricator Makefile.
|
||||||
|
# It contains calls to scripts placed in scripts directory.
|
||||||
|
|
||||||
|
help: Makefile
|
||||||
|
@echo -e "Metricator Makefile available subcommands:\n"
|
||||||
|
@cat $< | grep "## " | sort | sed -n 's/^## //p'
|
||||||
|
@echo ""
|
||||||
|
@make -f Makefile show-git-data
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
check-build-dir:
|
||||||
|
@if [ ! -d "._bin" ]; then mkdir ._bin; fi
|
||||||
|
|
||||||
|
## metricatord-build: builds metricator daemon and places into ${PWD}/._bin.
|
||||||
|
metricatord-build: check-build-dir
|
||||||
|
rm ./._bin/metricatord || true
|
||||||
|
cd cmd/metricatord && go build -o ../../._bin/metricatord
|
||||||
|
|
||||||
|
## metricatord-run: starts metricator daemon.
|
||||||
|
metricatord-run: metricatord-build
|
||||||
|
./._bin/metricatord -config ./metricator.example.yaml
|
||||||
|
|
||||||
|
show-git-data:
|
||||||
|
@echo "Parameters for current source code state:"
|
||||||
|
@scripts/show_git_data.sh
|
26
README.md
Normal file
26
README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Metricator
|
||||||
|
|
||||||
|
Simple proxy between prometheus-powered application and your NMS.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
I'm using NetXMS to monitor all of my systems. Many things I use exports metrics in prometheus format which can be utilized by custom parsing script. But I've encounter a performance problem when I need to monitor 30 metrics - parsing script will make 30 requests to prometheus endpoint which might affect performance.
|
||||||
|
|
||||||
|
Metricator will issue only one request and cache data in memory between them. Also it will expose HTTP API to get single metric which can be easily utilized with any NMS.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
* No authorization. **DO NOT** expose Metricator to wild world!
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
*TBW*
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
*TBW*
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Check [docs directory](/docs/INDEX.md).
|
62
cmd/metricatord/main.go
Normal file
62
cmd/metricatord/main.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"go.dev.pztrn.name/metricator/internal/common"
|
||||||
|
"go.dev.pztrn.name/metricator/internal/configuration"
|
||||||
|
"go.dev.pztrn.name/metricator/internal/datastore"
|
||||||
|
"go.dev.pztrn.name/metricator/internal/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Printf("Starting Metricator daemon, version %s from branch %s (build #%s, commit hash %s)\n",
|
||||||
|
common.Version,
|
||||||
|
common.Branch,
|
||||||
|
common.Build,
|
||||||
|
common.CommitHash,
|
||||||
|
)
|
||||||
|
|
||||||
|
mainCtx, cancelFunc := context.WithCancel(context.Background())
|
||||||
|
config := configuration.NewConfig()
|
||||||
|
|
||||||
|
httpSrv, httpStopped := httpserver.NewHTTPServer(mainCtx, config)
|
||||||
|
dataStore, dataStoreStopped := datastore.NewDataStore(mainCtx, config)
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
err := config.Parse()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to parse configuration:", err.Error())
|
||||||
|
}
|
||||||
|
log.Printf("Configuration parsed: %+v\n", config)
|
||||||
|
|
||||||
|
dataStore.Start()
|
||||||
|
httpSrv.Start()
|
||||||
|
|
||||||
|
log.Println("Metricator is started and ready to serve requests")
|
||||||
|
|
||||||
|
// CTRL+C handler.
|
||||||
|
signalHandler := make(chan os.Signal, 1)
|
||||||
|
shutdownDone := make(chan bool, 1)
|
||||||
|
|
||||||
|
signal.Notify(signalHandler, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-signalHandler
|
||||||
|
cancelFunc()
|
||||||
|
|
||||||
|
<-dataStoreStopped
|
||||||
|
<-httpStopped
|
||||||
|
|
||||||
|
shutdownDone <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-shutdownDone
|
||||||
|
log.Println("Metricator stopped")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
13
docs/INDEX.md
Normal file
13
docs/INDEX.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Metricator
|
||||||
|
|
||||||
|
Welcome to Metricator docs.
|
||||||
|
|
||||||
|
## What's the idea
|
||||||
|
|
||||||
|
Main idea for Metricator is to provide high-speed proxy between classic NMSes (like NetXMS, Nagios and so on) and other software that exposes metrics in Prometheus format.
|
||||||
|
|
||||||
|
Why proxy? Let's go by example.
|
||||||
|
|
||||||
|
Imagine that software you wish to monitor with classic NMS exposes 250 metric items and you wrote a simple script which returns needed data. Classic NMSes is able to process only one value per checker (usually), which means that processing every metric will do 250 HTTP requests to monitored software which is obviosly not good.
|
||||||
|
|
||||||
|
Metricator instead acts like Prometheus itself from monitored software PoV: it performs one HTTP request each timeout (configured in config), parses data and making it available to other requesters. As parsed metric data stored in memory it will be blazing fast and won't overload your system.
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module go.dev.pztrn.name/metricator
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v2 v2.3.0
|
3
go.sum
Normal file
3
go.sum
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
8
internal/common/const.go
Normal file
8
internal/common/const.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
var (
|
||||||
|
Branch string
|
||||||
|
Build string
|
||||||
|
CommitHash string
|
||||||
|
Version string
|
||||||
|
)
|
11
internal/common/context_keys.go
Normal file
11
internal/common/context_keys.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
// ContextKey is a type of context.Context keys.
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ContextKeyApplication specifies that returned value is a name of application.
|
||||||
|
ContextKeyApplication ContextKey = "applicationName"
|
||||||
|
// ContextKeyMetric specifies that returned value is a name of metric of application.
|
||||||
|
ContextKeyMetric ContextKey = "metricName"
|
||||||
|
)
|
6
internal/common/http_handler.go
Normal file
6
internal/common/http_handler.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// HTTPHandlerFunc describes signature of HTTP requests handling function.
|
||||||
|
type HTTPHandlerFunc func(context.Context) string
|
93
internal/configuration/config.go
Normal file
93
internal/configuration/config.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package configuration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrConfigurationFileDoesNotExist = errors.New("configuration file does not exist")
|
||||||
|
ErrConfigurationFilePathUndefined = errors.New("configuration file path wasn't provided")
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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]struct {
|
||||||
|
// Endpoint is a remote application endpoint which should give us metrics
|
||||||
|
// in Prometheus format.
|
||||||
|
Endpoint string
|
||||||
|
// TimeBetweenRequests is a minimal amount of time which should pass
|
||||||
|
// between requests.
|
||||||
|
TimeBetweenRequests time.Duration
|
||||||
|
}
|
||||||
|
// Datastore describes data storage configuration.
|
||||||
|
Datastore struct {
|
||||||
|
// ValidTimeout is a timeout for which every data entry will be considered
|
||||||
|
// as valid. After that timeout if value wasn't updated it will be considered
|
||||||
|
// as invalid and purged from memory.
|
||||||
|
ValidTimeout time.Duration `yaml:"valid_timeout"`
|
||||||
|
} `yaml:"datastore"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig returns new configuration.
|
||||||
|
func NewConfig() *Config {
|
||||||
|
c := &Config{}
|
||||||
|
c.initialize()
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes configuration.
|
||||||
|
func (c *Config) initialize() {
|
||||||
|
flag.StringVar(&c.configPath, "config", "", "Configuration file path.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses configuration.
|
||||||
|
func (c *Config) Parse() error {
|
||||||
|
if c.configPath == "" {
|
||||||
|
return ErrConfigurationFilePathUndefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(c.configPath, "~") {
|
||||||
|
userDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", "file path normalization: getting user's home directory", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.configPath = strings.Replace(c.configPath, "~", userDir, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgPath, err := filepath.Abs(c.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", "file path normalization: getting absolute path", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.configPath = cfgPath
|
||||||
|
|
||||||
|
if c.configPath == "" {
|
||||||
|
return fmt.Errorf("%s: %w", "file path normalization", ErrConfigurationFilePathUndefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileData, err := ioutil.ReadFile(c.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", "configuration file read", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(fileData, c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", "configuration file parsing", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
25
internal/datastore/application_store.go
Normal file
25
internal/datastore/application_store.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package datastore
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// This is application-specific data storage.
|
||||||
|
type applicationStorage struct {
|
||||||
|
metrics map[string]string
|
||||||
|
metricsMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates new application-specific storage.
|
||||||
|
func newApplicationStorage() *applicationStorage {
|
||||||
|
as := &applicationStorage{}
|
||||||
|
as.initialize()
|
||||||
|
|
||||||
|
return as
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes internal things.
|
||||||
|
func (as *applicationStorage) initialize() {
|
||||||
|
as.metrics = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts application-specific things, like goroutine for HTTP requests.
|
||||||
|
func (as *applicationStorage) start() {}
|
56
internal/datastore/datastore.go
Normal file
56
internal/datastore/datastore.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.dev.pztrn.name/metricator/internal/configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DataStore is a data storage structure. It keeps all gathered metrics and gives
|
||||||
|
// them away on request.
|
||||||
|
type DataStore struct {
|
||||||
|
config *configuration.Config
|
||||||
|
ctx context.Context
|
||||||
|
doneChan chan struct{}
|
||||||
|
|
||||||
|
datas map[string]*applicationStorage
|
||||||
|
datasMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDataStore creates new data storage.
|
||||||
|
func NewDataStore(ctx context.Context, cfg *configuration.Config) (*DataStore, chan struct{}) {
|
||||||
|
ds := &DataStore{
|
||||||
|
config: cfg,
|
||||||
|
ctx: ctx,
|
||||||
|
doneChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
ds.initialize()
|
||||||
|
|
||||||
|
return ds, ds.doneChan
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal things initialization.
|
||||||
|
func (ds *DataStore) initialize() {
|
||||||
|
ds.datas = make(map[string]*applicationStorage)
|
||||||
|
|
||||||
|
// Create applications defined in configuration.
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ds.ctx.Done()
|
||||||
|
log.Println("Data storage stopped")
|
||||||
|
|
||||||
|
ds.doneChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts data storage asynchronous things.
|
||||||
|
func (ds *DataStore) Start() {
|
||||||
|
log.Println("Starting data storage...")
|
||||||
|
|
||||||
|
ds.datasMutex.RLock()
|
||||||
|
for _, storage := range ds.datas {
|
||||||
|
storage.start()
|
||||||
|
}
|
||||||
|
}
|
20
internal/httpserver/handler.go
Normal file
20
internal/httpserver/handler.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.dev.pztrn.name/metricator/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP requests handler.
|
||||||
|
type handler struct {
|
||||||
|
handler common.HTTPHandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registers request's handler.
|
||||||
|
func (h *handler) register(hndl common.HTTPHandlerFunc) {
|
||||||
|
h.handler = hndl
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP handles every HTTP request.
|
||||||
|
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {}
|
79
internal/httpserver/httpserver.go
Normal file
79
internal/httpserver/httpserver.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.dev.pztrn.name/metricator/internal/configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPServer is a controlling structure for HTTP server.
|
||||||
|
type HTTPServer struct {
|
||||||
|
config *configuration.Config
|
||||||
|
ctx context.Context
|
||||||
|
doneChan chan struct{}
|
||||||
|
handler *handler
|
||||||
|
server *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPServer(ctx context.Context, cfg *configuration.Config) (*HTTPServer, chan struct{}) {
|
||||||
|
h := &HTTPServer{
|
||||||
|
config: cfg,
|
||||||
|
ctx: ctx,
|
||||||
|
doneChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
h.initialize()
|
||||||
|
|
||||||
|
return h, h.doneChan
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns request's context based on main context of application.
|
||||||
|
// Basically it returns main context and does nothing more.
|
||||||
|
func (h *HTTPServer) getRequestContext(_ net.Listener) context.Context {
|
||||||
|
return h.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes handler and HTTP server structure.
|
||||||
|
func (h *HTTPServer) initialize() {
|
||||||
|
h.handler = &handler{}
|
||||||
|
h.server = &http.Server{
|
||||||
|
// ToDo: make it all configurable.
|
||||||
|
Addr: ":34421",
|
||||||
|
BaseContext: h.getRequestContext,
|
||||||
|
Handler: h.handler,
|
||||||
|
ReadTimeout: time.Second * 10,
|
||||||
|
WriteTimeout: time.Second * 10,
|
||||||
|
MaxHeaderBytes: 1 << 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts HTTP server in another goroutine and one more goroutine which
|
||||||
|
// is listening to main context's Cancel() call to stop HTTP server.
|
||||||
|
func (h *HTTPServer) Start() {
|
||||||
|
go func() {
|
||||||
|
err := h.server.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "Server closed") {
|
||||||
|
log.Println("HTTP server failed to listen:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-h.ctx.Done()
|
||||||
|
log.Println("Shutting down HTTP server")
|
||||||
|
|
||||||
|
err := h.server.Shutdown(h.ctx)
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "context canceled") {
|
||||||
|
log.Println("Failed to stop HTTP server:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("HTTP server stopped")
|
||||||
|
|
||||||
|
h.doneChan <- struct{}{}
|
||||||
|
}()
|
||||||
|
}
|
2
metricator.example.yaml
Normal file
2
metricator.example.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
datastore:
|
||||||
|
valid_timeout: 30s
|
17
scripts/README.md
Normal file
17
scripts/README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Scripts directory
|
||||||
|
|
||||||
|
This directory contains various scripts that is used for whole application lifecycle - building, testing, deploying, etc.
|
||||||
|
|
||||||
|
**WARNING**: these scripts assuming that they're launched from root directory of project! Do not launch them like:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./some_stript.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead launch them from root directory like:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./scripts/some_script.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Otherwise your computer might explode. You were warned.
|
5
scripts/build.sh
Executable file
5
scripts/build.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Metricator build script.
|
||||||
|
|
||||||
|
source ./scripts/shell_helpers/get_git_data.sh
|
7
scripts/shell_helpers/get_git_data.sh
Normal file
7
scripts/shell_helpers/get_git_data.sh
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# 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"}
|
9
scripts/show_git_data.sh
Executable file
9
scripts/show_git_data.sh
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Showing git data in console.
|
||||||
|
source ./scripts/shell_helpers/get_git_data.sh
|
||||||
|
|
||||||
|
echo "* Branch: ${BRANCH}"
|
||||||
|
echo "* Build ID: ${BUILDID}"
|
||||||
|
echo "* Commit hash: ${COMMITHASH}"
|
||||||
|
echo "* Version: ${VERSION}"
|
Loading…
Reference in New Issue
Block a user