metrics

package module
v0.6.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 16 Imported by: 0

README

GoREST Metrics Plugin

CI Go Report Card License

A production-ready GoREST plugin for tracking integer metrics on polymorphic resources. Track view counts, downloads, likes, or any custom metric with historical data and efficient querying.

Features

  • Polymorphic Metrics: Track metrics for any resource type (posts, users, products, etc.)
  • Flexible Values: Support for both positive and negative integers (deltas, adjustments)
  • Unique Constraints: Enforces one metric per (resource, resource_id, name) combination
  • Historical Tracking: Automatic timestamp tracking with created_at
  • Advanced Filtering: Filter by resource type, ID, name, or value ranges
  • Multi-Database: Full support for PostgreSQL, MySQL, and SQLite
  • Efficient Indexing: Optimized composite indexes for fast queries
  • Type Safety: Generic CRUD operations with compile-time type checking

Installation

go get github.com/nicolasbonnici/gorest-metrics

Development Environment

To set up your development environment:

make install

This will:

  • Install Go dependencies
  • Install development tools (golangci-lint)
  • Set up git hooks (pre-commit linting and tests)

Quick Start

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/nicolasbonnici/gorest"
    "github.com/nicolasbonnici/gorest-metrics"
)

func main() {
    app := gorest.New(gorest.Config{
        Database: db,
        Plugins: []plugin.Plugin{
            metrics.NewPlugin(),
        },
        PluginConfigs: map[string]map[string]interface{}{
            "metrics": {
                "allowed_types": []interface{}{"post", "user", "product"},
                "max_name_length": 255,
                "only_positive_values": false,
                "pagination_limit": 50,
                "max_pagination_limit": 200,
            },
        },
    })

    app.Listen(":8080")
}

Configuration

Config Options
Option Type Default Description
allowed_types []string ["post"] Resource types that can have metrics
max_key_length int 255 Maximum length for metric keys (1-255)
only_positive_values bool false Restrict values to positive integers only
pagination_limit int 50 Default page size for list queries
max_pagination_limit int 200 Maximum allowed page size (1-1000)
Example Configuration (YAML)
plugins:
  metrics:
    allowed_types:
      - post
      - user
      - product
    max_key_length: 255
    only_positive_values: false
    pagination_limit: 50
    max_pagination_limit: 200

Database Schema

The plugin automatically creates a metrics table with the following structure:

CREATE TABLE metrics (
    id UUID PRIMARY KEY,
    resource TEXT NOT NULL,              -- Resource type (post, user, etc.)
    resource_id UUID NOT NULL,            -- Foreign key to resource
    name VARCHAR(255) NOT NULL,            -- Metric name (views, etc.)
    value INTEGER NOT NULL DEFAULT 0,     -- Metric value (supports negative)
    created_at TIMESTAMP NOT NULL,        -- Last update timestamp
    UNIQUE (resource, resource_id, name)   -- One metric per resource/name
);

-- Composite indexes for performance
CREATE INDEX idx_metrics_resource ON metrics(resource, resource_id, name);
CREATE INDEX idx_metrics_name ON metrics(name, created_at);
CREATE INDEX idx_metrics_resource_id ON metrics(resource_id);

API Endpoints

List Metrics

Get all metrics with filtering, pagination, and ordering.

GET /metrics?resource=post&resourceId={uuid}&name=views&limit=50&page=1

Query Parameters:

  • resource - Filter by resource type
  • resourceId - Filter by specific resource UUID
  • name - Filter by metric name
  • value[eq], value[gt], value[gte], value[lt], value[lte] - Filter by value ranges
  • limit - Results per page (default: 50, max: 200)
  • page - Page number (default: 1)
  • order - Sort order (e.g., -value, createdAt, -name)
  • count - Include total count (default: true)

Example Response:

{
  "@context": "/api/contexts/Metric",
  "@id": "/api/metrics",
  "@type": "hydra:Collection",
  "hydra:member": [
    {
      "@id": "/api/metrics/123e4567-e89b-12d3-a456-426614174000",
      "@type": "Metric",
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "resource": "post",
      "resourceId": "550e8400-e29b-41d4-a716-446655440000",
      "name": "views",
      "value": 1250,
      "createdAt": "2026-02-08T10:30:00Z"
    }
  ],
  "hydra:totalItems": 1,
  "hydra:view": {
    "@id": "/api/metrics?page=1",
    "@type": "hydra:PartialCollectionView",
    "hydra:first": "/api/metrics?page=1",
    "hydra:last": "/api/metrics?page=1"
  }
}
Get Metric by ID
GET /metrics/{id}

Example Response:

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "resource": "post",
  "resourceId": "550e8400-e29b-41d4-a716-446655440000",
  "name": "views",
  "value": 1250,
  "createdAt": "2026-02-08T10:30:00Z"
}
Create Metric
POST /metrics
Content-Type: application/json

