mirror of
https://github.com/wearepixel/pulsewise-collector.git
synced 2026-11-14 22:12:01 +00:00
feat: add PHP-FPM, nginx, SSL, and port monitoring
All four are opt-in via config file: - FPM_STATUS_URL — PHP-FPM pool stats (active/idle workers, slow requests, max children reached, accepted conn) - NGINX_STATUS_URL — nginx stub_status (active connections, reading/writing/waiting, accepts/handled/requests) - SSL_DOMAINS — TLS cert expiry check per domain (days left, valid, expires_at) - CHECK_PORTS — TCP port availability per port with known service name mapping Also refactors loadConfigFromFile to return *Config directly instead of a positional tuple.
This commit is contained in:
@@ -14,3 +14,19 @@ TOKEN=your-token-here
|
|||||||
|
|
||||||
# Optional — logs full response body from the server
|
# Optional — logs full response body from the server
|
||||||
# DEBUG=true
|
# DEBUG=true
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Optional integrations
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# PHP-FPM status page URL (requires pm.status_path = /status in php-fpm pool config)
|
||||||
|
# FPM_STATUS_URL=http://127.0.0.1/fpm-status
|
||||||
|
|
||||||
|
# Nginx stub_status URL (requires stub_status module and location block)
|
||||||
|
# NGINX_STATUS_URL=http://127.0.0.1/nginx-status
|
||||||
|
|
||||||
|
# SSL certificate expiry checks — comma-separated list of domains
|
||||||
|
# SSL_DOMAINS=example.com,api.example.com
|
||||||
|
|
||||||
|
# TCP port availability checks — comma-separated list of port numbers
|
||||||
|
# CHECK_PORTS=22,80,443,3306,6379
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -24,7 +26,7 @@ import (
|
|||||||
"github.com/shirou/gopsutil/v3/host"
|
"github.com/shirou/gopsutil/v3/host"
|
||||||
"github.com/shirou/gopsutil/v3/load"
|
"github.com/shirou/gopsutil/v3/load"
|
||||||
"github.com/shirou/gopsutil/v3/mem"
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
"github.com/shirou/gopsutil/v3/net"
|
psnet "github.com/shirou/gopsutil/v3/net"
|
||||||
"github.com/shirou/gopsutil/v3/process"
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,6 +68,10 @@ type Config struct {
|
|||||||
Interval time.Duration
|
Interval time.Duration
|
||||||
Debug bool
|
Debug bool
|
||||||
AutoUpdate bool
|
AutoUpdate bool
|
||||||
|
FPMStatusURL string
|
||||||
|
NginxStatusURL string
|
||||||
|
SSLDomains []string
|
||||||
|
CheckPorts []int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
@@ -86,6 +92,10 @@ type Metrics struct {
|
|||||||
Processes []ProcessInfo `json:"processes"`
|
Processes []ProcessInfo `json:"processes"`
|
||||||
TCP *TCPStats `json:"tcp,omitempty"`
|
TCP *TCPStats `json:"tcp,omitempty"`
|
||||||
FileDescriptors *FileDescriptors `json:"file_descriptors,omitempty"`
|
FileDescriptors *FileDescriptors `json:"file_descriptors,omitempty"`
|
||||||
|
FPM *FPMPoolStats `json:"fpm,omitempty"`
|
||||||
|
SSL []SSLCertInfo `json:"ssl,omitempty"`
|
||||||
|
Ports []PortCheck `json:"ports,omitempty"`
|
||||||
|
Nginx *NginxStats `json:"nginx,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemInfo struct {
|
type SystemInfo struct {
|
||||||
@@ -124,6 +134,43 @@ type FileDescriptors struct {
|
|||||||
Max int `json:"max"`
|
Max int `json:"max"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FPMPoolStats struct {
|
||||||
|
Pool string `json:"pool"`
|
||||||
|
ActiveProcesses int `json:"active_processes"`
|
||||||
|
IdleProcesses int `json:"idle_processes"`
|
||||||
|
TotalProcesses int `json:"total_processes"`
|
||||||
|
MaxActiveProcesses int `json:"max_active_processes"`
|
||||||
|
MaxChildrenReached int `json:"max_children_reached"`
|
||||||
|
ListenQueue int `json:"listen_queue"`
|
||||||
|
MaxListenQueue int `json:"max_listen_queue"`
|
||||||
|
SlowRequests int `json:"slow_requests"`
|
||||||
|
AcceptedConn int `json:"accepted_conn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SSLCertInfo struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
DaysLeft int `json:"days_left"`
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PortCheck struct {
|
||||||
|
Port int `json:"port"`
|
||||||
|
Open bool `json:"open"`
|
||||||
|
Service string `json:"service,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NginxStats struct {
|
||||||
|
ActiveConnections int `json:"active_connections"`
|
||||||
|
Reading int `json:"reading"`
|
||||||
|
Writing int `json:"writing"`
|
||||||
|
Waiting int `json:"waiting"`
|
||||||
|
Accepts int `json:"accepts"`
|
||||||
|
Handled int `json:"handled"`
|
||||||
|
Requests int `json:"requests"`
|
||||||
|
}
|
||||||
|
|
||||||
type MemoryMetrics struct {
|
type MemoryMetrics struct {
|
||||||
Total uint64 `json:"total"`
|
Total uint64 `json:"total"`
|
||||||
Available uint64 `json:"available"`
|
Available uint64 `json:"available"`
|
||||||
@@ -370,24 +417,24 @@ func runStatusCommand() {
|
|||||||
fmt.Printf("\n%spulsewise-collector%s v%s\n\n", ansiBold, ansiReset, version)
|
fmt.Printf("\n%spulsewise-collector%s v%s\n\n", ansiBold, ansiReset, version)
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
token, hostname, url, _, autoUpdate, err := loadConfigFromFile()
|
cfg, err := loadConfigFromFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s Not configured — %v\n\n", ansiRed, ansiReset)
|
fmt.Printf("%s Not configured — %v\n\n", ansiRed, ansiReset)
|
||||||
} else {
|
} else {
|
||||||
maskedToken := token
|
maskedToken := cfg.Token
|
||||||
if len(token) >= 8 {
|
if len(cfg.Token) >= 8 {
|
||||||
maskedToken = token[:4] + "..." + token[len(token)-4:]
|
maskedToken = cfg.Token[:4] + "..." + cfg.Token[len(cfg.Token)-4:]
|
||||||
} else {
|
} else {
|
||||||
maskedToken = "****"
|
maskedToken = "****"
|
||||||
}
|
}
|
||||||
autoUpdateStr := ansiGreen + "enabled" + ansiReset
|
autoUpdateStr := ansiGreen + "enabled" + ansiReset
|
||||||
if !autoUpdate {
|
if !cfg.AutoUpdate {
|
||||||
autoUpdateStr = ansiDim + "disabled" + ansiReset
|
autoUpdateStr = ansiDim + "disabled" + ansiReset
|
||||||
}
|
}
|
||||||
fmt.Printf("%sCollector%s\n", ansiBold, ansiReset)
|
fmt.Printf("%sCollector%s\n", ansiBold, ansiReset)
|
||||||
fmt.Printf("%s %s\n", label("Hostname"), hostname)
|
fmt.Printf("%s %s\n", label("Hostname"), cfg.Hostname)
|
||||||
fmt.Printf("%s %s\n", label("Token"), maskedToken)
|
fmt.Printf("%s %s\n", label("Token"), maskedToken)
|
||||||
fmt.Printf("%s %s\n", label("API endpoint"), url)
|
fmt.Printf("%s %s\n", label("API endpoint"), cfg.URL)
|
||||||
fmt.Printf("%s %s\n", label("Interval"), collectionInterval)
|
fmt.Printf("%s %s\n", label("Interval"), collectionInterval)
|
||||||
fmt.Printf("%s %s\n", label("Auto-update"), autoUpdateStr)
|
fmt.Printf("%s %s\n", label("Auto-update"), autoUpdateStr)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -717,22 +764,14 @@ func execSelf() {
|
|||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func loadConfig() *Config {
|
func loadConfig() *Config {
|
||||||
token, hostname, url, debug, autoUpdate, err := loadConfigFromFile()
|
cfg, err := loadConfigFromFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
return cfg
|
||||||
return &Config{
|
|
||||||
Token: token,
|
|
||||||
Hostname: hostname,
|
|
||||||
URL: url,
|
|
||||||
Interval: collectionInterval,
|
|
||||||
Debug: debug,
|
|
||||||
AutoUpdate: autoUpdate,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfigFromFile() (string, string, string, bool, bool, error) {
|
func loadConfigFromFile() (*Config, error) {
|
||||||
configPath := configFilePath
|
configPath := configFilePath
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
@@ -747,13 +786,15 @@ func loadConfigFromFile() (string, string, string, bool, bool, error) {
|
|||||||
|
|
||||||
file, err := os.Open(configPath)
|
file, err := os.Open(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", false, false, fmt.Errorf("config file not found at %s: %w", configPath, err)
|
return nil, fmt.Errorf("config file not found at %s: %w", configPath, err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
var token, hostname, url string
|
cfg := &Config{
|
||||||
var debug bool
|
URL: defaultURL,
|
||||||
autoUpdate := true
|
Interval: collectionInterval,
|
||||||
|
AutoUpdate: true,
|
||||||
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
@@ -761,46 +802,54 @@ func loadConfigFromFile() (string, string, string, bool, bool, error) {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(line, "TOKEN=") {
|
key, val, ok := strings.Cut(line, "=")
|
||||||
token = strings.TrimPrefix(line, "TOKEN=")
|
if !ok {
|
||||||
token = strings.Trim(token, `"'`)
|
continue
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(line, "HOSTNAME=") {
|
|
||||||
hostname = strings.TrimPrefix(line, "HOSTNAME=")
|
|
||||||
hostname = strings.Trim(hostname, `"'`)
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(line, "URL=") {
|
|
||||||
url = strings.TrimPrefix(line, "URL=")
|
|
||||||
url = strings.Trim(url, `"'`)
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(line, "DEBUG=") {
|
|
||||||
val := strings.TrimPrefix(line, "DEBUG=")
|
|
||||||
val = strings.Trim(val, `"'`)
|
val = strings.Trim(val, `"'`)
|
||||||
debug = val == "true" || val == "1"
|
switch key {
|
||||||
|
case "TOKEN":
|
||||||
|
cfg.Token = val
|
||||||
|
case "HOSTNAME":
|
||||||
|
cfg.Hostname = val
|
||||||
|
case "URL":
|
||||||
|
cfg.URL = val
|
||||||
|
case "DEBUG":
|
||||||
|
cfg.Debug = val == "true" || val == "1"
|
||||||
|
case "AUTO_UPDATE":
|
||||||
|
cfg.AutoUpdate = val != "false" && val != "0"
|
||||||
|
case "FPM_STATUS_URL":
|
||||||
|
cfg.FPMStatusURL = val
|
||||||
|
case "NGINX_STATUS_URL":
|
||||||
|
cfg.NginxStatusURL = val
|
||||||
|
case "SSL_DOMAINS":
|
||||||
|
for _, d := range strings.Split(val, ",") {
|
||||||
|
if d = strings.TrimSpace(d); d != "" {
|
||||||
|
cfg.SSLDomains = append(cfg.SSLDomains, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "CHECK_PORTS":
|
||||||
|
for _, p := range strings.Split(val, ",") {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if port, err := strconv.Atoi(p); err == nil && port > 0 && port < 65536 {
|
||||||
|
cfg.CheckPorts = append(cfg.CheckPorts, port)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(line, "AUTO_UPDATE=") {
|
|
||||||
val := strings.TrimPrefix(line, "AUTO_UPDATE=")
|
|
||||||
val = strings.Trim(val, `"'`)
|
|
||||||
autoUpdate = val != "false" && val != "0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return "", "", "", false, false, fmt.Errorf("error reading config file: %w", err)
|
return nil, fmt.Errorf("error reading config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if token == "" {
|
if cfg.Token == "" {
|
||||||
return "", "", "", false, false, fmt.Errorf("TOKEN not found in config file")
|
return nil, fmt.Errorf("TOKEN not found in config file")
|
||||||
}
|
}
|
||||||
if hostname == "" {
|
if cfg.Hostname == "" {
|
||||||
hostname, _ = os.Hostname()
|
cfg.Hostname, _ = os.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
if url == "" {
|
return cfg, nil
|
||||||
url = defaultURL
|
|
||||||
}
|
|
||||||
|
|
||||||
return token, hostname, url, debug, autoUpdate, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectAndSend(config *Config) {
|
func collectAndSend(config *Config) {
|
||||||
@@ -1037,7 +1086,7 @@ func collectMetrics(config *Config) (*Metrics, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Network (per-second rates)
|
// Network (per-second rates)
|
||||||
netStats, err := net.IOCounters(false)
|
netStats, err := psnet.IOCounters(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: Failed to get network stats: %v", err)
|
log.Printf("Warning: Failed to get network stats: %v", err)
|
||||||
} else if len(netStats) > 0 {
|
} else if len(netStats) > 0 {
|
||||||
@@ -1086,15 +1135,29 @@ func collectMetrics(config *Config) (*Metrics, error) {
|
|||||||
metrics.TCP = collectTCPStats()
|
metrics.TCP = collectTCPStats()
|
||||||
metrics.FileDescriptors = collectFileDescriptors()
|
metrics.FileDescriptors = collectFileDescriptors()
|
||||||
|
|
||||||
|
// Optional integrations (only collected when configured)
|
||||||
|
if config.FPMStatusURL != "" {
|
||||||
|
metrics.FPM = collectFPMStats(config.FPMStatusURL)
|
||||||
|
}
|
||||||
|
if config.NginxStatusURL != "" {
|
||||||
|
metrics.Nginx = collectNginxStats(config.NginxStatusURL)
|
||||||
|
}
|
||||||
|
if len(config.SSLDomains) > 0 {
|
||||||
|
metrics.SSL = collectSSLCerts(config.SSLDomains)
|
||||||
|
}
|
||||||
|
if len(config.CheckPorts) > 0 {
|
||||||
|
metrics.Ports = collectPortChecks(config.CheckPorts)
|
||||||
|
}
|
||||||
|
|
||||||
return metrics, nil
|
return metrics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectTCPStats() *TCPStats {
|
func collectTCPStats() *TCPStats {
|
||||||
conns, err := net.Connections("tcp")
|
conns, err := psnet.Connections("tcp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
conns6, _ := net.Connections("tcp6")
|
conns6, _ := psnet.Connections("tcp6")
|
||||||
conns = append(conns, conns6...)
|
conns = append(conns, conns6...)
|
||||||
|
|
||||||
stats := &TCPStats{}
|
stats := &TCPStats{}
|
||||||
@@ -1159,6 +1222,183 @@ func collectFileDescriptors() *FileDescriptors {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// knownPortServices maps common port numbers to service names.
|
||||||
|
var knownPortServices = map[int]string{
|
||||||
|
21: "ftp",
|
||||||
|
22: "ssh",
|
||||||
|
25: "smtp",
|
||||||
|
80: "http",
|
||||||
|
443: "https",
|
||||||
|
3306: "mysql",
|
||||||
|
5432: "postgres",
|
||||||
|
6379: "redis",
|
||||||
|
8080: "http-alt",
|
||||||
|
8443: "https-alt",
|
||||||
|
9200: "elasticsearch",
|
||||||
|
11211: "memcached",
|
||||||
|
27017: "mongodb",
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectFPMStats(statusURL string) *FPMPoolStats {
|
||||||
|
url := statusURL
|
||||||
|
if strings.Contains(url, "?") {
|
||||||
|
url += "&json"
|
||||||
|
} else {
|
||||||
|
url += "?json"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 3 * time.Second}
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP-FPM JSON keys contain spaces (e.g. "active processes")
|
||||||
|
var data struct {
|
||||||
|
Pool string `json:"pool"`
|
||||||
|
ActiveProcesses int `json:"active processes"`
|
||||||
|
IdleProcesses int `json:"idle processes"`
|
||||||
|
TotalProcesses int `json:"total processes"`
|
||||||
|
MaxActiveProcesses int `json:"max active processes"`
|
||||||
|
MaxChildrenReached int `json:"max children reached"`
|
||||||
|
ListenQueue int `json:"listen queue"`
|
||||||
|
MaxListenQueue int `json:"max listen queue"`
|
||||||
|
SlowRequests int `json:"slow requests"`
|
||||||
|
AcceptedConn int `json:"accepted conn"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FPMPoolStats{
|
||||||
|
Pool: data.Pool,
|
||||||
|
ActiveProcesses: data.ActiveProcesses,
|
||||||
|
IdleProcesses: data.IdleProcesses,
|
||||||
|
TotalProcesses: data.TotalProcesses,
|
||||||
|
MaxActiveProcesses: data.MaxActiveProcesses,
|
||||||
|
MaxChildrenReached: data.MaxChildrenReached,
|
||||||
|
ListenQueue: data.ListenQueue,
|
||||||
|
MaxListenQueue: data.MaxListenQueue,
|
||||||
|
SlowRequests: data.SlowRequests,
|
||||||
|
AcceptedConn: data.AcceptedConn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectNginxStats(statusURL string) *NginxStats {
|
||||||
|
client := &http.Client{Timeout: 3 * time.Second}
|
||||||
|
resp, err := client.Get(statusURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nginx stub_status format:
|
||||||
|
// Active connections: 291
|
||||||
|
// server accepts handled requests
|
||||||
|
// 16630948 16630948 31070465
|
||||||
|
// Reading: 6 Writing: 179 Waiting: 106
|
||||||
|
stats := &NginxStats{}
|
||||||
|
lines := strings.Split(string(body), "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "Active connections:"):
|
||||||
|
fmt.Sscanf(strings.TrimPrefix(line, "Active connections:"), " %d", &stats.ActiveConnections)
|
||||||
|
case strings.HasPrefix(line, "Reading:"):
|
||||||
|
fmt.Sscanf(line, "Reading: %d Writing: %d Waiting: %d", &stats.Reading, &stats.Writing, &stats.Waiting)
|
||||||
|
case i > 0 && strings.TrimSpace(lines[i-1]) == "server accepts handled requests":
|
||||||
|
fmt.Sscanf(line, "%d %d %d", &stats.Accepts, &stats.Handled, &stats.Requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectSSLCerts(domains []string) []SSLCertInfo {
|
||||||
|
results := make([]SSLCertInfo, 0, len(domains))
|
||||||
|
for _, domain := range domains {
|
||||||
|
results = append(results, checkSSLCert(domain))
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSSLCert(domain string) SSLCertInfo {
|
||||||
|
// Strip any scheme/path — we only need the hostname
|
||||||
|
domain = strings.TrimPrefix(domain, "https://")
|
||||||
|
domain = strings.TrimPrefix(domain, "http://")
|
||||||
|
if idx := strings.Index(domain, "/"); idx != -1 {
|
||||||
|
domain = domain[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
info := SSLCertInfo{Domain: domain}
|
||||||
|
|
||||||
|
// Resolve SNI hostname separately if domain includes a port
|
||||||
|
host := domain
|
||||||
|
addr := domain + ":443"
|
||||||
|
if strings.Contains(domain, ":") {
|
||||||
|
host = strings.Split(domain, ":")[0]
|
||||||
|
addr = domain
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := tls.DialWithDialer(
|
||||||
|
&net.Dialer{Timeout: 5 * time.Second},
|
||||||
|
"tcp",
|
||||||
|
addr,
|
||||||
|
&tls.Config{ServerName: host},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
info.Error = err.Error()
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
certs := conn.ConnectionState().PeerCertificates
|
||||||
|
if len(certs) == 0 {
|
||||||
|
info.Error = "no certificates returned"
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := certs[0]
|
||||||
|
now := time.Now()
|
||||||
|
info.ExpiresAt = cert.NotAfter.UTC()
|
||||||
|
info.DaysLeft = int(cert.NotAfter.Sub(now).Hours() / 24)
|
||||||
|
info.Valid = now.Before(cert.NotAfter) && now.After(cert.NotBefore)
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectPortChecks(ports []int) []PortCheck {
|
||||||
|
results := make([]PortCheck, 0, len(ports))
|
||||||
|
for _, port := range ports {
|
||||||
|
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
|
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
|
||||||
|
open := err == nil
|
||||||
|
if open {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
results = append(results, PortCheck{
|
||||||
|
Port: port,
|
||||||
|
Open: open,
|
||||||
|
Service: knownPortServices[port],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
func collectTopProcesses() ([]ProcessInfo, error) {
|
func collectTopProcesses() ([]ProcessInfo, error) {
|
||||||
pids, err := process.Pids()
|
pids, err := process.Pids()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+43
-1
@@ -114,5 +114,47 @@
|
|||||||
"cpu_time": 890.12,
|
"cpu_time": 890.12,
|
||||||
"memory_bytes": 134217728
|
"memory_bytes": 134217728
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"fpm": {
|
||||||
|
"pool": "www",
|
||||||
|
"active_processes": 3,
|
||||||
|
"idle_processes": 7,
|
||||||
|
"total_processes": 10,
|
||||||
|
"max_active_processes": 8,
|
||||||
|
"max_children_reached": 0,
|
||||||
|
"listen_queue": 0,
|
||||||
|
"max_listen_queue": 2,
|
||||||
|
"slow_requests": 0,
|
||||||
|
"accepted_conn": 84291
|
||||||
|
},
|
||||||
|
"ssl": [
|
||||||
|
{
|
||||||
|
"domain": "example.com",
|
||||||
|
"expires_at": "2026-06-01T12:00:00Z",
|
||||||
|
"days_left": 95,
|
||||||
|
"valid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"domain": "api.example.com",
|
||||||
|
"expires_at": "2026-06-01T12:00:00Z",
|
||||||
|
"days_left": 95,
|
||||||
|
"valid": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
{ "port": 22, "open": true, "service": "ssh" },
|
||||||
|
{ "port": 80, "open": true, "service": "http" },
|
||||||
|
{ "port": 443, "open": true, "service": "https" },
|
||||||
|
{ "port": 3306, "open": true, "service": "mysql" },
|
||||||
|
{ "port": 6379, "open": false, "service": "redis" }
|
||||||
|
],
|
||||||
|
"nginx": {
|
||||||
|
"active_connections": 42,
|
||||||
|
"reading": 1,
|
||||||
|
"writing": 8,
|
||||||
|
"waiting": 33,
|
||||||
|
"accepts": 1847293,
|
||||||
|
"handled": 1847293,
|
||||||
|
"requests": 3921847
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user