Add go sdk

This commit is contained in:
Oliver
2026-01-06 14:55:21 +01:00
parent 32bcd6db9e
commit e018bedca3
21 changed files with 1090 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Oliver Schlüter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,34 @@
package builtinevents
import (
"log/slog"
"github.com/OliverSchlueter/goutils/sloki"
"github.com/fancyinnovations/fancyanalytics/integrations/go-sdk/client"
)
func ServiceStarted(service string) {
evt := &client.Event{
Name: "ServiceStarted",
Properties: map[string]string{
"service": service,
},
}
if err := client.DefaultClient.SendEvent(evt); err != nil {
slog.Warn("failed to send ServiceStarted event", sloki.WrapError(err))
}
}
func ServiceStopped(service string) {
evt := &client.Event{
Name: "ServiceStopped",
Properties: map[string]string{
"service": service,
},
}
if err := client.DefaultClient.SendEvent(evt); err != nil {
slog.Warn("failed to send ServiceStopped event", sloki.WrapError(err))
}
}

124
client/client.go Normal file
View File

@@ -0,0 +1,124 @@
// Package client provides a client for the FancyAnalytics API.
package client
import (
"net/http"
"time"
"github.com/google/uuid"
)
const (
DefaultBaseURL = "https://fancyanalytics.net"
DefaultMaxIdleConnections = 100
DefaultIdleTimeout = 90 * time.Second
DefaultTimeout = 30 * time.Second
)
var DefaultClient *Client
// Client is a client for the FancyAnalytics API.
type Client struct {
baseURL string
authToken string
senderID string
projectID string
writeKey string
client *http.Client
}
// Configuration holds the configuration for the FancyAnalytics client.
type Configuration struct {
// BaseURL is the base URL of the FancyAnalytics API.
// If empty, DefaultBaseURL is used.
BaseURL string
// SenderID is the sender ID to use for requests.
// If empty a random UUID will be used.
SenderID string
// AuthToken is the authentication token to use for requests. Optional.
AuthToken string
// ProjectID is the project ID to use for requests.
ProjectID string
// WriteKey is the write key for the project. Optional.
WriteKey string
// MaxIdleConnections sets the maximum number of idle (keep-alive) connections across all hosts.
// If zero, DefaultMaxIdleConnections is used.
MaxIdleConnections int
// IdleConnectionTimeout is the maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.
// If zero, DefaultIdleTimeout is used.
IdleConnectionTimeout time.Duration
// RequestTimeout is the maximum amount of time a request can take before timing out.
// If zero, DefaultTimeout is used.
RequestTimeout time.Duration
}
// New creates a new FancyAnalytics client with the given configuration.
func New(cfg Configuration) (*Client, error) {
if cfg.ProjectID == "" {
return nil, ErrNoProjectID
}
if cfg.BaseURL == "" {
cfg.BaseURL = DefaultBaseURL
}
if cfg.SenderID == "" {
cfg.SenderID = uuid.New().String()
}
if cfg.MaxIdleConnections == 0 {
cfg.MaxIdleConnections = DefaultMaxIdleConnections
}
if cfg.IdleConnectionTimeout == 0 {
cfg.IdleConnectionTimeout = DefaultIdleTimeout
}
if cfg.RequestTimeout == 0 {
cfg.RequestTimeout = DefaultTimeout
}
transport := http.Transport{
MaxIdleConns: cfg.MaxIdleConnections,
IdleConnTimeout: cfg.IdleConnectionTimeout,
DisableKeepAlives: false,
DisableCompression: false,
}
c := &http.Client{
Transport: &transport,
Timeout: cfg.RequestTimeout,
}
return &Client{
baseURL: cfg.BaseURL,
authToken: cfg.AuthToken,
projectID: cfg.ProjectID,
writeKey: cfg.WriteKey,
senderID: cfg.SenderID,
client: c,
}, nil
}
// Ping checks if the FancyAnalytics API is reachable.
func (c *Client) Ping() error {
resp, err := c.client.Get(c.baseURL + "/collector/api/v1/ping")
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ErrUnexpectedStatusCode
}
return nil
}

