Repository: DevOops23b/GoSearch
Pull request #167: Release 1.2.1
Author: login: YoussefEL-M
Created: 2025-04-30T11:34:18Z | Merged: 2025-04-30T11:39:50Z

## Describe changes
Release adding logging and monitoring to our app

## Issue
#149 
#163 
#112 
- [ ] Not related to an issue

## Completed?

- [X] Yes
- [ ] No

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced monitoring and observability with Prometheus and Grafana, including new Docker Compose setup and Prometheus configuration.
  - Added detailed backend metrics for HTTP requests, CPU load, TLS certificate health, and user registrations, accessible at a new `/metrics` endpoint.
  - Enhanced search logging with query and client IP tracking, and added a fallback search for test scenarios.

- **Chores**
  - Updated environment variable handling and `.gitignore` to better support environment files.
  - Upgraded and added dependencies to support monitoring and system metrics.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.gitignore b/.gitignore
index e4b7722..e2fb273 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,9 @@ secrets/
 # Go workspace file
 go.work
 
+# environment
+*.env*
+
 # End
 
 # Playwright
diff --git a/backend b/backend
new file mode 100755
index 0000000..691ab2a
Binary files /dev/null and b/backend differ
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index aa6818c..c7cf7be 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -19,6 +19,7 @@ services:
       - TEMPLATE_PATH=${TEMPLATE_PATH}
       - STATIC_PATH=${STATIC_PATH}
       - SESSION_SECRET=${SESSION_SECRET}
