The very basic metricator-client and package.
Package can be used in external things if needed.
This commit is contained in:
		
							
								
								
									
										9
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								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} | ||||
|   | ||||
							
								
								
									
										105
									
								
								cmd/metricator-client/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								cmd/metricator-client/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
| } | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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. | ||||
| @@ -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() | ||||
|  | ||||
|   | ||||
							
								
								
									
										146
									
								
								pkg/client/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								pkg/client/client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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), | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										9
									
								
								pkg/client/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								pkg/client/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										9
									
								
								pkg/schema/apps_list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								pkg/schema/apps_list.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										9
									
								
								pkg/schema/metrics.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								pkg/schema/metrics.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user