151
client/client_test.go Normal file
View File

@@ -0,0 +1,151 @@
package client
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestNew(t *testing.T) {
for _, tc := range []struct {
name string
cfg Configuration
exp *Client
expErr error
}{
{
name: "default values",
cfg: Configuration{
AuthToken: "my-token",
ProjectID: "my-project",
},
expErr: nil,
},
{
name: "all values set",
cfg: Configuration{
BaseURL: "https://custom-url.com/",
AuthToken: "my-token",
ProjectID: "my-project",
SenderID: "custom-sender-id",
MaxIdleConnections: 50,
IdleConnectionTimeout: 60 * time.Second,
RequestTimeout: 15 * time.Second,
},
expErr: nil,
},
{
name: "missing project ID",
cfg: Configuration{
AuthToken: "my-token",
},
expErr: ErrNoProjectID,
},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := New(tc.cfg)
if !errors.Is(err, tc.expErr) {
t.Errorf("expected error %v, got %v", tc.expErr, err)
}
// If an error was expected, no need to check further.
if tc.expErr != nil {
return
}
if tc.cfg.BaseURL == "" {
if got.baseURL != DefaultBaseURL {
t.Errorf("expected baseURL %s, got %s", DefaultBaseURL, got.baseURL)
}
} else if got.baseURL != tc.cfg.BaseURL {
t.Errorf("expected baseURL %s, got %s", tc.cfg.BaseURL, got.baseURL)
}
if got.authToken != tc.cfg.AuthToken {
t.Errorf("expected authToken %s, got %s", tc.cfg.AuthToken, got.authToken)
}
if got.projectID != tc.cfg.ProjectID {
t.Errorf("expected projectID %s, got %s", tc.cfg.ProjectID, got.projectID)
}
if tc.cfg.SenderID == "" {
if got.senderID == "" {
t.Error("expected non-empty senderID, got empty")
}
} else if got.senderID != tc.cfg.SenderID {
t.Errorf("expected senderID %s, got %s", tc.cfg.SenderID, got.senderID)
}
if tc.cfg.MaxIdleConnections == 0 {
if got.client.Transport.(*http.Transport).MaxIdleConns != DefaultMaxIdleConnections {
t.Errorf("expected MaxIdleConnections %d, got %d", DefaultMaxIdleConnections, got.client.Transport.(*http.Transport).MaxIdleConns)
}
} else if got.client.Transport.(*http.Transport).MaxIdleConns != tc.cfg.MaxIdleConnections {
t.Errorf("expected MaxIdleConnections %d, got %d", tc.cfg.MaxIdleConnections, got.client.Transport.(*http.Transport).MaxIdleConns)
}
})
}
}
func TestClient_Ping(t *testing.T) {
for _, tc := range []struct {
name string
statusCode int
serverError bool
expErr error
}{
{
name: "successful ping",
statusCode: http.StatusOK,
expErr: nil,
},
{
name: "unexpected status code",
statusCode: http.StatusInternalServerError,
expErr: ErrUnexpectedStatusCode,
},
{
name: "network error",
serverError: true,
expErr: errors.New("network error"),
},
} {
t.Run(tc.name, func(t *testing.T) {
var server *httptest.Server
if tc.serverError {
// Simulate network error by using a closed server
server = httptest.NewServer(http.NotFoundHandler())
server.Close()
} else {
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tc.statusCode)
}))
}
client, err := New(Configuration{
BaseURL: server.URL,
AuthToken: "test-token",
ProjectID: "test-project",
})
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
err = client.Ping()
if tc.expErr == nil {
if err != nil {
t.Errorf("expected no error, got %v", err)
}
} else if tc.serverError {
if err == nil {
t.Errorf("expected network error, got nil")
}
} else if !errors.Is(err, tc.expErr) {
t.Errorf("expected error %v, got %v", tc.expErr, err)
}
})
}
}

8
client/errors.go Normal file
View File