+      - SEARCH_LOG_PATH=/app/src/backend/search.log
     depends_on:
       - postgres
       - elasticsearch
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..c76b55e
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,31 @@
+services:
+  prometheus:
+    image: prom/prometheus@sha256:b41a46cf049d3865d87ba72f6c531b688f1651a579c86f31584f41ee6d45a437
+    ports:
+      - "9090:9090"
+    volumes:
+      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+      - prometheus_data:/prometheus
+    command:
+      - '--config.file=/etc/prometheus/prometheus.yml'
+      - '--storage.tsdb.path=/prometheus'
+      - '--web.console.libraries=/etc/prometheus/console.libraries'
+      - '--web.console.templates=/etc/prometheus/consoles'
+    restart: unless-stopped
+
+  grafana:
+    image: grafana/grafana@sha256:95173c11b36f53a423ce47f00baee3f913a3e403f202a83c8bc3c7115a2cfdd0
+    ports:
+      - "3000:3000"
+    volumes:
+      - grafana_data:/var/lib/grafana
+    environment:
+      - GF_SECURITY_ADMIN_PASSWORD=${GF_ADMIN_PASSWORD}
+      - GF_USERS_ALLOWS_SIGNUP=false
+    depends_on:
+      - prometheus
+    restart: unless-stopped
+
+volumes:
+  prometheus_data:
+  grafana_data:  
\ No newline at end of file
diff --git a/go.mod b/go.mod
index b21e532..3671235 100644
--- a/go.mod
+++ b/go.mod
@@ -5,15 +5,15 @@ go 1.24
 require github.com/gorilla/mux v1.8.1
 
 require (
-	github.com/elastic/go-elasticsearch/v8 v8.17.1
+	github.com/elastic/go-elasticsearch/v8 v8.18.0
 	github.com/gorilla/sessions v1.4.0
 	github.com/lib/pq v1.10.9
-	github.com/mattn/go-sqlite3 v1.14.27
+	github.com/mattn/go-sqlite3 v1.14.28
 	golang.org/x/crypto v0.37.0
 )
 
 require (
-	github.com/elastic/elastic-transport-go/v8 v8.6.1 // indirect
+	github.com/elastic/elastic-transport-go/v8 v8.7.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/joho/godotenv v1.5.1 // indirect
@@ -23,6 +23,23 @@ require (
 	go.opentelemetry.io/otel/trace v1.29.0 // indirect
 )
 
+require (
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/go-ole/go-ole v1.2.6 // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/prometheus/client_golang v1.22.0 // indirect
+	github.com/prometheus/client_model v0.6.1 // indirect
+	github.com/prometheus/common v0.62.0 // indirect
+	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/shirou/gopsutil v3.21.11+incompatible // indirect
+	github.com/tklauser/go-sysconf v0.3.15 // indirect
+	github.com/tklauser/numcpus v0.10.0 // indirect
+	github.com/yusufpapurcu/wmi v1.2.4 // indirect
+	golang.org/x/sys v0.32.0 // indirect
+	google.golang.org/protobuf v1.36.5 // indirect
+)
+
 require (
 	github.com/DATA-DOG/go-sqlmock v1.5.2
 	github.com/davecgh/go-spew v1.1.1 // indirect
diff --git a/go.sum b/go.sum
index 6736dba..39d5779 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,19 @@
 github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
 github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/elastic/elastic-transport-go/v8 v8.6.1 h1:h2jQRqH6eLGiBSN4eZbQnJLtL4bC5b4lfVFRjw2R4e4=
 github.com/elastic/elastic-transport-go/v8 v8.6.1/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
+github.com/elastic/elastic-transport-go/v8 v8.7.0 h1:OgTneVuXP2uip4BA658Xi6Hfw+PeIOod2rY3GVMGoVE=
+github.com/elastic/elastic-transport-go/v8 v8.7.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
 github.com/elastic/go-elasticsearch/v8 v8.17.1 h1:bOXChDoCMB4TIwwGqKd031U8OXssmWLT3UrAr9EGs3Q=
 github.com/elastic/go-elasticsearch/v8 v8.17.1/go.mod h1:MVJCtL+gJJ7x5jFeUmA20O7rvipX8GcQmo5iBcmaJn4=
+github.com/elastic/go-elasticsearch/v8 v8.18.0 h1:ANNq1h7DEiPUaALb8+5w3baQzaS08WfHV0DNzp0VG4M=
+github.com/elastic/go-elasticsearch/v8 v8.18.0/go.mod h1:WLqwXsJmQoYkoA9JBFeEwPkQhCfAZuUvfpdU/NvSSf0=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -13,6 +21,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -30,10 +40,24 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
 github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
+github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
+github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
+github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
@@ -44,10 +68,21 @@ go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHy
 go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
 go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
 go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
+github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
+github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
+github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
+github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
 golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
 golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
 golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml
new file mode 100644
index 0000000..7185a36
--- /dev/null
+++ b/prometheus/prometheus.yml
@@ -0,0 +1,12 @@
+global:
+  scrape_interval: 15s
+  evaluation_interval: 15s
+
+scrape_configs:
+  - job_name: 'gosearch'
+    metrics_path: '/metrics'
+    scheme: http
+    static_configs:
+      - targets:
+          - 'gosearch.dk:8081'
+      
\ No newline at end of file
diff --git a/src/backend/app.go b/src/backend/app.go
index 3be6f7c..1195876 100644
--- a/src/backend/app.go
+++ b/src/backend/app.go
@@ -3,11 +3,13 @@ package main
 import (
 	"context"
 	"crypto/tls"
+	"crypto/x509"
 	"database/sql"
 	"encoding/json"
 	"fmt"
 	"log"
 	"os"
+	"strconv"
 	"strings"
 	"time"
 
@@ -35,8 +37,194 @@ import (
 	// Import the cron library to schedule periodic tasks
 	// PostgreSQL driver instead of SQLite.
 	_ "github.com/lib/pq"
+
+	// For monitoring
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"github.com/shirou/gopsutil/cpu"
 )
 
+///////////////////////////////////////////////////////////////////////////////////
+/// Monitoring with Prometheus
+///////////////////////////////////////////////////////////////////////////////////
+
+// Define metrics
+var (
+	httpRequestsTotal = promauto.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "http_requests_total",
+			Help: "Total number of HTTP requests",
+		},
+		[]string{"method", "endpoint", "status"},
+	)
+
+	httpRequestDuration = promauto.NewHistogramVec(
+		prometheus.HistogramOpts{
+			Name:    "http_request_duration_seconds",
+			Help:    "Duration of HTTP requests in seconds",
+			Buckets: prometheus.DefBuckets,
+		},
+		[]string{"method", "endpoint"},
+	)
+
+	cpuLoadPercentage = promauto.NewGauge(
+		prometheus.GaugeOpts{
+			Name: "cpu_load_percentage",
+			Help: "Current cpu load in percent",
+		},
+	)
+
+	certExpiryDays = promauto.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "tls_certificate_expiry_days",
+			Help: "Days until the tls certificate expires",
+		},
+		[]string{"domain"},
+	)
+
+	certValidity = promauto.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "tls_certificate_validity",
+			Help: "Certificate validity (1 = valid, 0 = invalid)",
+		},
+		[]string{"domain"},
+	)
+
+	newUserCounter = promauto.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "new_users_total_count",
+			Help: "New users",
+		},
+		[]string{"hour_of_day", "day_of_week"},
+	)
+)
+
+type statusRecorder struct {
+	http.ResponseWriter
+	statusCode int
+}
+
+func (rec *statusRecorder) WriteHeader(statusCode int) {
+	rec.statusCode = statusCode
+	rec.ResponseWriter.WriteHeader(statusCode)
+}
+
+func metricsMiddleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		// Custom response writer to track status
+		recorder := &statusRecorder{
+			ResponseWriter: w,
+			statusCode:     200,
+		}
+		start := time.Now()
+
+		next.ServeHTTP(recorder, r)
+
+		// Record metrics after the request is processed
+		duration := time.Since(start).Seconds()
+		httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
+
+		// Use actual status code
+		httpRequestsTotal.WithLabelValues(
+			r.Method,
+			r.URL.Path,
+			strconv.Itoa(recorder.statusCode),
+		).Inc()
+
+	})
+}
+
+func startMonitoring() {
+
+	// Start CPU monitoring
+	go monitorCPU()
+
+	// Start certificate monitoring
+	go certificateMonitoring()
+}
+
+func monitorCPU() {
+	for {
+		cpuPercent, err := cpu.Percent(time.Second, false)
+		if err != nil {
+			log.Printf("Error moitoring CPU: %v", err)
+		} else if len(cpuPercent) > 0 {
+			cpuLoadPercentage.Set(cpuPercent[0])
+		}
+		time.Sleep(30 * time.Second)
+	}
+}
+
+func certificateMonitoring() {
+	domains := []string{"gosearch.dk"}
+
+	for {
+		for _, domain := range domains {
+			checkCertificate(domain)
+		}
+		time.Sleep(1 * time.Hour)
+	}
+}
+
+func checkCertificate(domain string) {
+	rootCAs, err := x509.SystemCertPool()
+	if err != nil {
+		log.Printf("Error loading system certification pool: %v", err)
+		rootCAs = x509.NewCertPool()
+	}
+
+	config := &tls.Config{
+		RootCAs:    rootCAs,
+		ServerName: domain,
+	}
+
+	conn, err := tls.Dial("tcp", domain+":443", config)
+
+	certValid := 0.0
+	daysUntilExpiry := 0.0
+
+	if err != nil {
+		log.Printf("Certificate validation failed for %s: %v", domain, err)
+
+	} else {
+		defer conn.Close()
+
+		if len(conn.ConnectionState().PeerCertificates) > 0 {
+			cert := conn.ConnectionState().PeerCertificates[0]
+
+			daysUntilExpiry = time.Until(cert.NotAfter).Hours() / 24
+
+			opts := x509.VerifyOptions{
+				DNSName: domain,
+				Roots:   rootCAs,
+			}
+
+			if _, err := cert.Verify(opts); err == nil {
+				certValid = 1.0
+
+			} else {
+				log.Printf("Certificate chain validation failed for %s: %v", domain, err)
+			}
+		} else {
+			log.Printf("No certificates found for %s", domain)
+		}
+
+	}
+
+	certExpiryDays.WithLabelValues(domain).Set(daysUntilExpiry)
+	certValidity.WithLabelValues(domain).Set(certValid)
+}
+
+// Updates the user counter with current hour and weekday
+func incrementNewUserCounter() {
+	now := time.Now()
+	hourOfDay := strconv.Itoa(now.Hour())
+	dayOfWeek := now.Weekday().String()
+
+	newUserCounter.WithLabelValues(hourOfDay, dayOfWeek).Inc()
+}
+
 ///////////////////////////////////////////////////////////////////////////////////
 /// Configurations
 ///////////////////////////////////////////////////////////////////////////////////
