diff --git a/Makefile b/Makefile index d997f64..e62b9b9 100644 --- a/Makefile +++ b/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 + @rm ./._bin/metricator-client || true + @scripts/build.sh metricator-client + ## metricatord-build: builds metricator daemon and places into ${PWD}/._bin. metricatord-build: check-build-dir @rm ./._bin/metricatord || true @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} diff --git a/cmd/metricator-client/main.go b/cmd/metricator-client/main.go new file mode 100644 index 0000000..0b5d682 --- /dev/null +++ b/cmd/metricator-client/main.go @@ -0,0 +1,105 @@ +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" +) + +// 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.") +) + +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, + } + + c := client.NewClient(clientConfig, logger) + + var data interface{} + + switch { + case *appsList: + data = c.GetAppsList() + case *metricsList: + data = c.GetMetricsList(*application) + case *metric != "": + data = c.GetMetric(*application, *metric) + } + + 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)) +} diff --git a/internal/application/parser.go b/internal/application/parser.go index f964735..3b631db 100644 --- a/internal/application/parser.go +++ b/internal/application/parser.go @@ -3,12 +3,12 @@ package application import ( "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) +func (a *Application) parse(body string) map[string]schema.Metric { + data := make(map[string]schema.Metric) // ToDo: switch to bytes buffer and maybe do not read body in caller? splittedBody := strings.Split(body, "\n") @@ -35,7 +35,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) diff --git a/internal/storage/generic.go b/internal/storage/generic.go index ef84278..cb450fe 100644 --- a/internal/storage/generic.go +++ b/internal/storage/generic.go @@ -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() } diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index cd2c814..b3aa6e4 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -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. @@ -17,7 +17,7 @@ type Storage struct { ctx context.Context doneChan chan struct{} logger *logger.Logger - data map[string]models.Metric + data map[string]schema.Metric name string dataMutex sync.RWMutex } @@ -36,7 +36,7 @@ func NewStorage(ctx context.Context, name string, logger *logger.Logger) (*Stora } // 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 +46,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 +55,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 +75,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() diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..c65f741 --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,146 @@ +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 { + c := &Client{ + config: config, + logger: logger, + } + c.initialize() + + return c +} + +// 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:exhaustivestruct + c.httpClient = &http.Client{ + Timeout: time.Second * time.Duration(c.config.Timeout), + } +} diff --git a/pkg/client/config.go b/pkg/client/config.go new file mode 100644 index 0000000..ffc4c2e --- /dev/null +++ b/pkg/client/config.go @@ -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 +} diff --git a/pkg/schema/apps_list.go b/pkg/schema/apps_list.go new file mode 100644 index 0000000..c7741f7 --- /dev/null +++ b/pkg/schema/apps_list.go @@ -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 +} diff --git a/internal/models/metric.go b/pkg/schema/metric.go similarity index 90% rename from internal/models/metric.go rename to pkg/schema/metric.go index 5e2f3c6..f6fa3fd 100644 --- a/internal/models/metric.go +++ b/pkg/schema/metric.go @@ -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 diff --git a/pkg/schema/metrics.go b/pkg/schema/metrics.go new file mode 100644 index 0000000..f1c0c17 --- /dev/null +++ b/pkg/schema/metrics.go @@ -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 +}