@@ -0,0 +1,8 @@
package client
import "errors"
var (
ErrNoProjectID = errors.New("no project ID provided")
ErrUnexpectedStatusCode = errors.New("unexpected status code")
)

46
client/events.go Normal file
View File

@@ -0,0 +1,46 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
func (c *Client) SendEvent(evt *Event) error {
dto := createEventDTO{
ProjectID: c.projectID,
Name: evt.Name,
Timestamp: time.Now(),
Properties: evt.Properties,
WriteKey: c.writeKey,
}
body, err := json.Marshal(dto)
if err != nil {
return fmt.Errorf("could not marshal event: %w", err)
}
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/collector/api/v1/events", bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("could not create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if c.authToken != "" {
req.Header.Set("Authorization", "Bearer "+c.authToken)
}
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("could not send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ErrUnexpectedStatusCode
}
return nil
}

74
client/events_test.go Normal file
View File

@@ -0,0 +1,74 @@
package client
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
)
func TestClient_SendEvent(t *testing.T) {
for _, tc := range []struct {
name string
statusCode int
serverError bool
expErr error
}{
{
name: "successful event send",
statusCode: http.StatusOK,
expErr: nil,
},
{
name: "unexpected status code",
statusCode: http.StatusBadRequest,
expErr: ErrUnexpectedStatusCode,
},
{
name: "network error",
serverError: true,
expErr: errors.New("network error"),
},
} {
t.Run(tc.name, func(t *testing.T) {
var server *httptest.Server
if tc.serverError {
server = httptest.NewServer(http.NotFoundHandler())
server.Close()
} else {
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tc.statusCode)
}))
}
c, err := New(Configuration{
BaseURL: server.URL,
AuthToken: "token",
ProjectID: "project",
})
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
e := &Event{
Name: "test_event",
Properties: map[string]string{
"key": "value",
},
}
err = c.SendEvent(e)
if tc.expErr == nil {
if err != nil {
t.Errorf("expected no error, got %v", err)
}
} else if tc.serverError {
if err == nil {
t.Errorf("expected network error, got nil")
}
} else if !errors.Is(err, tc.expErr) {
t.Errorf("expected error %v, got %v", tc.expErr, err)
}
})
}
}

38
client/model.go Normal file
View File

@@ -0,0 +1,38 @@
package client
import "time"
type Event struct {
Name string `json:"name"`
Properties map[string]string `json:"properties"`
}
func (e *Event) WithProperty(key, value string) {
if e.Properties == nil {
e.Properties = make(map[string]string)
}
e.Properties[key] = value
}
type createEventDTO struct {
ProjectID string `json:"project_id"`
Name string `json:"name"`
Timestamp time.Time `json:"timestamp,omitempty"`
Properties map[string]string `json:"properties"`
WriteKey string `json:"write_key,omitempty"`
}
type RecordData struct {
Metric string `json:"metric"`
Label string `json:"label,omitempty"`
Value float64 `json:"value"`
}
type createMetricRecordDto struct {
SenderID string `json:"sender_id"`
ProjectID string `json:"project_id"`
Timestamp int64 `json:"timestamp,omitempty"`
WriteKey string `json:"write_key,omitempty"`
Data []RecordData `json:"data"`
}

46
client/records.go Normal file
View File

@@ -0,0 +1,46 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
func (c *Client) SendRecord(records []RecordData) error {
dto := createMetricRecordDto{
SenderID: c.senderID,
ProjectID: c.projectID,
Timestamp: time.Now().Unix(),
WriteKey: c.writeKey,
Data: records,
}
body, err := json.Marshal(dto)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/collector/api/v1/records", bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("could not create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if c.authToken != "" {
req.Header.Set("Authorization", "Bearer "+c.authToken)
}
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("could not send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ErrUnexpectedStatusCode
}
return nil
}

8
go.mod Normal file
View File

@@ -0,0 +1,8 @@
module git.fancyinnovations.com/fancyanalytics/go-sdk
go 1.25.5
require (
github.com/OliverSchlueter/goutils v0.0.24
github.com/google/uuid v1.6.0
)

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/OliverSchlueter/goutils v0.0.24/go.mod h1:Kf8k9AaaATGm6D/IVJkIfVZ4sUTXzHfy/cLQdpejFXE=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