@@ -47,6 +235,8 @@ var templatePath string
 
 var staticPath string
 
+var searchLogger = log.New(os.Stdout, "SEARCH: ", log.LstdFlags)
+
 func init() {
 
 	if err := godotenv.Load(".env.local"); err != nil {
@@ -287,6 +477,28 @@ func initElasticsearch() {
 }
 
 func searchPagesInEs(query string) ([]Page, error) {
+	///// TESTS FALLBACK ///////////
+	if esClient == nil {
+		// Simple DB search for test mode
+		var pages []Page
+		sqlStmt := "SELECT title, url, content FROM pages WHERE content LIKE ?"
+		likeQ := "%" + query + "%"
+		rows, err := db.Query(sqlStmt, likeQ)
+		if err != nil {
+			return nil, err
+		}
+		defer rows.Close()
+
+		for rows.Next() {
+			var p Page
+			if err := rows.Scan(&p.Title, &p.URL, &p.Content); err != nil {
+				continue
+			}
+			pages = append(pages, p)
+		}
+		return pages, nil
+	}
+	/////// PRODUCTION: real Elasticsearch search ───────────────────────────
 	var pages []Page
 
 	searchBody := strings.NewReader(fmt.Sprintf(`{
@@ -423,11 +635,16 @@ func aboutHandler(w http.ResponseWriter, r *http.Request) {
 // Viser search api-server.
 func searchHandler(w http.ResponseWriter, r *http.Request) {
 	//Henter search-query fra URL-parameteren.
+	log.Println("Search handler called")
+
 	queryParam := strings.TrimSpace(r.URL.Query().Get("q"))
 	if queryParam == "" {
 		http.Error(w, "No search query provided", http.StatusBadRequest)
 		return
 	}
+	//TO LOG THE QUERY//
+	log.Printf("Search query: %q from %s", queryParam, r.RemoteAddr)
+	searchLogger.Printf("query=%q from=%s", queryParam, r.RemoteAddr)
 
 	//Nuild search against Elasticsearch
 	pages, err := searchPagesInEs(queryParam)
@@ -726,6 +943,9 @@ func apiRegisterHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// Function to update number of users along with when they were created
+	incrementNewUserCounter()
+
 	// Create session and log the user in
 	session, err := store.Get(r, "session-name")
 	if err != nil {
@@ -886,6 +1106,7 @@ func startCronScheduler() {
 //////////////////////////////////////////////////////////////////////////////////
 
 func main() {
+
 	// initialiserer databasen og forbinder til den.
 	initDB()
 	defer closeDB()
@@ -897,15 +1118,47 @@ func main() {
 		log.Fatalf("Failed to sync pages: %v", err)
 	}
 
+	///NEW: initialize searchLogger////
+	logPath := os.Getenv("SEARCH_LOG_PATH")
+	if logPath == "" {
+		logPath = "search.log" // Default for Docker
+	}
+
+	f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+	if err != nil {
+		log.Printf("Warning: could not open search log file: %v, using stdout instead", err)
+		searchLogger = log.New(os.Stdout, "SEARCH: ", log.LstdFlags)
+	} else {
+		log.Printf("Search logs will be written to %s", logPath)
+		searchLogger = log.New(f, "SEARCH: ", log.LstdFlags)
+		defer f.Close()
+	}
+
 	checkTables()
 
 	// Start the cron scheduler to run checkTables periodically
 	startCronScheduler()
 
+	err = db.Ping()
+	if err != nil {
+		log.Fatalf("Database connection failed: %v", err)
+	}
+	fmt.Println("Database connection successful!")
+
+	startMonitoring()
+
 	// Detter er Gorilla Mux's route handler, i stedet for Flasks indbyggede router-handler
 	///Opretter en ny router
 	r := mux.NewRouter()
 
+	fmt.Println("Registering /metrics endpoint...")
+	r.Handle("/metrics", promhttp.Handler())
+
+	// Applying middleware function to all routes
+
+	appRouter := r.NewRoute().Subrouter()
+	appRouter.Use(metricsMiddleware)
+
 	//Definerer routerne.
 	r.HandleFunc("/", rootHandler).Methods("GET")             // Forside
 	r.HandleFunc("/about", aboutHandler).Methods("GET")       //about-side
diff --git a/src/backend/search.log b/src/backend/search.log
new file mode 100644
index 0000000..71b268e
--- /dev/null
+++ b/src/backend/search.log
@@ -0,0 +1,6 @@
+SEARCH: 2025/04/27 11:04:49 query="golang" from=[::1]:50021
+SEARCH: 2025/04/27 11:06:40 query="4" from=[::1]:50021
+SEARCH: 2025/04/27 11:07:02 query="database" from=[::1]:50021
+SEARCH: 2025/04/27 11:10:18 query="tlou" from=[::1]:50021
+SEARCH: 2025/04/27 11:10:27 query="devops" from=[::1]:50021
+SEARCH: 2025/04/28 20:33:34 query="python" from=172.19.0.1:48652


Reviews:
user: login: CVS2003
body: Approved
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-30T11:38:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>.gitignore (1)</summary><blockquote>

`26-28`: **Duplicate ignore rules – keep one source of truth**

You already ignore environment artefacts in lines 12-16 (`.env`, `.env.*`).  
Adding the blanket pattern `*.env*` duplicates the rule set and may hide typos (e.g. accidentally ignoring `environment.txt`). Consider consolidating to one canonical section.

</blockquote></details>
<details>
<summary>docker-compose.dev.yml (1)</summary><blockquote>

`22-23`: **Log-file path will not persist between container restarts**

`SEARCH_LOG_PATH=/app/src/backend/search.log` writes inside the container’s RW layer.  
When the container is recreated the file—and therefore the historical search logs—are lost.

Add a bind mount or named volume so logs survive:

```diff
    volumes:
       - ./src/backend:/app/src/backend
+      - ./logs/search:/app/src/backend/search.log   # persist search logs
```

</blockquote></details>
<details>
<summary>src/backend/app.go (2)</summary><blockquote>

`148-154`: **Minor typo in log message**

`"Error moitoring CPU"` → `"Error monitoring CPU"`.

```diff
- log.Printf("Error moitoring CPU: %v", err)
+ log.Printf("Error monitoring CPU: %v", err)
```

---

`1121-1135`: **Unbounded search.log may grow indefinitely**

Writing straight to a file without rotation can exhaust disk space.  
Consider integrating log rotation (`lumberjack`, `logrotate`, or Docker-side log drivers).

</blockquote></details>
<details>
<summary>docker-compose.yml (1)</summary><blockquote>

`7-8`: **Ensure the Prometheus configuration mount is read-only.**

Mounting the configuration file as read-only prevents accidental changes at runtime.


Apply this diff:
```diff
 services:
   prometheus:
     volumes:
-      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
       - prometheus_data:/prometheus
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ed753f3ff3d915116d78b22ee09cd15688b89d88 and 31684f709c594cf20a925f52ee145d4d3650a6a1.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `go.sum` is excluded by `!**/*.sum`
* `src/backend/search.log` is excluded by `!**/*.log`

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `.gitignore` (1 hunks)
* `docker-compose.dev.yml` (1 hunks)
* `docker-compose.yml` (1 hunks)
* `go.mod` (2 hunks)
* `prometheus/prometheus.yml` (1 hunks)
* `src/backend/app.go` (8 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 YAMLlint (1.35.1)</summary>

<details>
<summary>prometheus/prometheus.yml</summary>

[error] 12-12: no new line character at the end of file

(new-line-at-end-of-file)

---

[error] 12-12: trailing spaces

(trailing-spaces)

</details>
<details>
<summary>docker-compose.yml</summary>

[warning] 31-31: too many spaces after colon

(colons)

---

[error] 31-31: no new line character at the end of file

(new-line-at-end-of-file)

---

[error] 31-31: trailing spaces

(trailing-spaces)

</details>

</details>
<details>
<summary>🪛 ast-grep (0.31.1)</summary>

<details>
<summary>src/backend/app.go</summary>

[warning] 176-179: MinVersion` is missing from this TLS configuration.  By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled.  Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. Add `MinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.
Context: tls.Config{
		RootCAs:    rootCAs,
		ServerName: domain,
	}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
     https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(missing-ssl-minversion-go)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>go.mod (1)</summary>

`7-13`: **Large indirect-dependency block – check `go mod tidy` output**

The new Prometheus stack pulls in many indirect packages. Make sure they are all still required after compilation (`go mod tidy`) to avoid bloating your build.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-30T11:39:15Z

Review comments:
diff_hunk: @@ -0,0 +1,12 @@
+global:
+  scrape_interval: 15s
+  evaluation_interval: 15s
+
+scrape_configs:
+  - job_name: 'gosearch'
+    metrics_path: '/metrics'
+    scheme: http
+    static_configs:
+      - targets:
+          - 'gosearch.dk:8081'
+      
path: prometheus/prometheus.yml
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**YAML lint failures & unreachable scrape target**

1. YAML-lint flagged:
   • trailing spaces on line 12  
   • missing terminating newline

2. Target `gosearch.dk:8081` is the **host-mapped** port.  
   Inside the compose network Prometheus cannot reach the host mapping; you’ll want the service name & internal port (`go-app:8080` by default).

Apply:

```diff
-      - targets:
-          - 'gosearch.dk:8081'␠
+      - targets:
+          - 'go-app:8080'
+
```




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'gosearch'
    metrics_path: '/metrics'
    scheme: http
    static_configs:
      - targets:
          - 'go-app:8080'
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 YAMLlint (1.35.1)</summary>

[error] 12-12: no new line character at the end of file

(new-line-at-end-of-file)

---

[error] 12-12: trailing spaces

(trailing-spaces)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T11:39:14Z
author_association: NONE
diff_hunk: @@ -287,6 +477,28 @@ func initElasticsearch() {
 }
 
 func searchPagesInEs(query string) ([]Page, error) {
+	///// TESTS FALLBACK ///////////
+	if esClient == nil {
+		// Simple DB search for test mode
+		var pages []Page
+		sqlStmt := "SELECT title, url, content FROM pages WHERE content LIKE ?"
+		likeQ := "%" + query + "%"
+		rows, err := db.Query(sqlStmt, likeQ)
+		if err != nil {
+			return nil, err
+		}
+		defer rows.Close()
+
+		for rows.Next() {
+			var p Page
+			if err := rows.Scan(&p.Title, &p.URL, &p.Content); err != nil {
+				continue
+			}
+			pages = append(pages, p)
+		}
+		return pages, nil
+	}
path: src/backend/app.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**SQL placeholder incompatible with PostgreSQL – fallback search will panic**

`lib/pq` expects `$1`, `$2`, …, not `?`. The current query produces “pq: syntax error at or near "?"”.

```diff
- sqlStmt := "SELECT title, url, content FROM pages WHERE content LIKE ?"
+ sqlStmt := "SELECT title, url, content FROM pages WHERE content LIKE $1"
```

Consider `ILIKE` for case-insensitive search as well.




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
	///// TESTS FALLBACK ///////////
	if esClient == nil {
		// Simple DB search for test mode
		var pages []Page
-		sqlStmt := "SELECT title, url, content FROM pages WHERE content LIKE ?"
+		sqlStmt := "SELECT title, url, content FROM pages WHERE content LIKE $1"
		likeQ := "%" + query + "%"
		rows, err := db.Query(sqlStmt, likeQ)
		if err != nil {
			return nil, err
		}
		defer rows.Close()

		for rows.Next() {
			var p Page
			if err := rows.Scan(&p.Title, &p.URL, &p.Content); err != nil {
				continue
			}
			pages = append(pages, p)
		}
		return pages, nil
	}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T11:39:14Z
author_association: NONE
diff_hunk: @@ -897,15 +1118,47 @@
 		log.Fatalf("Failed to sync pages: %v", err)
 	}
 
+	///NEW: initialize searchLogger////
+	logPath := os.Getenv("SEARCH_LOG_PATH")
+	if logPath == "" {
+		logPath = "search.log" // Default for Docker
+	}
+
+	f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+	if err != nil {
+		log.Printf("Warning: could not open search log file: %v, using stdout instead", err)
+		searchLogger = log.New(os.Stdout, "SEARCH: ", log.LstdFlags)
+	} else {
+		log.Printf("Search logs will be written to %s", logPath)
+		searchLogger = log.New(f, "SEARCH: ", log.LstdFlags)
+		defer f.Close()
+	}
+
 	checkTables()
 
 	// Start the cron scheduler to run checkTables periodically
 	startCronScheduler()
 
+	err = db.Ping()
+	if err != nil {
+		log.Fatalf("Database connection failed: %v", err)
+	}
+	fmt.Println("Database connection successful!")
+
+	startMonitoring()
+
 	// Detter er Gorilla Mux's route handler, i stedet for Flasks indbyggede router-handler
 	///Opretter en ny router
 	r := mux.NewRouter()
 
+	fmt.Println("Registering /metrics endpoint...")
+	r.Handle("/metrics", promhttp.Handler())
+
+	// Applying middleware function to all routes
+
+	appRouter := r.NewRoute().Subrouter()
+	appRouter.Use(metricsMiddleware)
+
path: src/backend/app.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Metrics middleware is never applied to the registered routes**

You create a sub-router `appRouter` with `metricsMiddleware`, but all handlers are attached to the parent router `r`, so no HTTP metrics are collected.

Simplest fix – attach the middleware to `r` directly and drop the unused sub-router:

```diff
- appRouter := r.NewRoute().Subrouter()
- appRouter.Use(metricsMiddleware)
+ r.Use(metricsMiddleware)
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T11:39:14Z
author_association: NONE
diff_hunk: @@ -35,8 +37,194 @@
 	// Import the cron library to schedule periodic tasks
 	// PostgreSQL driver instead of SQLite.
 	_ "github.com/lib/pq"
+
+	// For monitoring
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"github.com/shirou/gopsutil/cpu"
 )
 
+///////////////////////////////////////////////////////////////////////////////////
+/// Monitoring with Prometheus
+///////////////////////////////////////////////////////////////////////////////////
+
+// Define metrics
+var (
+	httpRequestsTotal = promauto.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "http_requests_total",
+			Help: "Total number of HTTP requests",
+		},
+		[]string{"method", "endpoint", "status"},
+	)
+
+	httpRequestDuration = promauto.NewHistogramVec(
+		prometheus.HistogramOpts{
+			Name:    "http_request_duration_seconds",
+			Help:    "Duration of HTTP requests in seconds",
+			Buckets: prometheus.DefBuckets,
+		},
+		[]string{"method", "endpoint"},
+	)
+
+	cpuLoadPercentage = promauto.NewGauge(
+		prometheus.GaugeOpts{
+			Name: "cpu_load_percentage",
+			Help: "Current cpu load in percent",
+		},
+	)
+
+	certExpiryDays = promauto.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "tls_certificate_expiry_days",
+			Help: "Days until the tls certificate expires",
+		},
+		[]string{"domain"},
+	)
+
+	certValidity = promauto.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "tls_certificate_validity",
+			Help: "Certificate validity (1 = valid, 0 = invalid)",
+		},
+		[]string{"domain"},
+	)
+
+	newUserCounter = promauto.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "new_users_total_count",
+			Help: "New users",
+		},
+		[]string{"hour_of_day", "day_of_week"},
+	)
+)
+
+type statusRecorder struct {
+	http.ResponseWriter
+	statusCode int
+}
+
+func (rec *statusRecorder) WriteHeader(statusCode int) {
+	rec.statusCode = statusCode
+	rec.ResponseWriter.WriteHeader(statusCode)
+}
+
+func metricsMiddleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		// Custom response writer to track status
+		recorder := &statusRecorder{
+			ResponseWriter: w,
+			statusCode:     200,
+		}
+		start := time.Now()
+
+		next.ServeHTTP(recorder, r)
+
+		// Record metrics after the request is processed
+		duration := time.Since(start).Seconds()
+		httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
+
+		// Use actual status code
+		httpRequestsTotal.WithLabelValues(
+			r.Method,
+			r.URL.Path,
+			strconv.Itoa(recorder.statusCode),
+		).Inc()
+
+	})
+}
+
+func startMonitoring() {
+
+	// Start CPU monitoring
+	go monitorCPU()
+
+	// Start certificate monitoring
+	go certificateMonitoring()
+}
+
+func monitorCPU() {
+	for {
+		cpuPercent, err := cpu.Percent(time.Second, false)
+		if err != nil {
+			log.Printf("Error moitoring CPU: %v", err)
+		} else if len(cpuPercent) > 0 {
+			cpuLoadPercentage.Set(cpuPercent[0])
+		}
+		time.Sleep(30 * time.Second)
+	}
+}
+
+func certificateMonitoring() {
+	domains := []string{"gosearch.dk"}
+
+	for {
+		for _, domain := range domains {
+			checkCertificate(domain)
+		}
+		time.Sleep(1 * time.Hour)
+	}
+}
+
+func checkCertificate(domain string) {
+	rootCAs, err := x509.SystemCertPool()
+	if err != nil {
+		log.Printf("Error loading system certification pool: %v", err)
+		rootCAs = x509.NewCertPool()
+	}
+
+	config := &tls.Config{
+		RootCAs:    rootCAs,
+		ServerName: domain,
+	}
+
+	conn, err := tls.Dial("tcp", domain+":443", config)
+
+	certValid := 0.0
+	daysUntilExpiry := 0.0
path: src/backend/app.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Harden TLS settings – enforce a minimum version**

Static analysis correctly warns that the TLS dialer accepts TLS 1.0. Bump to TLS 1.3 (or at least 1.2):

```diff
  config := &tls.Config{
     RootCAs:    rootCAs,
     ServerName: domain,
+    MinVersion: tls.VersionTLS12,
  }
```




<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
	config := &tls.Config{
		RootCAs:    rootCAs,
		ServerName: domain,
		MinVersion: tls.VersionTLS12,
	}

	conn, err := tls.Dial("tcp", domain+":443", config)

	certValid := 0.0
	daysUntilExpiry := 0.0
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 ast-grep (0.31.1)</summary>

[warning] 176-179: MinVersion` is missing from this TLS configuration.  By default, TLS 1.2 is currently used as the minimum when acting as a client, and TLS 1.0 when acting as a server. General purpose web applications should default to TLS 1.3 with all other protocols disabled.  Only where it is known that a web server must support legacy clients with unsupported an insecure browsers (such as Internet Explorer 10), it may be necessary to enable TLS 1.0 to provide support. Add `MinVersion: tls.VersionTLS13' to the TLS configuration to bump the minimum version to TLS 1.3.
Context: tls.Config{
		RootCAs:    rootCAs,
		ServerName: domain,
	}
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
     https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(missing-ssl-minversion-go)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T11:39:15Z
author_association: NONE
diff_hunk: @@ -0,0 +1,31 @@
+services:
+  prometheus:
+    image: prom/prometheus@sha256:b41a46cf049d3865d87ba72f6c531b688f1651a579c86f31584f41ee6d45a437
+    ports:
+      - "9090:9090"
+    volumes:
+      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+      - prometheus_data:/prometheus
+    command:
+      - '--config.file=/etc/prometheus/prometheus.yml'
+      - '--storage.tsdb.path=/prometheus'
+      - '--web.console.libraries=/etc/prometheus/console.libraries'
+      - '--web.console.templates=/etc/prometheus/consoles'
+    restart: unless-stopped
+
+  grafana:
+    image: grafana/grafana@sha256:95173c11b36f53a423ce47f00baee3f913a3e403f202a83c8bc3c7115a2cfdd0
+    ports:
+      - "3000:3000"
+    volumes:
+      - grafana_data:/var/lib/grafana
+    environment:
+      - GF_SECURITY_ADMIN_PASSWORD=${GF_ADMIN_PASSWORD}
+      - GF_USERS_ALLOWS_SIGNUP=false
+    depends_on:
+      - prometheus
+    restart: unless-stopped
+
+volumes:
+  prometheus_data:
+  grafana_data:  
path: docker-compose.yml
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove trailing spaces and add final newline.**

Line 31 has trailing spaces and the file is missing a terminating newline. This can lead to YAML parsing errors.


```diff
 volumes:
   prometheus_data:
-  grafana_data:  
+  grafana_data:
+
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
volumes:
  prometheus_data:
  grafana_data:

`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 YAMLlint (1.35.1)</summary>

[warning] 31-31: too many spaces after colon

(colons)

---

[error] 31-31: no new line character at the end of file

(new-line-at-end-of-file)

---

[error] 31-31: trailing spaces

(trailing-spaces)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T11:39:15Z
author_association: NONE
diff_hunk: @@ -0,0 +1,31 @@
+services:
+  prometheus:
+    image: prom/prometheus@sha256:b41a46cf049d3865d87ba72f6c531b688f1651a579c86f31584f41ee6d45a437
+    ports:
+      - "9090:9090"
+    volumes:
+      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
+      - prometheus_data:/prometheus
+    command:
+      - '--config.file=/etc/prometheus/prometheus.yml'
+      - '--storage.tsdb.path=/prometheus'
+      - '--web.console.libraries=/etc/prometheus/console.libraries'
+      - '--web.console.templates=/etc/prometheus/consoles'
+    restart: unless-stopped
+
+  grafana:
+    image: grafana/grafana@sha256:95173c11b36f53a423ce47f00baee3f913a3e403f202a83c8bc3c7115a2cfdd0
+    ports:
+      - "3000:3000"
+    volumes:
+      - grafana_data:/var/lib/grafana
+    environment:
+      - GF_SECURITY_ADMIN_PASSWORD=${GF_ADMIN_PASSWORD}
+      - GF_USERS_ALLOWS_SIGNUP=false
path: docker-compose.yml
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

_⚠️ Potential issue_

**Correct Grafana signup flag and secure admin password.**

The variable `GF_USERS_ALLOWS_SIGNUP` is incorrect (should be `GF_USERS_ALLOW_SIGNUP`). Additionally, storing the admin password in plain env vars is not recommended—use Docker secrets instead.


```diff
 services:
   grafana:
     environment:
-      - GF_SECURITY_ADMIN_PASSWORD=${GF_ADMIN_PASSWORD}
-      - GF_USERS_ALLOWS_SIGNUP=false
+      - GF_SECURITY_ADMIN_PASSWORD_FILE=/run/secrets/grafana_admin_password
+      - GF_USERS_ALLOW_SIGNUP=false
 secrets:
+  grafana_admin_password:
+    file: ./secrets/grafana_admin_password.txt
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T11:39:15Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-30T11:34:25Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces comprehensive observability and monitoring features to the backend application. Prometheus and Grafana are integrated via new Docker Compose configurations, and a Prometheus configuration file is added for scraping metrics from the backend. The backend code is enhanced with Prometheus metrics for HTTP requests, CPU load, TLS certificate health, and user registrations, along with middleware for metrics collection and background monitoring routines. Search logging is improved, and fallback search logic is implemented. Dependency versions are updated, and environment variable management is tightened in the `.gitignore` and Docker Compose files.

## Changes

| File(s)                                           | Change Summary |
|---------------------------------------------------|---------------|
| `.gitignore`                                      | Added rule to ignore all `.env*` files for environment variable management. |
| `docker-compose.dev.yml`                          | Added `SEARCH_LOG_PATH` environment variable to the `app` service. |
| `docker-compose.yml`                              | Introduced new Compose file defining Prometheus and Grafana services with persistent volumes and environment configuration. |
| `prometheus/prometheus.yml`                       | Added Prometheus configuration to scrape metrics from the backend at 15-second intervals. |
| `go.mod`                                          | Upgraded dependency versions and added new indirect dependencies related to monitoring and system utilities. |
| `src/backend/app.go`                              | Integrated Prometheus metrics for HTTP traffic, CPU load, TLS certificates, and user registrations; added metrics middleware, background monitoring goroutines, enhanced search logging, and fallback SQL search logic. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant Backend
    participant Prometheus
    participant Grafana

    Client->>Backend: HTTP Request (/search, /register, etc.)
    Backend->>Backend: Middleware collects metrics (requests, duration, status)
    Backend->>Backend: Background goroutines monitor CPU and TLS certs
    Backend->>Backend: Log search queries and client IPs
    Backend->>Backend: Increment user registration metric on /register
    Prometheus->>Backend: Scrape /metrics endpoint every 15s
    Backend-->>Prometheus: Serve collected metrics
    Grafana->>Prometheus: Query metrics for dashboards
```

## Suggested reviewers

- mitanuriel
- BlablaTara

## Poem

> In the warren, metrics bloom and grow,  
> Prometheus watches, Grafana puts on a show.  
> Search logs are burrowed, certs checked with care,  
> CPU and users—rabbits everywhere!  
> With every hop, the backend’s in sight,  
> Monitoring magic, day and night.  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=DevOops23b/GoSearch&utm_content=167):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-30T11:34:56Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DevOops23b_GoSearch&pullRequest=167) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=DevOops23b_GoSearch&pullRequest=167&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DevOops23b_GoSearch&pullRequest=167&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DevOops23b_GoSearch&pullRequest=167&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DevOops23b_GoSearch&pullRequest=167&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DevOops23b_GoSearch&pullRequest=167&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=DevOops23b_GoSearch&pullRequest=167)


author_association: NONE