diff --git a/internal/metrics/collector.go b/internal/metrics/collector.go index 954754d..7a84495 100644 --- a/internal/metrics/collector.go +++ b/internal/metrics/collector.go @@ -6,6 +6,7 @@ import ( "friendica-exporter/serverinfo" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "strconv" ) const ( @@ -97,6 +98,30 @@ var ( metricPrefix+"reports_closed", "Number of closed reports.", nil, nil) + serverInfoDesc = prometheus.NewDesc( + metricPrefix+"server_info", + "Contains meta information about Server as labels. Value is always 1.", + []string{"version"}, nil) + phpInfoDesc = prometheus.NewDesc( + metricPrefix+"php_info", + "Contains meta information about PHP as labels. Value is always 1.", + []string{"version"}, nil) + phpMemoryLimitDesc = prometheus.NewDesc( + metricPrefix+"php_memory_limit_bytes", + "Configured PHP memory limit in bytes.", + nil, nil) + phpMaxUploadSizeDesc = prometheus.NewDesc( + metricPrefix+"php_upload_max_size_bytes", + "Configured maximum upload size in bytes.", + nil, nil) + phpMaxPostSizeDesc = prometheus.NewDesc( + metricPrefix+"php_post_max_size_bytes", + "Configured maximum post size in bytes.", + nil, nil) + databaseMaxAllowedPacketDesc = prometheus.NewDesc( + metricPrefix+"php_post_max_allowed_packet", + "Configured maximum allowed packet length to send to or receive from the server.", + nil, nil) ) type friendicaCollector struct { @@ -148,6 +173,12 @@ func (c *friendicaCollector) Describe(ch chan<- *prometheus.Desc) { ch <- reportsNewest ch <- reportsOpen ch <- reportsClosed + ch <- serverInfoDesc + ch <- phpInfoDesc + ch <- phpMaxUploadSizeDesc + ch <- phpMaxPostSizeDesc + ch <- phpMemoryLimitDesc + ch <- databaseMaxAllowedPacketDesc } func (c *friendicaCollector) Collect(ch chan<- prometheus.Metric) { @@ -212,6 +243,10 @@ func readMetrics(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) err return err } + if err := collectPhpMetrics(ch, status); err != nil { + return err + } + return nil } @@ -532,3 +567,60 @@ func collectPacketsPerDirection(ch chan<- prometheus.Metric, status *serverinfo. return nil } + +func collectPhpMetrics(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error { + + metric, err := prometheus.NewConstMetric(phpInfoDesc, prometheus.GaugeValue, 1, status.Server.PHP.Version) + if err != nil { + return fmt.Errorf("error creating metric for %s: %w", phpInfoDesc, err) + } + ch <- metric + + metric, err = prometheus.NewConstMetric(serverInfoDesc, prometheus.GaugeValue, 1, status.Server.Version) + if err != nil { + return fmt.Errorf("error creating metric for %s: %w", serverInfoDesc, err) + } + ch <- metric + + metricVal, err := strconv.ParseFloat(status.Server.Database.MaxAllowedPacket, 64) + if err != nil { + return fmt.Errorf("error converting value to byte for %s: %w", databaseMaxAllowedPacketDesc, err) + } + metric, err = prometheus.NewConstMetric(databaseMaxAllowedPacketDesc, prometheus.GaugeValue, metricVal) + if err != nil { + return fmt.Errorf("error creating metric for %s: %w", databaseMaxAllowedPacketDesc, err) + } + ch <- metric + + metrics := []struct { + desc *prometheus.Desc + value string + }{ + { + desc: phpMemoryLimitDesc, + value: status.Server.PHP.MemoryLimit, + }, + { + desc: phpMaxPostSizeDesc, + value: status.Server.PHP.PostMaxSize, + }, + { + desc: phpMaxUploadSizeDesc, + value: status.Server.PHP.UploadMaxFilesize, + }, + } + + for _, m := range metrics { + metricVal, err := ConvertMemoryToBytes(m.value) + if err != nil { + return fmt.Errorf("error converting value to byte for %s: %w", m.desc, err) + } + metric, err = prometheus.NewConstMetric(m.desc, prometheus.GaugeValue, float64(metricVal)) + if err != nil { + return fmt.Errorf("error creating metric for %s: %w", m.desc, err) + } + ch <- metric + } + + return nil +} diff --git a/internal/metrics/converter.go b/internal/metrics/converter.go new file mode 100644 index 0000000..850b999 --- /dev/null +++ b/internal/metrics/converter.go @@ -0,0 +1,51 @@ +package metrics + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +// ConvertMemoryToBytes converts a memory limit string into bytes. +// Supported units are: K (Kilobytes), M (Megabytes), G (Gigabytes), case-insensitive. +// Anything else is interpreted as bytes. +func ConvertMemoryToBytes(memory string) (uint64, error) { + // Trim whitespace + memory = strings.TrimSpace(memory) + + // Regular expression to match the pattern (number + optional unit) + re := regexp.MustCompile(`(?i)^([\d.]+)([KMG]?)$`) + matches := re.FindStringSubmatch(memory) + if matches == nil { + return 0, errors.New("invalid memory format") + } + + // Extract the numeric part and unit + numberStr := matches[1] + unit := strings.ToUpper(matches[2]) + + // Parse the number, truncate fractional parts as PHP does + number, err := strconv.ParseFloat(numberStr, 64) + if err != nil { + return 0, fmt.Errorf("invalid numeric value: %v", err) + } + numberInt := uint64(number) // Truncate fractional part by casting to uint64 + + // Convert to bytes based on the unit + var multiplier uint64 + switch unit { + case "K": + multiplier = 1024 // Kilobytes + case "M": + multiplier = 1024 * 1024 // Megabytes + case "G": + multiplier = 1024 * 1024 * 1024 // Gigabytes + default: + multiplier = 1 // Bytes + } + + // Calculate the result + return numberInt * multiplier, nil +} diff --git a/internal/metrics/converter_test.go b/internal/metrics/converter_test.go new file mode 100644 index 0000000..ba906c6 --- /dev/null +++ b/internal/metrics/converter_test.go @@ -0,0 +1,124 @@ +package metrics + +import ( + "errors" + "friendica-exporter/internal/testutil" + "testing" +) + +func TestConvertMemoryToBytes(t *testing.T) { + tt := []struct { + desc string + memory string + wantErr error + wantEqual uint64 + }{ + { + desc: "1 kilobyte (upper case)", + memory: "1K", + wantErr: nil, + wantEqual: 1024, + }, + { + desc: "1 kilobyte (lower case)", + memory: "1k", + wantErr: nil, + wantEqual: 1024, + }, + { + desc: "1 megabyte (upper case)", + memory: "1M", + wantErr: nil, + wantEqual: 1048576, + }, + { + desc: "1 megabyte (lower case)", + memory: "1m", + wantErr: nil, + wantEqual: 1048576, + }, + { + desc: "1 gigabyte (upper case)", + memory: "1G", + wantErr: nil, + wantEqual: 1073741824, + }, + { + desc: "1 gigabyte (lower case)", + memory: "1g", + wantErr: nil, + wantEqual: 1073741824, + }, + { + desc: "2 Kilobytes", + memory: "2K", + wantErr: nil, + wantEqual: 2048, + }, + { + desc: "5 gigabytes", + memory: "5g", + wantErr: nil, + wantEqual: 5368709120, + }, + { + desc: "0.5 Megabyte", + memory: "0.5M", + wantErr: nil, + wantEqual: 0, + }, + { + desc: "10.7 Kilobytes", + memory: "10.7k", + wantErr: nil, + wantEqual: 10240, + }, + { + desc: "12345234 Bytes", + memory: "12345234", + wantErr: nil, + wantEqual: 12345234, + }, + { + desc: "Invalid", + memory: "invalid", + wantErr: errors.New("invalid memory format"), + wantEqual: 0, + }, + { + desc: "1234X", + memory: "1234X", + wantErr: errors.New("invalid memory format"), + wantEqual: 0, + }, + { + desc: "512 Megabytes", + memory: "512M", + wantErr: nil, + wantEqual: 536870912, + }, + { + desc: "CopyPasteError", + memory: "512M512M", + wantErr: errors.New("invalid memory format"), + wantEqual: 0, + }, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + equal, err := ConvertMemoryToBytes(tc.memory) + + if !testutil.EqualErrorMessage(err, tc.wantErr) { + t.Errorf("got error %q, want %q", err, tc.wantErr) + } + + if equal != tc.wantEqual { + t.Errorf("got equal %v, want %v", equal, tc.wantEqual) + } + }) + } +} diff --git a/serverinfo/serverinfo.go b/serverinfo/serverinfo.go index 1232c73..e6fac74 100644 --- a/serverinfo/serverinfo.go +++ b/serverinfo/serverinfo.go @@ -108,7 +108,7 @@ type Update struct { type PHP struct { Version string `json:"version"` UploadMaxFilesize string `json:"upload_max_filesize"` - PostMaxSize string `json:"post_max_filesize"` + PostMaxSize string `json:"post_max_size"` MemoryLimit string `json:"memory_limit"` }