233
httpmetrics/httpmetrics.go Normal file
View File

@@ -0,0 +1,233 @@
package httpmetrics
import (
"net/http"
"sync"
"time"
"github.com/fancyinnovations/fancyanalytics/integrations/go-sdk/client"
)
type Service struct {
lastReset time.Time
statusCodes map[string]int64
requestSizes []int64
responseSizes []int64
durations []int64
mu sync.Mutex
}
func NewService() *Service {
s := &Service{}
s.resetMetrics()
return s
}
func (s *Service) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
sr := &StatusRecorder{
ResponseWriter: w,
Status: http.StatusOK,
}
next.ServeHTTP(sr, r)
duration := time.Since(startTime).Milliseconds()
s.mu.Lock()
defer s.mu.Unlock()
s.durations = append(s.durations, duration)
s.requestSizes = append(s.requestSizes, r.ContentLength)
s.responseSizes = append(s.responseSizes, sr.Size)
statusCodeCategory := sr.Status / 100
switch statusCodeCategory {
case 2:
s.statusCodes["2xx"]++
case 3:
s.statusCodes["3xx"]++
case 4:
s.statusCodes["4xx"]++
case 5:
s.statusCodes["5xx"]++
}
})
}
func (s *Service) collectAndReset() RequestMetrics {
s.mu.Lock()
defer s.mu.Unlock()
// duration stats
var totalDuration int64
var minDuration int64 = -1
var maxDuration int64 = -1
for _, d := range s.durations {
totalDuration += d
if minDuration == -1 || d < minDuration {
minDuration = d
}
if d > maxDuration {
maxDuration = d
}
}
var avgDuration float64
if len(s.durations) > 0 {
avgDuration = float64(totalDuration) / float64(len(s.durations))
}
// request size stats
var totalRequestSize int64
var minRequestSize int64 = -1
var maxRequestSize int64 = -1
for _, rs := range s.requestSizes {
totalRequestSize += rs
if minRequestSize == -1 || rs < minRequestSize {
minRequestSize = rs
}
if rs > maxRequestSize {
maxRequestSize = rs
}
}
var avgRequestSize float64
if len(s.requestSizes) > 0 {
avgRequestSize = float64(totalRequestSize) / float64(len(s.requestSizes))
}
// response size stats
var totalResponseSize int64
var minResponseSize int64 = -1
var maxResponseSize int64 = -1
for _, rs := range s.responseSizes {
totalResponseSize += rs
if minResponseSize == -1 || rs < minResponseSize {
minResponseSize = rs
}
if rs > maxResponseSize {
maxResponseSize = rs
}
}
var avgResponseSize float64
if len(s.responseSizes) > 0 {
avgResponseSize = float64(totalResponseSize) / float64(len(s.responseSizes))
}
// requests per second
rps := float64(len(s.durations)) / time.Since(s.lastReset).Seconds()
metrics := RequestMetrics{
RequestsPerSecond: rps,
StatusCodes: s.statusCodes,
}
metrics.Durations.Min = minDuration
metrics.Durations.Max = maxDuration
metrics.Durations.Avg = avgDuration
metrics.RequestSizes.Min = minRequestSize
metrics.RequestSizes.Max = maxRequestSize
metrics.RequestSizes.Avg = avgRequestSize
metrics.ResponseSizes.Min = minResponseSize
metrics.ResponseSizes.Max = maxResponseSize
metrics.ResponseSizes.Avg = avgResponseSize
s.resetMetrics()
return metrics
}
func (s *Service) resetMetrics() {
s.statusCodes = map[string]int64{
"2xx": 0,
"3xx": 0,
"4xx": 0,
"5xx": 0,
}
s.durations = []int64{}
s.requestSizes = []int64{}
s.responseSizes = []int64{}
s.lastReset = time.Now()
}
func (s *Service) MetricProvider() ([]client.RecordData, error) {
metrics := s.collectAndReset()
records := []client.RecordData{
{
Metric: "requests_per_second",
Label: "",
Value: metrics.RequestsPerSecond,
},
}
records = append(records,
client.RecordData{
Metric: "min_request_duration",
Label: "",
Value: float64(metrics.Durations.Min),
},
client.RecordData{
Metric: "max_request_duration",
Label: "",
Value: float64(metrics.Durations.Max),
},
client.RecordData{
Metric: "avg_request_duration",
Label: "",
Value: metrics.Durations.Avg,
},
)
records = append(records,
client.RecordData{
Metric: "min_request_size",
Label: "",
Value: float64(metrics.RequestSizes.Min),
},
client.RecordData{
Metric: "max_request_size",
Label: "",
Value: float64(metrics.RequestSizes.Max),
},
client.RecordData{
Metric: "avg_request_size",
Label: "",
Value: metrics.RequestSizes.Avg,
},
)
records = append(records,
client.RecordData{
Metric: "min_response_size",
Label: "",
Value: float64(metrics.ResponseSizes.Min),
},
client.RecordData{
Metric: "max_response_size",
Label: "",
Value: float64(metrics.ResponseSizes.Max),
},
client.RecordData{
Metric: "avg_response_size",
Label: "",
Value: metrics.ResponseSizes.Avg,
},
)
for status, count := range metrics.StatusCodes {
records = append(records, client.RecordData{
Metric: "status_code_count",
Label: status,
Value: float64(count),
})
}
return records, nil
}

