internal/stack/dump.go (103 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package stack
import (
"context"
"fmt"
"os"
"path/filepath"
"slices"
"time"
"github.com/elastic/elastic-package/internal/logger"
"github.com/elastic/elastic-package/internal/profile"
)
const (
elasticAgentService = "elastic-agent"
fleetServerService = "fleet-server"
)
// DumpOptions defines dumping options for Elatic stack data.
type DumpOptions struct {
Profile *profile.Profile
// Output is the path where the logs are copied. If not defined, logs are only returned as part of the dump results.
Output string
// Services is the list of services to get the logs from. If not defined, logs from all available services are dumped.
Services []string
// Since is the time to dump logs from.
Since time.Time
}
// DumpResult contains the result of a dump operation.
type DumpResult struct {
ServiceName string
Logs []byte
LogsFile string
InternalLogsDir string
}
// Dump function exports stack data and dumps them as local artifacts, which can be used for debug purposes.
func Dump(ctx context.Context, options DumpOptions) ([]DumpResult, error) {
logger.Debugf("Dump Elastic stack data")
results, err := dumpStackLogs(ctx, options)
if err != nil {
return nil, fmt.Errorf("can't dump Elastic stack logs: %w", err)
}
return results, nil
}
func dumpStackLogs(ctx context.Context, options DumpOptions) ([]DumpResult, error) {
logger.Debugf("Dump stack logs (location: %s)", options.Output)
err := os.RemoveAll(options.Output)
if err != nil {
return nil, fmt.Errorf("can't remove output location: %w", err)
}
localServices := &localServicesManager{
profile: options.Profile,
}
services, err := localServices.serviceNames()
if err != nil {
return nil, fmt.Errorf("failed to get local services: %w", err)
}
for _, requestedService := range options.Services {
if !slices.Contains(services, requestedService) {
return nil, fmt.Errorf("%w: local service %s does not exist", ErrUnavailableStack, requestedService)
}
}
var results []DumpResult
for _, serviceName := range services {
if len(options.Services) > 0 && !slices.Contains(options.Services, serviceName) {
continue
}
logger.Debugf("Dump stack logs for %s", serviceName)
content, err := dockerComposeLogsSince(ctx, serviceName, options.Profile, options.Since)
if err != nil {
return nil, fmt.Errorf("can't fetch service logs (service: %s): %v", serviceName, err)
}
if options.Output == "" {
results = append(results, DumpResult{
ServiceName: serviceName,
Logs: content,
})
continue
}
result := DumpResult{
ServiceName: serviceName,
}
logsPath := filepath.Join(options.Output, "logs")
err = os.MkdirAll(logsPath, 0755)
if err != nil {
return nil, fmt.Errorf("can't create output location (path: %s): %w", logsPath, err)
}
logPath, err := writeLogFiles(logsPath, serviceName, content)
if err != nil {
return nil, fmt.Errorf("can't write log files for service %q: %w", serviceName, err)
}
result.LogsFile = logPath
switch serviceName {
case elasticAgentService, fleetServerService:
logPath, err := copyDockerInternalLogs(serviceName, logsPath, options.Profile)
if err != nil {
return nil, fmt.Errorf("can't copy internal logs (service: %s): %w", serviceName, err)
}
result.InternalLogsDir = logPath
}
results = append(results, result)
}
return results, nil
}
func writeLogFiles(logsPath, serviceName string, content []byte) (string, error) {
logPath := filepath.Join(logsPath, serviceName+".log")
err := os.WriteFile(logPath, content, 0644)
if err != nil {
return "", fmt.Errorf("can't write service logs (service: %s): %v", serviceName, err)
}
return logPath, nil
}