diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ef8e83c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.github +.cursor +.vscode +bin +dist +demo +testdata +*.md +*.log +*.tmp +*.out +coverage.out +deploy/nginx.conf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f5bfa4..9056332 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,3 +60,126 @@ jobs: - name: Validate rules run: ./bin/metrics-analyzer validate ./automated-rules + container-smoke: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + + - name: Read version + id: version + run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" + + - name: Set build time + id: build_time + run: echo "build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" + + - name: Build image + run: | + docker build \ + --build-arg VERSION="${{ steps.version.outputs.version }}" \ + --build-arg BUILD_TIME="${{ steps.build_time.outputs.build_time }}" \ + -t sma:ci . + + - name: Start container + run: | + docker run -d --rm --name sma-ci -p 8080:8080 sma:ci + + - name: Wait for readiness + run: | + for i in {1..10}; do + if curl -fsS http://localhost:8080/health >/dev/null; then + exit 0 + fi + sleep 1 + done + docker logs sma-ci + exit 1 + + - name: Check version endpoint + run: | + for i in {1..5}; do + if curl -fsS http://localhost:8080/version -o /tmp/version.json; then + if [ -s /tmp/version.json ]; then + break + fi + fi + sleep 1 + done + if [ ! -s /tmp/version.json ]; then + echo "version response was empty" + cat /tmp/version.json || true + docker logs sma-ci || true + exit 1 + fi + python3 - <<'PY' + import json + with open("/tmp/version.json") as f: + data = json.load(f) + assert "version" in data + assert "lastUpdate" in data + print("version ok") + PY + + - name: Analyze sample metrics + run: | + curl -fsS -o /tmp/analysis.json -X POST \ + -F "file=@testdata/fixtures/sample_metrics.txt" \ + http://localhost:8080/api/analyze/both + python3 - <<'PY' + import json + with open("/tmp/analysis.json") as f: + data = json.load(f) + assert "console" in data + assert "markdown" in data + print("analysis ok") + PY + + - name: Show container logs on failure + if: failure() + run: docker logs sma-ci + + container-image: + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + env: + QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }} + QUAY_PASSWORD: ${{ secrets.QUAY_PASSWORD }} + steps: + - uses: actions/checkout@v4 + + - name: Read version + id: version + run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" + + - name: Set build time + id: build_time + run: echo "build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Quay + id: login + if: ${{ env.QUAY_USERNAME != '' && env.QUAY_PASSWORD != '' }} + uses: docker/login-action@v3 + with: + registry: quay.io + username: ${{ env.QUAY_USERNAME }} + password: ${{ env.QUAY_PASSWORD }} + + - name: Build and push image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: ${{ steps.login.conclusion == 'success' }} + tags: | + quay.io/prygiels/sma:${{ steps.version.outputs.version }} + quay.io/prygiels/sma:${{ github.sha }} + quay.io/prygiels/sma:latest + build-args: | + VERSION=${{ steps.version.outputs.version }} + BUILD_TIME=${{ steps.build_time.outputs.build_time }} + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e2ad497 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /src + +RUN apk add --no-cache git + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +ARG VERSION=dev +ARG BUILD_TIME="" + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -ldflags "-X main.buildVersion=${VERSION} -X main.buildTime=${BUILD_TIME}" \ + -o /out/web-server ./web/server + +FROM alpine:3.19 + +WORKDIR /app + +RUN apk add --no-cache ca-certificates nginx +RUN adduser -D -H -u 10001 appuser + +COPY --from=builder /out/web-server /app/web-server +COPY web/static /app/web/static +COPY automated-rules /app/automated-rules +COPY templates/markdown.tmpl /app/templates/markdown.tmpl +COPY deploy/nginx.container.conf /etc/nginx/nginx.conf +COPY deploy/container-entrypoint.sh /app/entrypoint.sh + +RUN chmod +x /app/entrypoint.sh \ + && chown -R 10001:10001 /app /etc/nginx + +EXPOSE 8080 + +USER 10001 + +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/VERSION b/VERSION index bcab45a..81340c7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.3 +0.0.4 diff --git a/charts/sensor-metrics-analyzer/Chart.yaml b/charts/sensor-metrics-analyzer/Chart.yaml new file mode 100644 index 0000000..7ba1740 --- /dev/null +++ b/charts/sensor-metrics-analyzer/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: sma +description: Sensor Metrics Analyzer web server +type: application +version: 0.1.0 +appVersion: "0.0.3" diff --git a/charts/sensor-metrics-analyzer/templates/_helpers.tpl b/charts/sensor-metrics-analyzer/templates/_helpers.tpl new file mode 100644 index 0000000..f4177ca --- /dev/null +++ b/charts/sensor-metrics-analyzer/templates/_helpers.tpl @@ -0,0 +1,19 @@ +{{- define "sensor-metrics-analyzer.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "sensor-metrics-analyzer.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s" (include "sensor-metrics-analyzer.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "sensor-metrics-analyzer.labels" -}} +app.kubernetes.io/name: {{ include "sensor-metrics-analyzer.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | quote }} +{{- end -}} diff --git a/charts/sensor-metrics-analyzer/templates/deployment.yaml b/charts/sensor-metrics-analyzer/templates/deployment.yaml new file mode 100644 index 0000000..520a820 --- /dev/null +++ b/charts/sensor-metrics-analyzer/templates/deployment.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "sensor-metrics-analyzer.fullname" . }} + labels: + {{- include "sensor-metrics-analyzer.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "sensor-metrics-analyzer.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "sensor-metrics-analyzer.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: app + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: LISTEN_ADDR + value: ":8081" + - name: RULES_DIR + value: {{ .Values.config.rulesDir | quote }} + - name: LOAD_LEVEL_DIR + value: {{ .Values.config.loadLevelDir | quote }} + - name: TEMPLATE_PATH + value: {{ .Values.config.templatePath | quote }} + - name: MAX_FILE_SIZE + value: {{ .Values.config.maxFileSize | quote }} + - name: REQUEST_TIMEOUT + value: {{ .Values.config.requestTimeout | quote }} + ports: + - name: http + containerPort: 8080 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + runAsNonRoot: true + runAsUser: 10001 + runAsGroup: 10001 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} diff --git a/charts/sensor-metrics-analyzer/templates/ingress.yaml b/charts/sensor-metrics-analyzer/templates/ingress.yaml new file mode 100644 index 0000000..294ec2d --- /dev/null +++ b/charts/sensor-metrics-analyzer/templates/ingress.yaml @@ -0,0 +1,33 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "sensor-metrics-analyzer.fullname" . }} + labels: + {{- include "sensor-metrics-analyzer.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + ingressClassName: {{ .Values.ingress.className }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "sensor-metrics-analyzer.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/sensor-metrics-analyzer/templates/service.yaml b/charts/sensor-metrics-analyzer/templates/service.yaml new file mode 100644 index 0000000..cbb35bb --- /dev/null +++ b/charts/sensor-metrics-analyzer/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "sensor-metrics-analyzer.fullname" . }} + labels: + {{- include "sensor-metrics-analyzer.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + app.kubernetes.io/name: {{ include "sensor-metrics-analyzer.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: 8080 diff --git a/charts/sensor-metrics-analyzer/values.yaml b/charts/sensor-metrics-analyzer/values.yaml new file mode 100644 index 0000000..a26c684 --- /dev/null +++ b/charts/sensor-metrics-analyzer/values.yaml @@ -0,0 +1,36 @@ +replicaCount: 2 + +image: + repository: quay.io/prygiels/sma + tag: "0.0.3" + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: nginx + annotations: {} + hosts: + - host: sensor-metrics.rhacs.io + paths: + - path: / + pathType: Prefix + tls: [] + +config: + rulesDir: /app/automated-rules + loadLevelDir: /app/automated-rules/load-level + templatePath: /app/templates/markdown.tmpl + maxFileSize: "52428800" + requestTimeout: "60s" + +resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi diff --git a/deploy/container-entrypoint.sh b/deploy/container-entrypoint.sh new file mode 100644 index 0000000..fb0497c --- /dev/null +++ b/deploy/container-entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -e + +if [ -d /dev/shm ]; then + export TMPDIR=/dev/shm + mkdir -p \ + /dev/shm/nginx/client_body \ + /dev/shm/nginx/proxy \ + /dev/shm/nginx/fastcgi \ + /dev/shm/nginx/uwsgi \ + /dev/shm/nginx/scgi +else + mkdir -p \ + /tmp/nginx/client_body \ + /tmp/nginx/proxy \ + /tmp/nginx/fastcgi \ + /tmp/nginx/uwsgi \ + /tmp/nginx/scgi +fi + +if [ -z "${LISTEN_ADDR:-}" ]; then + export LISTEN_ADDR=":8081" +fi + +/app/web-server & + +exec nginx -e /dev/stderr -g 'daemon off;' -c /etc/nginx/nginx.conf diff --git a/web/nginx.conf b/deploy/nginx.conf similarity index 100% rename from web/nginx.conf rename to deploy/nginx.conf diff --git a/deploy/nginx.container.conf b/deploy/nginx.container.conf new file mode 100644 index 0000000..7e41c3e --- /dev/null +++ b/deploy/nginx.container.conf @@ -0,0 +1,70 @@ +worker_processes 1; +pid /tmp/nginx.pid; +error_log /dev/stderr info; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + client_body_in_file_only off; + client_body_buffer_size 50m; + client_body_temp_path /dev/shm/nginx/client_body; + proxy_temp_path /dev/shm/nginx/proxy; + fastcgi_temp_path /dev/shm/nginx/fastcgi; + uwsgi_temp_path /dev/shm/nginx/uwsgi; + scgi_temp_path /dev/shm/nginx/scgi; + + log_format minimal '$request_method $uri $status $body_bytes_sent'; + access_log /dev/stdout minimal; + error_log /dev/stderr warn; + + server { + listen 8080; + server_name _; + server_tokens off; + + client_max_body_size 50m; + + root /app/web/static; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + } + + location /health { + proxy_pass http://127.0.0.1:8081/health; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + location /version { + proxy_pass http://127.0.0.1:8081/version; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + } +} diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 4aaaa3d..cb58fd3 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -3,6 +3,7 @@ package analyzer import ( "fmt" "io" + "os" "path/filepath" "strings" @@ -24,6 +25,19 @@ type Options struct { // AnalyzeFile parses metrics and evaluates rules, returning the analysis report. func AnalyzeFile(metricsFile string, opts Options) (rules.AnalysisReport, error) { + if opts.ClusterName == "" { + opts.ClusterName = ExtractClusterName(metricsFile) + } + file, err := os.Open(metricsFile) + if err != nil { + return rules.AnalysisReport{}, err + } + defer file.Close() + return AnalyzeReader(file, opts) +} + +// AnalyzeReader parses metrics from a reader and evaluates rules, returning the analysis report. +func AnalyzeReader(reader io.Reader, opts Options) (rules.AnalysisReport, error) { logOut := opts.Logger if logOut == nil { logOut = io.Discard @@ -39,11 +53,6 @@ func AnalyzeFile(metricsFile string, opts Options) (rules.AnalysisReport, error) loadLevelDir = filepath.Join(rulesDir, "load-level") } - clusterName := opts.ClusterName - if clusterName == "" { - clusterName = ExtractClusterName(metricsFile) - } - fmt.Fprintf(logOut, "Loading load detection rules from %s...\n", loadLevelDir) loadRules, err := rules.LoadLoadDetectionRules(loadLevelDir) if err != nil { @@ -58,8 +67,8 @@ func AnalyzeFile(metricsFile string, opts Options) (rules.AnalysisReport, error) } fmt.Fprintf(logOut, "Loaded %d rules\n", len(rulesList)) - fmt.Fprintf(logOut, "Parsing metrics from %s...\n", metricsFile) - metrics, err := parser.ParseFile(metricsFile) + fmt.Fprintf(logOut, "Parsing metrics from reader...\n") + metrics, err := parser.ParseReader(reader) if err != nil { return rules.AnalysisReport{}, fmt.Errorf("failed to parse metrics: %w", err) } @@ -85,7 +94,7 @@ func AnalyzeFile(metricsFile string, opts Options) (rules.AnalysisReport, error) fmt.Fprintf(logOut, "Evaluating rules...\n") report := evaluator.EvaluateAllRules(rulesList, metrics, detectedLoadLevel, acsVersion) - report.ClusterName = clusterName + report.ClusterName = opts.ClusterName return report, nil } diff --git a/internal/parser/prometheus.go b/internal/parser/prometheus.go index 2d03043..6652439 100644 --- a/internal/parser/prometheus.go +++ b/internal/parser/prometheus.go @@ -2,6 +2,7 @@ package parser import ( "bufio" + "io" "os" "regexp" "sort" @@ -39,16 +40,20 @@ var ( simpleMetricRegex = regexp.MustCompile(`^([a-zA-Z_][a-zA-Z0-9_:]*)\s+(.+)$`) ) -// ParseFile parses a Prometheus metrics file +// ParseFile parses a Prometheus metrics file. func ParseFile(filepath string) (MetricsData, error) { file, err := os.Open(filepath) if err != nil { return nil, err } defer file.Close() + return ParseReader(file) +} +// ParseReader parses Prometheus metrics from a reader. +func ParseReader(reader io.Reader) (MetricsData, error) { metrics := make(MetricsData) - scanner := bufio.NewScanner(file) + scanner := bufio.NewScanner(reader) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) @@ -246,6 +251,7 @@ func (md MetricsData) GetHistogramCount(baseName string) (float64, bool) { func (md MetricsData) DetectACSVersion() (string, bool) { // Try common version metrics versionMetrics := []string{ + "rox_sensor_info", "rox_sensor_version_info", "rox_central_version_info", "rox_version", @@ -263,6 +269,9 @@ func (md MetricsData) DetectACSVersion() (string, bool) { if version, ok := v.Labels["rox_version"]; ok && version != "" { return version, true } + if version, ok := v.Labels["sensor_version"]; ok && version != "" { + return version, true + } } } } diff --git a/templates/markdown.tmpl b/templates/markdown.tmpl index 4a1a7fa..ac0ee82 100644 --- a/templates/markdown.tmpl +++ b/templates/markdown.tmpl @@ -1,9 +1,9 @@ # Automated Metrics Analysis Report -**Cluster:** {{.ClusterName}} -**ACS Version:** {{.ACSVersion}} -**Load Level:** {{.LoadLevel}} -**Generated:** {{.Timestamp.Format "2006-01-02 15:04:05"}} +- **Cluster:** {{.ClusterName}} +- **ACS Version:** {{.ACSVersion}} +- **Load Level:** {{.LoadLevel}} +- **Report Generated:** {{.Timestamp.Format "2006-01-02 15:04:05"}} ## Summary @@ -25,7 +25,7 @@ {{ $r.Message }} {{ if $r.ReviewStatus }} #### Review status -This alert was generated by evaluating a rule. That rule was reviewed by: +This alert was generated by evaluating a rule. That rule was reviewed: {{ $r.ReviewStatus }} {{ end }} @@ -62,7 +62,7 @@ This alert was generated by evaluating a rule. That rule was reviewed by: {{ $r.Message }} {{ if $r.ReviewStatus }} #### Review status -This warning was generated by evaluating a rule. That rule was reviewed by: +This warning was generated by evaluating a rule. That rule was reviewed: {{ $r.ReviewStatus }} {{ end }} diff --git a/web/DEPLOYMENT.md b/web/DEPLOYMENT.md deleted file mode 100644 index c19fa13..0000000 --- a/web/DEPLOYMENT.md +++ /dev/null @@ -1,207 +0,0 @@ -# Deployment Guide - -This guide covers deploying the Sensor Metrics Analyzer Web service on a Linux server. - -## Prerequisites - -- Linux server with systemd -- Nginx installed -- Go 1.21+ (for building, or use pre-built binaries) -- Root or sudo access - -## Step 1: Prepare the Application - -1. Clone or copy the repository to the server: - ```bash - cd /opt - git clone sensor-metrics-analyzer - # Or copy the files to /opt/sensor-metrics-analyzer - ``` - -2. Download the precompiled binary from GitHub Releases: - ```bash - cd /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go - curl -L -o bin/web-server https://github.com/stackrox/sensor-metrics-analyzer/releases/latest/download/web-server-linux-amd64 - chmod +x bin/web-server - ``` - -3. Verify the binary exists: - ```bash - ls -lh /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/ - # Should show: web-server - ``` - -## Step 2: Create System User - -Create a dedicated user for running the service: - -```bash -sudo useradd -r -s /bin/false -d /opt/sensor-metrics-analyzer sensor-metrics -sudo chown -R sensor-metrics:sensor-metrics /opt/sensor-metrics-analyzer -``` - -## Step 3: Configure Systemd Service - -1. Copy the service file: - ```bash - sudo cp /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/web/sensor-metrics-web.service \ - /etc/systemd/system/ - ``` - -2. Edit the service file to match your paths: - ```bash - sudo nano /etc/systemd/system/sensor-metrics-web.service - ``` - - Update these paths if different: - - `WorkingDirectory`: `/opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/web/server` - - `ExecStart`: `/opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/web-server` - - `RULES_DIR`: `/opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/automated-rules` - - `LOAD_LEVEL_DIR`: `/opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/automated-rules/load-level` - - `TEMPLATE_PATH`: `/opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/templates/markdown.tmpl` - -3. Reload systemd and start the service: - ```bash - sudo systemctl daemon-reload - sudo systemctl enable sensor-metrics-web - sudo systemctl start sensor-metrics-web - ``` - -4. Verify the service is running: - ```bash - sudo systemctl status sensor-metrics-web - ``` - -5. Check logs: - ```bash - sudo journalctl -u sensor-metrics-web -f - ``` - -## Step 4: Configure Nginx - -1. Copy the nginx configuration: - ```bash - sudo cp /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/web/nginx.conf \ - /etc/nginx/sites-available/sensor-metrics-web - ``` - -2. Edit the configuration: - ```bash - sudo nano /etc/nginx/sites-available/sensor-metrics-web - ``` - - Update: - - `root`: `/opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/web/static` - - `server_name`: Your domain name (or leave as `_` for default) - -3. Enable the site and disable the default nginx site: - ```bash - sudo rm /etc/nginx/sites-enabled/default - sudo ln -s /etc/nginx/sites-available/sensor-metrics-web \ - /etc/nginx/sites-enabled/ - ``` - -4. Test nginx configuration: - ```bash - sudo nginx -t - ``` - -5. Reload nginx: - ```bash - sudo systemctl reload nginx - ``` - -## Step 5: Verify Deployment - -1. Test the backend health endpoint directly: - ```bash - curl http://localhost:8080/health - ``` - -2. Test the nginx health endpoint: - ```bash - curl http://localhost/health - # Or: curl http://your-domain/health - ``` - -3. Test the API directly: - ```bash - curl -X POST http://localhost/api/analyze/both \ - -F "file=@/path/to/test-metrics.prom" - ``` - -4. Access the web interface: - Open `http://your-server-ip` or `http://your-domain` in a browser - - Note: The backend on `:8080` only serves API endpoints, so `/` will return 404. - -## Step 6: Firewall Configuration - -If using a firewall, allow HTTP traffic: - -```bash -# For UFW -sudo ufw allow 80/tcp - -# For firewalld -sudo firewall-cmd --permanent --add-service=http -sudo firewall-cmd --reload -``` - -## Troubleshooting - -### Service won't start - -1. Check service status: - ```bash - sudo systemctl status sensor-metrics-web - ``` - -2. Check logs: - ```bash - sudo journalctl -u sensor-metrics-web -n 50 - ``` - -3. Verify binary exists and is executable: - ```bash - ls -l /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/web-server - ``` - -4. Test running manually: - ```bash - sudo -u sensor-metrics /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/web-server \ - --rules /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/automated-rules - ``` - -### Nginx 502 Bad Gateway - -1. Verify backend is running: - ```bash - curl http://localhost:8080/health - ``` - -2. Check nginx error logs: - ```bash - sudo tail -f /var/log/nginx/error.log - ``` - -3. Verify proxy_pass URL in nginx config matches backend listen address - -### File upload fails - -1. Check nginx `client_max_body_size` setting -2. Verify file size is within limits (default: 50MB) -3. Check backend logs for detailed errors - -## Security Hardening - -1. **Use HTTPS**: Set up SSL/TLS certificates (Let's Encrypt recommended) -2. **Restrict access**: Use firewall rules to limit access to specific IPs if needed -3. **Regular updates**: Keep the application and system updated -4. **Monitor logs**: Set up log monitoring for suspicious activity - -## Next Steps - -- See [UPDATE.md](./UPDATE.md) for update procedures -- Configure log rotation if needed -- Set up monitoring/alerting for the service diff --git a/web/UPDATE.md b/web/UPDATE.md deleted file mode 100644 index 5f05695..0000000 --- a/web/UPDATE.md +++ /dev/null @@ -1,208 +0,0 @@ -# Update Guide - -This guide covers updating the Sensor Metrics Analyzer Web service. - -## Update Procedure - -### Step 1: Stop the Service - -```bash -sudo systemctl stop sensor-metrics-web -``` - -### Step 2: Backup Current Version (Optional but Recommended) - -```bash -# Backup binaries -sudo cp /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/web-server \ - /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/web-server.backup -``` - -### Step 3: Update the Application - -**Option A: Git Pull (if using git)** - -```bash -cd /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go -git pull origin main # or your branch name -``` - -**Option B: Manual Copy** - -Copy the new files to the server, preserving the directory structure. - -git add -### Step 4: Download Updated Binary - -```bash -cd /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go -curl -L -o bin/web-server https://github.com/stackrox/sensor-metrics-analyzer/releases/latest/download/web-server-linux-amd64 -chmod +x bin/web-server -``` - -### Step 5: Update Configuration Files (if needed) - -Check if any configuration files have changed: - -```bash -# Compare service file -diff /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/web/sensor-metrics-web.service \ - /etc/systemd/system/sensor-metrics-web.service - -# Compare nginx config -diff /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/web/nginx.conf \ - /etc/nginx/sites-available/sensor-metrics-web -``` - -If there are differences, update the files in `/etc/`: - -```bash -# Update systemd service (review changes first!) -sudo cp /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/web/sensor-metrics-web.service \ - /etc/systemd/system/ -sudo systemctl daemon-reload - -# Update nginx config (review changes first!) -sudo cp /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/web/nginx.conf \ - /etc/nginx/sites-available/sensor-metrics-web -sudo nginx -t # Test configuration -sudo systemctl reload nginx -``` - -### Step 6: Update Frontend Files - -```bash -sudo cp -r /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/web/static/* \ - /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/web/static/ -``` - -### Step 7: Verify Binaries - -```bash -# Check binaries exist and are executable -ls -lh /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/ - -# Test web server binary (should show usage) -/opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/web-server --help -``` - -### Step 8: Start the Service - -```bash -sudo systemctl start sensor-metrics-web -sudo systemctl status sensor-metrics-web -``` - -### Step 9: Verify Health - -```bash -# Check health endpoint -curl http://localhost:8080/health - -# Check service logs -sudo journalctl -u sensor-metrics-web -f -``` - -### Step 10: Test the Web Interface - -1. Open the web interface in a browser -2. Upload a test metrics file -3. Verify both console and markdown outputs are generated correctly - -## Rollback Procedure - -If something goes wrong, rollback to the previous version: - -```bash -# Stop service -sudo systemctl stop sensor-metrics-web - -# Restore binaries -sudo cp /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/web-server.backup \ - /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/web-server - -# Start service -sudo systemctl start sensor-metrics-web -``` - -## Automated Update Script - -You can create a simple update script: - -```bash -#!/bin/bash -# /opt/sensor-metrics-analyzer/update.sh - -set -e - -SERVICE_NAME="sensor-metrics-web" -APP_DIR="/opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go" -BIN_DIR="$APP_DIR/bin" - -echo "Stopping service..." -sudo systemctl stop $SERVICE_NAME - -echo "Backing up binaries..." -sudo cp $BIN_DIR/web-server $BIN_DIR/web-server.backup.$(date +%Y%m%d_%H%M%S) - -echo "Updating application..." -cd $APP_DIR -# git pull # Uncomment if using git - -echo "Downloading binary..." -curl -L -o $BIN_DIR/web-server https://github.com/stackrox/sensor-metrics-analyzer/releases/latest/download/web-server-linux-amd64 -chmod +x $BIN_DIR/web-server - -echo "Starting service..." -sudo systemctl start $SERVICE_NAME - -echo "Waiting for service to start..." -sleep 2 - -echo "Checking service status..." -sudo systemctl status $SERVICE_NAME --no-pager - -echo "Testing health endpoint..." -curl -s http://localhost:8080/health || echo "Health check failed!" - -echo "Update complete!" -``` - -Make it executable: -```bash -chmod +x /opt/sensor-metrics-analyzer/update.sh -``` - -## Update Checklist - -- [ ] Stop the service -- [ ] Backup current binaries -- [ ] Update application files -- [ ] Rebuild binaries -- [ ] Update configuration files (if changed) -- [ ] Update frontend files -- [ ] Verify binaries -- [ ] Start the service -- [ ] Verify health endpoint -- [ ] Test web interface -- [ ] Monitor logs for errors - -## Troubleshooting Updates - -### Service fails to start after update - -1. Check logs: `sudo journalctl -u sensor-metrics-web -n 50` -2. Verify binary exists: `ls -l /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/web-server` -3. Test binary manually: `sudo -u sensor-metrics /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/web-server --help` -4. Rollback if needed - -### Binary not found errors - -- Verify the build completed successfully -- Check file permissions: `sudo chown sensor-metrics:sensor-metrics /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/*` -- Ensure binaries are executable: `sudo chmod +x /opt/sensor-metrics-analyzer/sensor-metrics-analyzer-go/bin/*` - -### Configuration errors - -- Review configuration file changes before applying -- Test nginx config: `sudo nginx -t` -- Reload systemd after service file changes: `sudo systemctl daemon-reload` diff --git a/web/server/main.go b/web/server/main.go index 0a04e7a..e6798bd 100644 --- a/web/server/main.go +++ b/web/server/main.go @@ -5,7 +5,6 @@ import ( "encoding/json" "flag" "fmt" - "io" "log" "net/http" "os" @@ -168,30 +167,14 @@ func handleAnalyzeBoth(cfg *Config) http.HandlerFunc { } defer file.Close() - // Create temporary file - tmpFile, err := os.CreateTemp("", "metrics-*.prom") - if err != nil { - respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create temp file: %v", err)) - return - } - defer os.Remove(tmpFile.Name()) - defer tmpFile.Close() - - // Copy uploaded file to temp file - if _, err := io.Copy(tmpFile, file); err != nil { - respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to save file: %v", err)) - return - } - tmpFile.Close() - - log.Printf("Processing file: %s (%d bytes)", header.Filename, header.Size) + log.Printf("Processing upload (%d bytes)", header.Size) response := AnalyzeResponse{} - report, err := analyzer.AnalyzeFile(tmpFile.Name(), analyzer.Options{ + report, err := analyzer.AnalyzeReader(file, analyzer.Options{ RulesDir: cfg.RulesDir, LoadLevelDir: cfg.LoadLevelDir, ClusterName: analyzer.ExtractClusterName(header.Filename), - Logger: io.Discard, + Logger: log.New(os.Stdout, "analyzer: ", log.LstdFlags).Writer(), }) if err := ctx.Err(); err != nil { respondError(w, http.StatusRequestTimeout, "Request timed out") diff --git a/web/static/index.html b/web/static/index.html index d67ad88..4516f07 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -328,30 +328,38 @@

Sensor Metrics Analyzer

const releasesUrl = 'https://github.com/stackrox/sensor-metrics-analyzer/releases'; releaseLink.href = releasesUrl; - fetch('/version') - .then(response => response.ok ? response.json() : null) - .then(data => { - if (!data) { - throw new Error('Failed to fetch version info'); - } - if (data.version) { - versionValue.textContent = data.version; - } - if (data.lastUpdate) { - const date = new Date(data.lastUpdate); - lastUpdateValue.textContent = Number.isNaN(date.getTime()) - ? data.lastUpdate - : new Intl.DateTimeFormat(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric' - }).format(date); - } - }) - .catch(() => { - versionValue.textContent = 'Unavailable'; - lastUpdateValue.textContent = 'Unavailable'; - }); + const fetchVersionInfo = (attempt = 1) => { + fetch('/version', { cache: 'no-store' }) + .then(response => response.ok ? response.json() : null) + .then(data => { + if (!data) { + throw new Error('Failed to fetch version info'); + } + if (data.version) { + versionValue.textContent = data.version; + } + if (data.lastUpdate) { + const date = new Date(data.lastUpdate); + lastUpdateValue.textContent = Number.isNaN(date.getTime()) + ? data.lastUpdate + : new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric' + }).format(date); + } + }) + .catch(() => { + if (attempt < 3) { + setTimeout(() => fetchVersionInfo(attempt + 1), 1000 * attempt); + return; + } + versionValue.textContent = 'Unavailable'; + lastUpdateValue.textContent = 'Unavailable'; + }); + }; + + fetchVersionInfo(); // File input change fileInput.addEventListener('change', (e) => {