{
  "resource": "post",
  "resourceId": "550e8400-e29b-41d4-a716-446655440000",
  "name": "views",
  "value": 1
}

Response: 201 Created with created metric object

Note: Creating a metric with duplicate (resource, resourceId, name) will fail with 500 Internal Server Error. Consider using Update endpoint to modify existing metrics.

Update Metric

Update only the value of an existing metric.

PUT /metrics/{id}
Content-Type: application/json

{
  "value": 1251
}

Response: 200 OK with updated metric object

Delete Metric
DELETE /metrics/{id}

Response: 204 No Content

Use Cases

View Counter
# Increment view count for a blog post
curl -X POST http://localhost:8080/metrics \
  -H "Content-Type: application/json" \
  -d '{
    "resource": "post",
    "resourceId": "550e8400-e29b-41d4-a716-446655440000",
    "name": "views",
    "value": 1
  }'

# Get current view count
curl "http://localhost:8080/metrics?resource=post&resourceId=550e8400-e29b-41d4-a716-446655440000&name=views"

# Update view count (after fetching metric ID)
curl -X PUT http://localhost:8080/metrics/{id} \
  -H "Content-Type: application/json" \
  -d '{"value": 125}'
Download Tracker
# Track product downloads
curl -X POST http://localhost:8080/metrics \
  -H "Content-Type: application/json" \
  -d '{
    "resource": "product",
    "resourceId": "650e8400-e29b-41d4-a716-446655440000",
    "name": "download_count",
    "value": 1
  }'

# Get top 10 most downloaded products
curl "http://localhost:8080/metrics?name=download_count&order=-value&limit=10"
Reputation System
# Track user reputation (supports negative values)
curl -X POST http://localhost:8080/metrics \
  -H "Content-Type: application/json" \
  -d '{
    "resource": "user",
    "resourceId": "750e8400-e29b-41d4-a716-446655440000",
    "name": "reputation",
    "value": 100
  }'

# Decrease reputation
curl -X PUT http://localhost:8080/metrics/{id} \
  -H "Content-Type: application/json" \
  -d '{"value": 90}'
Analytics Dashboard
# Get all metrics for a specific post
curl "http://localhost:8080/metrics?resource=post&resourceId=550e8400-e29b-41d4-a716-446655440000"

# Get metrics with high values (engagement threshold)
curl "http://localhost:8080/metrics?value[gte]=1000&order=-value"

# Get recently updated metrics
curl "http://localhost:8080/metrics?order=-createdAt&limit=20"

Development

Prerequisites
  • Go 1.25.1+
  • golangci-lint (for linting)
Setup
# Clone the repository
git clone https://github.com/nicolasbonnici/gorest-metrics.git
cd gorest-metrics

# Install dependencies
go mod download

# Install development tools
make install
Available Make Targets
make help              # Show all available targets
make test              # Run tests with race detection and coverage
make coverage          # Generate HTML coverage report
make lint              # Run golangci-lint
make lint-fix          # Run linter with auto-fix
make build             # Build verification
make clean             # Clean caches and build artifacts
Running Tests
# Run all tests with coverage
make test

# Generate coverage report
make coverage
# Open coverage.html in browser

# Run with verbose output
go test -v -race ./...
Git Hooks

Install pre-commit hooks to ensure code quality:

./.githooks/install.sh

The pre-commit hook runs:

  • make lint - Code linting
  • make test - Full test suite

To bypass hooks (not recommended):

git commit --no-verify

Security

  • Input Validation: All requests validated before database operations
  • Resource Type Whitelist: Only configured resource types allowed
  • UUID Validation: Strict UUID format enforcement for resource IDs
  • name Length Limits: Configurable maximum name length (1-255)
  • Value Constraints: Optional positive-only value enforcement
  • Filter Limits: Maximum 50 values per filter field to prevent abuse

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Support

Documentation

Index

Constants

View Source
const MaxFilterValuesPerField = 50

Variables

This section is empty.

Functions

func NewPlugin

func NewPlugin() plugin.Plugin

func RegisterMetricRoutes

func RegisterMetricRoutes(router fiber.Router, db database.Database, config *Config, writer *batchWriter)

func RegisterRoutes

func RegisterRoutes(router fiber.Router, db database.Database, config *Config, writer *batchWriter)

Types

type Config

type Config struct {
	Database           database.Database
	AllowedTypes       []string `json:"allowed_types" yaml:"allowed_types"`
	MaxKeyLength       int      `json:"max_key_length" yaml:"max_key_length"`
	OnlyPositiveValues bool     `json:"only_positive_values" yaml:"only_positive_values"`
	PaginationLimit    int      `json:"pagination_limit" yaml:"pagination_limit"`
	MaxPaginationLimit int      `json:"max_pagination_limit" yaml:"max_pagination_limit"`
}

func DefaultConfig

func DefaultConfig() Config

func (*Config) IsAllowedType

