package main import ( "context" "encoding/json" "fmt" "log/slog" "net/http" "os/exec" "strconv" "strings" "time" "github.com/elastic/go-elasticsearch/v8" ) type WebService struct { server *http.Server esClient *elasticsearch.Client config *Config } func NewWebService(config *Config, esClient *elasticsearch.Client) *WebService { mux := http.NewServeMux() ws := &WebService{ esClient: esClient, config: config, } mux.HandleFunc("/export", ws.handleExport) mux.HandleFunc("/health", ws.handleHealth) mux.HandleFunc("/indices", ws.handleIndices) addr := fmt.Sprintf("%s:%d", config.WebService.Host, config.WebService.Port) ws.server = &http.Server{ Addr: addr, Handler: mux, ReadTimeout: 30 * time.Second, WriteTimeout: 300 * time.Second, IdleTimeout: 60 * time.Second, } return ws } func (ws *WebService) Start(ctx context.Context) error { go func() { <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := ws.server.Shutdown(shutdownCtx); err != nil { slog.Error("web service shutdown error", "error", err) } }() slog.Info("Starting web service", "address", ws.server.Addr) if err := ws.server.ListenAndServe(); err != http.ErrServerClosed { return fmt.Errorf("web service error: %w", err) } return nil } func (ws *WebService) handleExport(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } indices := ws.parseIndicesParam(r) if len(indices) == 0 { http.Error(w, "No indices specified. Use ?indices=index1,index2", http.StatusBadRequest) return } size := ws.parseSizeParam(r) since := ws.parseSinceParam(r) slog.Info("Export request received", "indices", indices, "size", size, "since", since) w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Disposition", "attachment; filename=elasticsearch_export.json") exporter := NewElasticsearchExporter(ws.esClient) if err := exporter.ExportToStream(r.Context(), indices, size, since, w); err != nil { slog.Error("export error", "error", err) http.Error(w, fmt.Sprintf("Export error: %v", err), http.StatusInternalServerError) return } slog.Info("Export completed successfully", "indices", indices) } func (ws *WebService) handleHealth(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() res, err := ws.esClient.Info(ws.esClient.Info.WithContext(ctx)) if err != nil { w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(map[string]any{ "status": "unhealthy", "error": err.Error(), }) return } res.Body.Close() if res.IsError() { w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(map[string]any{ "status": "unhealthy", "error": res.String(), }) return } statusMap := make(map[string]any) statusMap["elasticsearch"] = map[string]any{"status": "healthy", "timestamp": time.Now()} for _, service := range ws.config.Services { statusCommand := []string{"sudo", "systemctl", "status", service.Name, "--no-pager"} if service.Enabled { serviceStatus, err := exec.Command(statusCommand[0], statusCommand[1:]...).Output() if err != nil { slog.Error("error executing status command", "error", err) continue } lines := strings.SplitSeq(string(serviceStatus), "\n") for line := range lines { if strings.Contains(line, "Active:") { serviceHealth, found := strings.CutPrefix(strings.TrimSpace(line), "Active:") if found { statusMap[service.Name] = map[string]any{"status": serviceHealth, "timestamp": time.Now()} } } } } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(statusMap) } func (ws *WebService) handleIndices(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() res, err := ws.esClient.Cat.Indices( ws.esClient.Cat.Indices.WithContext(ctx), ws.esClient.Cat.Indices.WithFormat("json"), ) if err != nil { http.Error(w, fmt.Sprintf("Error fetching indices: %v", err), http.StatusInternalServerError) return } defer res.Body.Close() if res.IsError() { http.Error(w, fmt.Sprintf("Elasticsearch error: %s", res.String()), http.StatusInternalServerError) return } var indices []map[string]any if err := json.NewDecoder(res.Body).Decode(&indices); err != nil { http.Error(w, fmt.Sprintf("Error decoding response: %v", err), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "indices": indices, "count": len(indices), }) } func (ws *WebService) parseIndicesParam(r *http.Request) []string { indicesParam := r.URL.Query().Get("indices") if indicesParam == "" { return nil } indices := strings.Split(indicesParam, ",") var result []string for _, index := range indices { index = strings.TrimSpace(index) if index != "" { result = append(result, index) } } return result } func (ws *WebService) parseSinceParam(r *http.Request) int { sinceParam := r.URL.Query().Get("since") if sinceParam == "" { return 0 } since, err := strconv.Atoi(sinceParam) if err != nil { return 0 } return since } func (ws *WebService) parseSizeParam(r *http.Request) int { sizeParam := r.URL.Query().Get("size") if sizeParam == "" { return 1000 } size, err := strconv.Atoi(sizeParam) if err != nil || size <= 0 { return 1000 } if size > 10000 { size = 10000 } return size }