diff --git a/backend/api/spec.yaml b/backend/api/spec.yaml index 4cd82b8b7..824732a1e 100644 --- a/backend/api/spec.yaml +++ b/backend/api/spec.yaml @@ -1177,6 +1177,53 @@ paths: description: Activity not found response "500": description: List activity error response + /instance-metrics/prometheus: + get: + description: Get latest instance stats + operationId: getLatestInstanceStats + security: + - oidcBearerAuth: [] + - oidcCookieAuth: [] + - githubCookieAuth: [] + responses: + "200": + description: Get latest instance stats success response + content: + text/plain: + schema: + type: string + "500": + description: Get latest instance stats error response + /instance-metrics/json: + get: + description: Get instance stats + operationId: getInstanceStats + parameters: + - in: query + name: page + required: false + schema: + type: integer + minimum: 0 + - in: query + name: perpage + required: false + schema: + type: integer + minimum: 10 + security: + - oidcBearerAuth: [] + - oidcCookieAuth: [] + - githubCookieAuth: [] + responses: + "200": + description: List instance stats response + content: + application/x-ndjson: + schema: + $ref: "#/components/schemas/instanceStats" + "500": + description: Get instance stats error response components: schemas: ## request Body @@ -1339,6 +1386,46 @@ components: package_id: type: string + instanceStatsPage: + type: object + required: + - totalCount + - count + - applications + properties: + totalCount: + type: integer + count: + type: integer + applications: + type: array + items: + $ref: "#/components/schemas/instanceStats" + + instanceStats: + type: object + required: + - type + - channel + - version + - arch + - timestamp + - instances + properties: + type: + type: string + enum: ["instance_count"] + channel: + type: string + version: + type: string + arch: + type: string + timestamp: + type: string + count: + type: integer + ## response config: diff --git a/backend/pkg/api/instances.go b/backend/pkg/api/instances.go index becce2856..9473db62e 100644 --- a/backend/pkg/api/instances.go +++ b/backend/pkg/api/instances.go @@ -723,11 +723,24 @@ func (api *API) instanceStatsQuery(t *time.Time, duration *time.Duration) *goqu. return query } +// GetInstanceStatsCount returns the total number of InstanceStats. +func (api *API) GetInstanceStatsCount() (int, error) { + query := goqu.From("instance_stats").Select(goqu.L("count(*)")) + return api.GetCountQuery(query) +} + // GetInstanceStats returns an InstanceStats table with all instances that have // been previously been checked in. -func (api *API) GetInstanceStats() ([]InstanceStats, error) { - query, _, err := goqu.From("instance_stats"). - Order(goqu.C("timestamp").Asc()).ToSQL() +func (api *API) GetInstanceStats(page, perPage uint64) ([]InstanceStats, error) { + page, perPage = validatePaginationParams(page, perPage) + limit, offset := sqlPaginate(page, perPage) + + query, _, err := goqu. + From("instance_stats"). + Limit(limit). + Offset(offset). + Order(goqu.C("timestamp").Asc()). + ToSQL() if err != nil { return nil, err } diff --git a/backend/pkg/api/instances_test.go b/backend/pkg/api/instances_test.go index be3e766c3..6f0c96ce0 100644 --- a/backend/pkg/api/instances_test.go +++ b/backend/pkg/api/instances_test.go @@ -300,7 +300,7 @@ func TestUpdateInstanceStats(t *testing.T) { a := newForTest(t) defer a.Close() - instances, err := a.GetInstanceStats() + instances, err := a.GetInstanceStats(1, 100) assert.NoError(t, err) assert.Equal(t, 0, len(instances)) @@ -328,7 +328,7 @@ func TestUpdateInstanceStats(t *testing.T) { err = a.UpdateInstanceStats(&ts, &elapsed) assert.NoError(t, err) - instances, err = a.GetInstanceStats() + instances, err = a.GetInstanceStats(1, 100) assert.NoError(t, err) assert.Equal(t, 2, len(instances)) @@ -357,7 +357,7 @@ func TestUpdateInstanceStats(t *testing.T) { err = a.UpdateInstanceStats(&ts3, &elapsed) assert.NoError(t, err) - instances, err = a.GetInstanceStats() + instances, err = a.GetInstanceStats(1, 100) assert.NoError(t, err) assert.Equal(t, 5, len(instances)) diff --git a/backend/pkg/api/metrics.go b/backend/pkg/api/metrics.go index 255a1cc39..cb56cfea4 100644 --- a/backend/pkg/api/metrics.go +++ b/backend/pkg/api/metrics.go @@ -21,6 +21,12 @@ WHERE a.id = e.application_id AND e.event_type_id = et.id AND et.result = 0 AND GROUP BY app_name ORDER BY app_name `, ignoreFakeInstanceCondition("e.instance_id")) + + latestInstanceStatsSQL = ` +SELECT channel_name, version, arch, timestamp, instances AS instances_count +FROM instance_stats +WHERE timestamp = (SELECT MAX(timestamp) FROM instance_stats) +` ) type AppInstancesPerChannelMetric struct { @@ -77,6 +83,38 @@ func (api *API) GetFailedUpdatesMetrics() ([]FailedUpdatesMetric, error) { return metrics, nil } +type LatestInstanceStatsMetric struct { + ChannelName string `db:"channel_name" json:"channel_name"` + Version string `db:"version" json:"version"` + Arch string `db:"arch" json:"arch"` + Timestamp string `db:"timestamp" json:"timestamp"` + InstancesCount int `db:"instances_count" json:"instances_count"` +} + +func (api *API) GetLatestInstanceStatsMetrics() ([]LatestInstanceStatsMetric, error) { + var metrics []LatestInstanceStatsMetric + rows, err := api.db.Queryx(latestInstanceStatsSQL) + if err != nil { + return nil, fmt.Errorf("querying latest instance stats from SQL: %w", err) + } + defer rows.Close() + + for rows.Next() { + var metric LatestInstanceStatsMetric + if err := rows.StructScan(&metric); err != nil { + return nil, fmt.Errorf("scanning instance stat metric: %w", err) + } + + metrics = append(metrics, metric) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return metrics, nil +} + func (api *API) DbStats() sql.DBStats { return api.db.Stats() } diff --git a/backend/pkg/codegen/client.gen.go b/backend/pkg/codegen/client.gen.go index cf971961f..61a4c0d11 100644 --- a/backend/pkg/codegen/client.gen.go +++ b/backend/pkg/codegen/client.gen.go @@ -203,6 +203,12 @@ type ClientInterface interface { // Health request Health(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetInstanceStats request + GetInstanceStats(ctx context.Context, params *GetInstanceStatsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetLatestInstanceStats request + GetLatestInstanceStats(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // Login request Login(ctx context.Context, params *LoginParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -716,6 +722,30 @@ func (c *Client) Health(ctx context.Context, reqEditors ...RequestEditorFn) (*ht return c.Client.Do(req) } +func (c *Client) GetInstanceStats(ctx context.Context, params *GetInstanceStatsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetInstanceStatsRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetLatestInstanceStats(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetLatestInstanceStatsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) Login(ctx context.Context, params *LoginParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewLoginRequest(c.Server, params) if err != nil { @@ -2687,6 +2717,98 @@ func NewHealthRequest(server string) (*http.Request, error) { return req, nil } +// NewGetInstanceStatsRequest generates requests for GetInstanceStats +func NewGetInstanceStatsRequest(server string, params *GetInstanceStatsParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/instance-metrics/json") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Page != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "page", runtime.ParamLocationQuery, *params.Page); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Perpage != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "perpage", runtime.ParamLocationQuery, *params.Perpage); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetLatestInstanceStatsRequest generates requests for GetLatestInstanceStats +func NewGetLatestInstanceStatsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/instance-metrics/prometheus") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewLoginRequest generates requests for Login func NewLoginRequest(server string, params *LoginParams) (*http.Request, error) { var err error @@ -3061,6 +3183,12 @@ type ClientWithResponsesInterface interface { // HealthWithResponse request HealthWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*HealthResponse, error) + // GetInstanceStatsWithResponse request + GetInstanceStatsWithResponse(ctx context.Context, params *GetInstanceStatsParams, reqEditors ...RequestEditorFn) (*GetInstanceStatsResponse, error) + + // GetLatestInstanceStatsWithResponse request + GetLatestInstanceStatsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetLatestInstanceStatsResponse, error) + // LoginWithResponse request LoginWithResponse(ctx context.Context, params *LoginParams, reqEditors ...RequestEditorFn) (*LoginResponse, error) @@ -3781,6 +3909,48 @@ func (r HealthResponse) StatusCode() int { return 0 } +type GetInstanceStatsResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r GetInstanceStatsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetInstanceStatsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetLatestInstanceStatsResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r GetLatestInstanceStatsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetLatestInstanceStatsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type LoginResponse struct { Body []byte HTTPResponse *http.Response @@ -4268,6 +4438,24 @@ func (c *ClientWithResponses) HealthWithResponse(ctx context.Context, reqEditors return ParseHealthResponse(rsp) } +// GetInstanceStatsWithResponse request returning *GetInstanceStatsResponse +func (c *ClientWithResponses) GetInstanceStatsWithResponse(ctx context.Context, params *GetInstanceStatsParams, reqEditors ...RequestEditorFn) (*GetInstanceStatsResponse, error) { + rsp, err := c.GetInstanceStats(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetInstanceStatsResponse(rsp) +} + +// GetLatestInstanceStatsWithResponse request returning *GetLatestInstanceStatsResponse +func (c *ClientWithResponses) GetLatestInstanceStatsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetLatestInstanceStatsResponse, error) { + rsp, err := c.GetLatestInstanceStats(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetLatestInstanceStatsResponse(rsp) +} + // LoginWithResponse request returning *LoginResponse func (c *ClientWithResponses) LoginWithResponse(ctx context.Context, params *LoginParams, reqEditors ...RequestEditorFn) (*LoginResponse, error) { rsp, err := c.Login(ctx, params, reqEditors...) @@ -5112,6 +5300,38 @@ func ParseHealthResponse(rsp *http.Response) (*HealthResponse, error) { return response, nil } +// ParseGetInstanceStatsResponse parses an HTTP response from a GetInstanceStatsWithResponse call +func ParseGetInstanceStatsResponse(rsp *http.Response) (*GetInstanceStatsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetInstanceStatsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetLatestInstanceStatsResponse parses an HTTP response from a GetLatestInstanceStatsWithResponse call +func ParseGetLatestInstanceStatsResponse(rsp *http.Response) (*GetLatestInstanceStatsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetLatestInstanceStatsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + // ParseLoginResponse parses an HTTP response from a LoginWithResponse call func ParseLoginResponse(rsp *http.Response) (*LoginResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/backend/pkg/codegen/server.gen.go b/backend/pkg/codegen/server.gen.go index cbd88e7a9..cb0956ff8 100644 --- a/backend/pkg/codegen/server.gen.go +++ b/backend/pkg/codegen/server.gen.go @@ -110,6 +110,12 @@ type ServerInterface interface { // (GET /health) Health(ctx echo.Context) error + // (GET /instance-metrics/json) + GetInstanceStats(ctx echo.Context, params GetInstanceStatsParams) error + + // (GET /instance-metrics/prometheus) + GetLatestInstanceStats(ctx echo.Context) error + // (GET /login) Login(ctx echo.Context, params LoginParams) error @@ -1207,6 +1213,52 @@ func (w *ServerInterfaceWrapper) Health(ctx echo.Context) error { return err } +// GetInstanceStats converts echo context to params. +func (w *ServerInterfaceWrapper) GetInstanceStats(ctx echo.Context) error { + var err error + + ctx.Set(OidcBearerAuthScopes, []string{}) + + ctx.Set(OidcCookieAuthScopes, []string{}) + + ctx.Set(GithubCookieAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetInstanceStatsParams + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page", ctx.QueryParams(), ¶ms.Page) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter page: %s", err)) + } + + // ------------- Optional query parameter "perpage" ------------- + + err = runtime.BindQueryParameter("form", true, false, "perpage", ctx.QueryParams(), ¶ms.Perpage) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter perpage: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetInstanceStats(ctx, params) + return err +} + +// GetLatestInstanceStats converts echo context to params. +func (w *ServerInterfaceWrapper) GetLatestInstanceStats(ctx echo.Context) error { + var err error + + ctx.Set(OidcBearerAuthScopes, []string{}) + + ctx.Set(OidcCookieAuthScopes, []string{}) + + ctx.Set(GithubCookieAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetLatestInstanceStats(ctx) + return err +} + // Login converts echo context to params. func (w *ServerInterfaceWrapper) Login(ctx echo.Context) error { var err error @@ -1369,6 +1421,8 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.PUT(baseURL+"/api/instances/:instanceID", wrapper.UpdateInstance) router.GET(baseURL+"/config", wrapper.GetConfig) router.GET(baseURL+"/health", wrapper.Health) + router.GET(baseURL+"/instance-metrics/json", wrapper.GetInstanceStats) + router.GET(baseURL+"/instance-metrics/prometheus", wrapper.GetLatestInstanceStats) router.GET(baseURL+"/login", wrapper.Login) router.GET(baseURL+"/login/cb", wrapper.LoginCb) router.POST(baseURL+"/login/token", wrapper.LoginToken) diff --git a/backend/pkg/codegen/spec.gen.go b/backend/pkg/codegen/spec.gen.go index e13259bfb..32631a911 100644 --- a/backend/pkg/codegen/spec.gen.go +++ b/backend/pkg/codegen/spec.gen.go @@ -18,75 +18,78 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/pkg/codegen/types.gen.go b/backend/pkg/codegen/types.gen.go index 69d5aa0fb..1baeafd88 100644 --- a/backend/pkg/codegen/types.gen.go +++ b/backend/pkg/codegen/types.gen.go @@ -13,6 +13,11 @@ const ( OidcCookieAuthScopes = "oidcCookieAuth.Scopes" ) +// Defines values for InstanceStatsType. +const ( + InstanceStatsTypeInstanceCount InstanceStatsType = "instance_count" +) + // Activity defines model for activity. type Activity struct { AppID string `json:"app_id"` @@ -234,6 +239,19 @@ type InstancePage struct { Total int `json:"total"` } +// InstanceStats defines model for instanceStats. +type InstanceStats struct { + Arch string `json:"arch"` + Channel string `json:"channel"` + Count *int `json:"count,omitempty"` + Timestamp string `json:"timestamp"` + Type InstanceStatsType `json:"type"` + Version string `json:"version"` +} + +// InstanceStatsType defines model for InstanceStats.Type. +type InstanceStatsType string + // InstanceStatusHistories defines model for instanceStatusHistories. type InstanceStatusHistories = []InstanceStatusHistory @@ -394,6 +412,12 @@ type PaginatePackagesParams struct { SearchVersion *string `form:"searchVersion,omitempty" json:"searchVersion,omitempty"` } +// GetInstanceStatsParams defines parameters for GetInstanceStats. +type GetInstanceStatsParams struct { + Page *int `form:"page,omitempty" json:"page,omitempty"` + Perpage *int `form:"perpage,omitempty" json:"perpage,omitempty"` +} + // LoginParams defines parameters for Login. type LoginParams struct { LoginRedirectUrl string `form:"login_redirect_url" json:"login_redirect_url"` diff --git a/backend/pkg/handler/instances.go b/backend/pkg/handler/instances.go index 9e684a28d..b0def299e 100644 --- a/backend/pkg/handler/instances.go +++ b/backend/pkg/handler/instances.go @@ -2,11 +2,13 @@ package handler import ( "database/sql" + "encoding/json" "net/http" "github.com/labstack/echo/v4" "github.com/kinvolk/nebraska/backend/pkg/codegen" + "github.com/kinvolk/nebraska/backend/pkg/metrics" ) func (h *Handler) GetInstance(ctx echo.Context, appIDorProductID string, _ string, instanceID string) error { @@ -68,3 +70,65 @@ func (h *Handler) UpdateInstance(ctx echo.Context, instanceID string) error { return ctx.JSON(http.StatusOK, instance) } + +func (h *Handler) GetLatestInstanceStats(ctx echo.Context) error { + metrics.InstanceMetricsHandler.ServeHTTP(ctx.Response(), ctx.Request()) + return nil +} + +func (h *Handler) GetInstanceStats(ctx echo.Context, params codegen.GetInstanceStatsParams) error { + if params.Page == nil { + params.Page = &defaultPage + } + + if params.Perpage == nil { + params.Perpage = &defaultPerPage + } + + totalCount, err := h.db.GetInstanceStatsCount() + if err != nil { + logger.Error().Err(err).Msg("get instance stats count") + return ctx.NoContent(http.StatusBadRequest) + } + + metrics, err := h.db.GetInstanceStats(uint64(*params.Page), uint64(*params.Perpage)) + if err != nil { + logger.Error().Err(err).Msg("getInstanceStats - getting instance stats") + return ctx.NoContent(http.StatusInternalServerError) + } + + ctx.Response().Header().Set(echo.HeaderContentType, "application/x-ndjson") + ctx.Response().WriteHeader(http.StatusOK) + + m := make([]map[string]interface{}, len(metrics)) + for i, metric := range metrics { + formattedMetric := map[string]interface{}{ + "type": "instance_count", + "channel": metric.ChannelName, + "version": metric.Version, + "arch": metric.Arch, + "timestamp": metric.Timestamp, + "count": metric.Instances, + } + + m[i] = formattedMetric + } + + p := metricPage{ + TotalCount: totalCount, + Count: len(metrics), + Metrics: m, + } + if err := json.NewEncoder(ctx.Response()).Encode(p); err != nil { + logger.Error().Err(err).Msg("getInstanceStats - encoding instance stats") + return ctx.NoContent(http.StatusInternalServerError) + } + + return nil +} + +type metricPage struct { + TotalCount int `json:"totalCount"` + Count int `json:"count"` + Metrics []map[string]interface{} `json:"metrics"` +} diff --git a/backend/pkg/metrics/metrics.go b/backend/pkg/metrics/metrics.go index 056b05890..27fddda60 100644 --- a/backend/pkg/metrics/metrics.go +++ b/backend/pkg/metrics/metrics.go @@ -6,6 +6,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/kinvolk/nebraska/backend/pkg/api" "github.com/kinvolk/nebraska/backend/pkg/util" @@ -16,6 +17,9 @@ const ( ) var ( + InstanceMetricsRegistry = prometheus.NewRegistry() + InstanceMetricsHandler = promhttp.HandlerFor(InstanceMetricsRegistry, promhttp.HandlerOpts{}) + appInstancePerChannelGaugeMetric = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "nebraska", @@ -40,6 +44,20 @@ var ( }, ) + latestInstanceStatsGaugeMetric = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "nebraska", + Name: "instance_count", + Help: "Number of instances per channel, version, and architecture", + }, + []string{ + "channel", + "version", + "arch", + "timestamp", + }, + ) + openConnections = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "nebraska", @@ -72,15 +90,22 @@ func registerNebraskaMetrics() error { collectors := []prometheus.Collector{ appInstancePerChannelGaugeMetric, failedUpdatesGaugeMetric, + latestInstanceStatsGaugeMetric, openConnections, inUseConnections, idleConnections, } for _, collector := range collectors { - err := prometheus.Register(collector) - if err != nil { - return err + if collector == latestInstanceStatsGaugeMetric { + if err := InstanceMetricsRegistry.Register(collector); err != nil { + return fmt.Errorf("registering instance stats collector: %w", err) + } + } else { + err := prometheus.Register(collector) + if err != nil { + return fmt.Errorf("registering Prometheus collector: %w", err) + } } } return nil @@ -130,6 +155,9 @@ func RegisterAndInstrument(api *api.API) error { // calculateMetrics calculates the application metrics and updates the respective metric. func calculateMetrics(api *api.API) error { + // reset instance stats on each refresh + latestInstanceStatsGaugeMetric.Reset() + aipcMetrics, err := api.GetAppInstancesPerChannelMetrics() if err != nil { return fmt.Errorf("failed to get app instances per channel metrics: %w", err) @@ -148,6 +176,15 @@ func calculateMetrics(api *api.API) error { failedUpdatesGaugeMetric.WithLabelValues(metric.ApplicationName).Set(float64(metric.FailureCount)) } + lisMetrics, err := api.GetLatestInstanceStatsMetrics() + if err != nil { + return fmt.Errorf("failed to get latest instance stats metrics: %w", err) + } + + for _, metric := range lisMetrics { + latestInstanceStatsGaugeMetric.WithLabelValues(metric.ChannelName, metric.Version, metric.Arch, metric.Timestamp).Set(float64(metric.InstancesCount)) + } + // db stats dbStats := api.DbStats() openConnections.Set(float64(dbStats.OpenConnections))