34
httpmetrics/model.go Normal file
View File

@@ -0,0 +1,34 @@
package httpmetrics
import "net/http"
type StatusRecorder struct {
http.ResponseWriter
Status int
Size int64
}
func (s *StatusRecorder) WriteHeader(code int) {
s.Status = code
s.ResponseWriter.WriteHeader(code)
}
func (s *StatusRecorder) Write(b []byte) (int, error) {
n, err := s.ResponseWriter.Write(b)
s.Size += int64(n)
return n, err
}
type RequestMetrics struct {
RequestsPerSecond float64
StatusCodes map[string]int64
Durations Stats
RequestSizes Stats
ResponseSizes Stats
}
type Stats struct {
Min int64
Max int64
Avg float64
}

View File

@@ -0,0 +1,91 @@
package metriccollector
import (
"log/slog"
"time"
"github.com/OliverSchlueter/goutils/sloki"
"github.com/fancyinnovations/fancyanalytics/integrations/go-sdk/client"
)
type Service struct {
c *client.Client
interval time.Duration
providers []MetricProvider
schedulerStarted bool
abortScheduler chan struct{}
}
type Configuration struct {
Client *client.Client
Interval time.Duration
Providers []MetricProvider
}
func NewService(cfg *Configuration) *Service {
if cfg.Interval <= 0 {
cfg.Interval = 60
}
if cfg.Providers == nil {
cfg.Providers = []MetricProvider{}
}
return &Service{
c: cfg.Client,
interval: cfg.Interval,
providers: cfg.Providers,
schedulerStarted: false,
abortScheduler: make(chan struct{}),
}
}
func (s *Service) AddProvider(provider MetricProvider) {
s.providers = append(s.providers, provider)
}
func (s *Service) Send() error {
var records []client.RecordData
for _, provider := range s.providers {
providerRecords, err := provider()
if err != nil {
continue
}
records = append(records, providerRecords...)
}
if err := s.c.SendRecord(records); err != nil {
return err
}
return nil
}
func (s *Service) StartScheduler() {
if s.schedulerStarted {
return
}
s.schedulerStarted = true
go func() {
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := s.Send(); err != nil {
slog.Warn("failed to send metrics", sloki.WrapError(err))
}
case <-s.abortScheduler:
return
}
}
}()
}
func (s *Service) StopScheduler() {
s.abortScheduler <- struct{}{}
s.schedulerStarted = false
}

5
metriccollector/model.go Normal file
View File