func (c *Config) IsAllowedType(resourceType string) bool

func (*Config) Validate

func (c *Config) Validate() error

type Metric

type Metric struct {
	Id         string     `json:"id,omitempty" db:"id"`
	Resource   string     `json:"resource" db:"resource"`
	ResourceId string     `json:"resourceId" db:"resource_id"`
	Key        string     `json:"key" db:"name"`
	Value      int        `json:"value" db:"value"`
	CreatedAt  *time.Time `json:"createdAt,omitempty" db:"created_at"`
}

func (Metric) TableName

func (Metric) TableName() string

type MetricConverter added in v0.1.5

type MetricConverter struct{}

func (*MetricConverter) CreateDTOToModel added in v0.1.5

func (c *MetricConverter) CreateDTOToModel(dto MetricCreateDTO) Metric

func (*MetricConverter) ModelToResponseDTO added in v0.1.5

func (c *MetricConverter) ModelToResponseDTO(model Metric) MetricResponseDTO

func (*MetricConverter) ModelsToResponseDTOs added in v0.1.5

func (c *MetricConverter) ModelsToResponseDTOs(models []Metric) []MetricResponseDTO

func (*MetricConverter) UpdateDTOToModel added in v0.1.5

func (c *MetricConverter) UpdateDTOToModel(dto MetricUpdateDTO) Metric

type MetricCreateDTO added in v0.1.5

type MetricCreateDTO struct {
	Resource   string `json:"resource"`
	ResourceId string `json:"resourceId"`
	Key        string `json:"key"`
	Value      int    `json:"value"`
}

type MetricHooks added in v0.1.5

type MetricHooks struct {
	// contains filtered or unexported fields
}

func NewMetricHooks added in v0.1.5

func NewMetricHooks(config *Config) *MetricHooks

func (*MetricHooks) CreateHook added in v0.1.5

func (h *MetricHooks) CreateHook(c fiber.Ctx, dto MetricCreateDTO, model *Metric) error

func (*MetricHooks) GetAllHook added in v0.1.5

func (h *MetricHooks) GetAllHook(c fiber.Ctx, conditions *[]query.Condition, orderBy *[]crud.OrderByClause) error

func (*MetricHooks) UpdateHook added in v0.1.5

func (h *MetricHooks) UpdateHook(c fiber.Ctx, dto MetricUpdateDTO, model *Metric) error

type MetricResource

type MetricResource struct {
	// contains filtered or unexported fields
}

func (*MetricResource) Create

func (r *MetricResource) Create(c fiber.Ctx) error

Create records a metric without blocking on the database: it validates the request synchronously (preserving the 400s the sync path returned) and hands the row to the async batch writer, then answers 201 from the in-memory model.

func (*MetricResource) Delete

func (r *MetricResource) Delete(c fiber.Ctx) error

func (*MetricResource) GetAll added in v0.1.5

func (r *MetricResource) GetAll(c fiber.Ctx) error

func (*MetricResource) GetByID added in v0.1.5

func (r *MetricResource) GetByID(c fiber.Ctx) error

func (*MetricResource) Update

func (r *MetricResource) Update(c fiber.Ctx) error

type MetricResponseDTO added in v0.1.5

type MetricResponseDTO struct {
	ID         string     `json:"id"`
	Resource   string     `json:"resource"`
	ResourceID string     `json:"resourceId"`
	Key        string     `json:"key"`
	Value      int        `json:"value"`
	CreatedAt  *time.Time `json:"createdAt,omitempty"`
}

type MetricUpdateDTO added in v0.1.5

type MetricUpdateDTO struct {
	Value int `json:"value"`
}

type MetricsPlugin

type MetricsPlugin struct {
	// contains filtered or unexported fields
}

func (*MetricsPlugin) Close added in v0.6.0

func (p *MetricsPlugin) Close(ctx context.Context) error

Close flushes any buffered metrics and stops the background writer. Hosts must call it during graceful shutdown (after the HTTP server has drained) so no accepted metric is lost.

func (*MetricsPlugin) Dependencies

func (p *MetricsPlugin) Dependencies() []string

func (*MetricsPlugin) GetOpenAPIResources

func (p *MetricsPlugin) GetOpenAPIResources() []plugin.OpenAPIResource

func (*MetricsPlugin) Handler

func (p *MetricsPlugin) Handler() fiber.Handler

func (*MetricsPlugin) Initialize

func (p *MetricsPlugin) Initialize(config map[string]interface{}) error

func (*MetricsPlugin) MigrationDependencies

func (p *MetricsPlugin) MigrationDependencies() []string

func (*MetricsPlugin) MigrationSource

func (p *MetricsPlugin) MigrationSource() interface{}

func (*MetricsPlugin) Name

func (p *MetricsPlugin) Name() string

func (*MetricsPlugin) SetupEndpoints

func (p *MetricsPlugin) SetupEndpoints(router fiber.Router) error

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL