packagemetricsimport("honeypot/internal/database""honeypot/internal/types""net/http""os""sort""strconv""strings""sync""unicode/utf8""github.com/prometheus/client_golang/prometheus""github.com/prometheus/client_golang/prometheus/promhttp")const(topNCount=20// Number of top items to track)// SanitizeUTF8 ensures a string is valid UTF-8 for Prometheus labels.// Invalid UTF-8 sequences are replaced with the Unicode replacement character (U+FFFD).funcSanitizeUTF8(sstring)string{ifutf8.ValidString(s){returns}// Replace invalid UTF-8 sequences with Unicode replacement characterreturnstrings.ToValidUTF8(s,"\uFFFD")}// MetricsCollector collects and exposes honeypot metrics.typeMetricsCollectorstruct{disableHWMetricsbool// Prometheus metricspacketsTotal*prometheus.CounterVecauthAttempts*prometheus.CounterVectopFields*prometheus.GaugeVecloadAvg*prometheus.GaugeVectempCPU*prometheus.GaugeVecdatabaseRows*prometheus.GaugeVecdatabaseSizeprometheus.Gauge// Database referencedb*database.Database// Internal trackingfieldCountsmap[string]map[string]map[string]int64// type -> field_name -> value -> countregisteredFieldsmap[string]map[string]bool// type -> field_name -> truemusync.RWMutex}// NewMetricsCollector creates a new metrics collector.funcNewMetricsCollector(disableHWMetricsbool)*MetricsCollector{mc:=&MetricsCollector{disableHWMetrics:disableHWMetrics,packetsTotal:prometheus.NewCounterVec(prometheus.CounterOpts{Name:"honeypot_packets_total",Help:"Total number of packets/requests by honeypot type and event type",},[]string{"type","event"},),authAttempts:prometheus.NewCounterVec(prometheus.CounterOpts{Name:"honeypot_auth_attempts_total",Help:"Total number of authentication attempts by honeypot type",},[]string{"type"},),topFields:prometheus.NewGaugeVec(prometheus.GaugeOpts{Name:"honeypot_top_fields",Help:"Top field values by count, configurable per honeypot type",},[]string{"type","field_name","value"},),loadAvg:prometheus.NewGaugeVec(prometheus.GaugeOpts{Name:"honeypot_load_avg",Help:"Load average of the system",},[]string{"type"},),tempCPU:prometheus.NewGaugeVec(prometheus.GaugeOpts{Name:"honeypot_temp_cpu",Help:"Temperature in degrees Celsius",},[]string{"type"},),databaseRows:prometheus.NewGaugeVec(prometheus.GaugeOpts{Name:"honeypot_database_rows",Help:"Number of rows in database tables",},[]string{"table"},),databaseSize:prometheus.NewGauge(prometheus.GaugeOpts{Name:"honeypot_database_size_bytes",Help:"Total size of the database file and WAL in bytes",},),fieldCounts:make(map[string]map[string]map[string]int64),registeredFields:make(map[string]map[string]bool),}// Register all metrics (ignore errors if already registered)_=prometheus.Register(mc.packetsTotal)_=prometheus.Register(mc.authAttempts)_=prometheus.Register(mc.topFields)_=prometheus.Register(mc.loadAvg)_=prometheus.Register(mc.tempCPU)_=prometheus.Register(mc.databaseRows)_=prometheus.Register(mc.databaseSize)// Auto-register common fields that should be tracked for all honeypot typesmc.registerCommonFields()returnmc}// registerCommonFields registers common fields that should be tracked for all honeypot types.func(mc*MetricsCollector)registerCommonFields(){// Track username and password for all types (they'll only be present in auth_attempt events)mc.RegisterTopNField("*","username")mc.RegisterTopNField("*","password")// Track remote_addr for all typesmc.RegisterTopNField("*","remote_addr")// Track port for packetlogger type (will be set in RecordEvent)mc.RegisterTopNField("packetlogger","port")}// RecordEvent processes a honeypot event and updates metrics.func(mc*MetricsCollector)RecordEvent(etypes.LogEvent){mc.mu.Lock()defermc.mu.Unlock()t:=string(e.Type)// Increment packets/requests countermc.packetsTotal.WithLabelValues(t,string(e.Event)).Inc()// Track auth attemptsife.Event==types.EventAuthAttempt{mc.authAttempts.WithLabelValues(t).Inc()}// Track all registered fields (including common ones and type-specific ones)// Check both type-specific and wildcard registrationsfieldsToTrack:=make(map[string]bool)// Add type-specific registered fieldsifregistered,ok:=mc.registeredFields[t];ok{forfieldName:=rangeregistered{fieldsToTrack[fieldName]=true}}// Add wildcard registered fieldsifregistered,ok:=mc.registeredFields["*"];ok{forfieldName:=rangeregistered{fieldsToTrack[fieldName]=true}}// Track all registered fieldsforfieldName:=rangefieldsToTrack{varvalueanyvarokbool// Special handling for fields that are tracked but not stored in the Fields mapiffieldName=="remote_addr"{value=e.RemoteAddrok=e.RemoteAddr!=""}elseiffieldName=="port"&&e.Type==types.HoneypotTypePacketLogger&&e.Event!=types.EventICMPPacket{varprotocolstringswitche.Event{casetypes.EventTCPPacket:protocol="tcp"casetypes.EventUDPPacket:protocol="udp"}ifprotocol!=""{value=protocol+":"+strconv.Itoa(int(e.DstPort))ok=true}}elseife.Fields!=nil{value,ok=e.Fields[fieldName]}ifok{varstrValuestringswitchv:=value.(type){casestring:strValue=vdefault:// Skip non-string valuescontinue}ifstrValue!=""{ifmc.fieldCounts[t]==nil{mc.fieldCounts[t]=make(map[string]map[string]int64)}ifmc.fieldCounts[t][fieldName]==nil{mc.fieldCounts[t][fieldName]=make(map[string]int64)}mc.fieldCounts[t][fieldName][strValue]++}}}}// RegisterTopNField registers a field for top-N tracking for a specific honeypot type.func(mc*MetricsCollector)RegisterTopNField(honeypotTypetypes.HoneypotType,fieldNamestring){mc.mu.Lock()defermc.mu.Unlock()ifmc.registeredFields[string(honeypotType)]==nil{mc.registeredFields[string(honeypotType)]=make(map[string]bool)}mc.registeredFields[string(honeypotType)][fieldName]=true}// updateTopNGauges updates the Prometheus gauges with top-N items.func(mc*MetricsCollector)updateTopNGauges(){mc.updateTopFields()}// updateTopFields updates the top fields gauge for all registered fields.func(mc*MetricsCollector)updateTopFields(){// Reset all gauges firstmc.topFields.Reset()// Process each honeypot typeforhType,fieldNames:=rangemc.fieldCounts{// Process each field name for this honeypot typeforfieldName,values:=rangefieldNames{// Get top N values for this fieldtopValues:=getTopN(values,topNCount)forvalue,count:=rangetopValues{mc.topFields.WithLabelValues(SanitizeUTF8(hType),SanitizeUTF8(fieldName),SanitizeUTF8(value),).Set(float64(count))}}}}// updateLoadAvg updates the load average gauge.func(mc*MetricsCollector)updateLoadAvg(){// read load average from /proc/loadavgloadAvg,err:=os.ReadFile("/proc/loadavg")iferr!=nil{return}loadAvgStr:=strings.Split(string(loadAvg)," ")loadAvg1:=loadAvgStr[0]loadAvg5:=loadAvgStr[1]loadAvg15:=loadAvgStr[2]loadAvg1Float,err:=strconv.ParseFloat(loadAvg1,64)iferr!=nil{return}loadAvg5Float,err:=strconv.ParseFloat(loadAvg5,64)iferr!=nil{return}loadAvg15Float,err:=strconv.ParseFloat(loadAvg15,64)iferr!=nil{return}mc.loadAvg.WithLabelValues("loadavg1").Set(loadAvg1Float)mc.loadAvg.WithLabelValues("loadavg5").Set(loadAvg5Float)mc.loadAvg.WithLabelValues("loadavg15").Set(loadAvg15Float)}// updateTempCPU updates the temperature of the CPU.func(mc*MetricsCollector)updateTempCPU(){// read temperature from /sys/class/thermal/thermal_zone0/temptemp,err:=os.ReadFile("/sys/class/thermal/thermal_zone0/temp")iferr!=nil{return}tempStr:=strings.TrimSpace(string(temp))tempFloat,err:=strconv.ParseFloat(tempStr,32)iferr!=nil{return}mc.tempCPU.WithLabelValues("cpu").Set(tempFloat/1000)}// SetDatabase sets the database reference for metrics collection.func(mc*MetricsCollector)SetDatabase(db*database.Database){mc.mu.Lock()defermc.mu.Unlock()mc.db=db}// updateDatabaseStats updates the database statistics gauges.func(mc*MetricsCollector)updateDatabaseStats(){mc.mu.RLock()db:=mc.dbmc.mu.RUnlock()ifdb==nil{return}stats,err:=db.GetDatabaseStats()iferr!=nil{return}mc.databaseRows.WithLabelValues("events").Set(float64(stats.RowsEvents))mc.databaseRows.WithLabelValues("ips").Set(float64(stats.RowsIps))mc.databaseRows.WithLabelValues("blocklist").Set(float64(stats.RowsBlocklist))mc.databaseRows.WithLabelValues("unresolved_ips").Set(float64(stats.RowsUnresolvedIPs))mc.databaseSize.Set(float64(stats.DatabaseSize))}// getTopN returns the top N items from a map sorted by count.funcgetTopN(countsmap[string]int64,nint)map[string]int64{iflen(counts)==0{returnmake(map[string]int64)}// Convert to slice for sortingtypeitemstruct{keystringcountint64}items:=make([]item,0,len(counts))fork,v:=rangecounts{items=append(items,item{k,v})}// Sort by count (descending)sort.Slice(items,func(i,jint)bool{returnitems[i].count>items[j].count})// Take top NtopN:=make(map[string]int64)limit:=niflen(items)<limit{limit=len(items)}fori:=0;i<limit;i++{topN[items[i].key]=items[i].count}returntopN}// GetHandler returns the HTTP handler for Prometheus metrics.func(mc*MetricsCollector)GetHandler()http.Handler{h:=promhttp.Handler()returnhttp.HandlerFunc(func(whttp.ResponseWriter,r*http.Request){mc.mu.RLock()mc.updateTopNGauges()if!mc.disableHWMetrics{mc.updateLoadAvg()mc.updateTempCPU()}mc.mu.RUnlock()mc.updateDatabaseStats()h.ServeHTTP(w,r)})}