Initial commit.

This commit is contained in:
2020-11-28 23:34:20 +05:00
commit cdf9997cfe
20 changed files with 475 additions and 0 deletions

8
internal/common/const.go Normal file
View File

@@ -0,0 +1,8 @@
package common
var (
Branch string
Build string
CommitHash string
Version string
)

View 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"
)

View File

@@ -0,0 +1,6 @@
package common
import "context"
// HTTPHandlerFunc describes signature of HTTP requests handling function.
type HTTPHandlerFunc func(context.Context) string

View 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
}

View 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() {}

View 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()
}
}

View 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) {}

View 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{}{}
}()
}