@@ -0,0 +1,5 @@
package metriccollector
import "github.com/fancyinnovations/fancyanalytics/integrations/go-sdk/client"
type MetricProvider func() ([]client.RecordData, error)

26
performancemetrics/cpu.go Normal file
View File

@@ -0,0 +1,26 @@
package performancemetrics
import (
"os"
"syscall"
)
func getCpuTime() (float64, error) {
_, err := os.FindProcess(os.Getpid())
if err != nil {
return 0, err
}
// Use syscall to get CPU times
var ru syscall.Rusage
err = syscall.Getrusage(syscall.RUSAGE_SELF, &ru)
if err != nil {
return 0, err
}
// Total CPU time in seconds
userSec := float64(ru.Utime.Sec) + float64(ru.Utime.Usec)/1e6
sysSec := float64(ru.Stime.Sec) + float64(ru.Stime.Usec)/1e6
return userSec + sysSec, nil
}

View File

@@ -0,0 +1,16 @@
package performancemetrics
import "syscall"
func getDiskUsage(path string) (used, free, total uint64, err error) {
var stat syscall.Statfs_t
err = syscall.Statfs(path, &stat)
if err != nil {
return
}
total = stat.Blocks * uint64(stat.Bsize)
free = stat.Bfree * uint64(stat.Bsize)
used = total - free
return
}

View File

@@ -0,0 +1,9 @@
package performancemetrics
import "runtime"
func getAllocatedMemory() uint64 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.Alloc
}

View File

@@ -0,0 +1,15 @@
package performancemetrics
type PerformanceMetrics struct {
Uptime int64
CpuUsage float64
AllocatedMemory uint64
GoroutineCount int
DiskUsage DiskUsage
}
type DiskUsage struct {
Used uint64
Free uint64
Total uint64
}

View File

@@ -0,0 +1,108 @@
package performancemetrics
import (
"runtime"
"time"
"github.com/fancyinnovations/fancyanalytics/integrations/go-sdk/client"
)
type Service struct {
appStartTime time.Time
lastReset time.Time
}
func NewService() *Service {
s := &Service{
appStartTime: time.Now(),
}
s.resetMetrics()
return s
}
func (s *Service) resetMetrics() {
s.lastReset = time.Now()
}
func (s *Service) collectAndReset() (*PerformanceMetrics, error) {
cpuTime, err := getCpuTime()
if err != nil {
return nil, err
}
cpuUsage := cpuTime / time.Since(s.lastReset).Seconds()
goroutineCount := runtime.NumGoroutine()
allocedMemory := getAllocatedMemory()
usedDisk, freeDisk, totalDisk, err := getDiskUsage("/")
if err != nil {
return nil, err
}
uptimeMs := time.Since(s.appStartTime).Milliseconds()
metrics := &PerformanceMetrics{
Uptime: uptimeMs,
CpuUsage: cpuUsage,
AllocatedMemory: allocedMemory,
GoroutineCount: goroutineCount,
DiskUsage: DiskUsage{
Used: usedDisk,
Free: freeDisk,
Total: totalDisk,
},
}
s.resetMetrics()
return metrics, nil
}
func (s *Service) MetricProvider() ([]client.RecordData, error) {
metrics, err := s.collectAndReset()
if err != nil {
return nil, err
}
records := []client.RecordData{
{
Metric: "uptime",
Label: "",
Value: float64(metrics.Uptime),
},
{
Metric: "cpu_usage",
Label: "",
Value: metrics.CpuUsage,
},
{
Metric: "allocated_memory",
Label: "",
Value: float64(metrics.AllocatedMemory),
},
{
Metric: "goroutine_count",
Label: "",
Value: float64(metrics.GoroutineCount),
},
{
Metric: "disk_usage_total",
Label: "",
Value: float64(metrics.DiskUsage.Total),
},
{
Metric: "disk_usage",
Label: "free",
Value: float64(metrics.DiskUsage.Free),
},
{
Metric: "disk_usage",
Label: "used",
Value: float64(metrics.DiskUsage.Used),
},
}
return records, nil
}