Skip to content

Commit eba9ed5

Browse files
authored
Generalize dump stack function and fix issue when checking elasticsearch logs (elastic#1809)
Merge the multiple functionality to dump logs into a single `Dump` method, and add different implementations for each stack provider. `Dump` can be used now to dump logs to file or to variable, and to collect logs only since a given time. This replaces `GetServiceLogs`. `Dump` implementation in the serverless provider returns an specific error telling that this functionality is not implemented for any service except elastic-package. This is used to fix a regression that was producing failures when trying to get logs from elasticsearch. Some errors that were previously only logged or ignored are returned and raised now.
1 parent 45b0921 commit eba9ed5

File tree

8 files changed

+151
-70
lines changed

8 files changed

+151
-70
lines changed

internal/stack/dump.go

Lines changed: 71 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"os"
1111
"path/filepath"
12+
"slices"
1213
"time"
1314

1415
"github.com/elastic/elastic-package/internal/logger"
@@ -22,82 +23,112 @@ const (
2223

2324
// DumpOptions defines dumping options for Elatic stack data.
2425
type DumpOptions struct {
25-
Output string
2626
Profile *profile.Profile
27-
}
2827

29-
func GetServiceLogs(ctx context.Context, serviceName string, profile *profile.Profile, since time.Time) ([]byte, error) {
30-
services, err := localServiceNames(DockerComposeProjectName(profile))
31-
if err != nil {
32-
return nil, fmt.Errorf("failed to get local services: %w", err)
33-
}
28+
// Output is the path where the logs are copied. If not defined, logs are only returned as part of the dump results.
29+
Output string
3430

35-
for _, service := range services {
36-
if service != serviceName {
37-
continue
38-
}
31+
// Services is the list of services to get the logs from. If not defined, logs from all available services are dumped.
32+
Services []string
3933

40-
return dockerComposeLogsSince(ctx, serviceName, profile, since)
41-
}
34+
// Since is the time to dump logs from.
35+
Since time.Time
36+
}
4237

43-
return nil, fmt.Errorf("service %s not found in local services", serviceName)
38+
// DumpResult contains the result of a dump operation.
39+
type DumpResult struct {
40+
ServiceName string
41+
Logs []byte
42+
LogsFile string
43+
InternalLogsDir string
4444
}
4545

4646
// Dump function exports stack data and dumps them as local artifacts, which can be used for debug purposes.
47-
func Dump(ctx context.Context, options DumpOptions) (string, error) {
47+
func Dump(ctx context.Context, options DumpOptions) ([]DumpResult, error) {
4848
logger.Debugf("Dump Elastic stack data")
4949

50-
err := dumpStackLogs(ctx, options)
50+
results, err := dumpStackLogs(ctx, options)
5151
if err != nil {
52-
return "", fmt.Errorf("can't dump Elastic stack logs: %w", err)
52+
return nil, fmt.Errorf("can't dump Elastic stack logs: %w", err)
5353
}
54-
return options.Output, nil
54+
return results, nil
5555
}
5656

57-
func dumpStackLogs(ctx context.Context, options DumpOptions) error {
57+
func dumpStackLogs(ctx context.Context, options DumpOptions) ([]DumpResult, error) {
5858
logger.Debugf("Dump stack logs (location: %s)", options.Output)
5959
err := os.RemoveAll(options.Output)
6060
if err != nil {
61-
return fmt.Errorf("can't remove output location: %w", err)
61+
return nil, fmt.Errorf("can't remove output location: %w", err)
6262
}
6363

64-
logsPath := filepath.Join(options.Output, "logs")
65-
err = os.MkdirAll(logsPath, 0755)
64+
services, err := localServiceNames(DockerComposeProjectName(options.Profile))
6665
if err != nil {
67-
return fmt.Errorf("can't create output location (path: %s): %w", logsPath, err)
66+
return nil, fmt.Errorf("failed to get local services: %w", err)
6867
}
6968

70-
services, err := localServiceNames(DockerComposeProjectName(options.Profile))
71-
if err != nil {
72-
return fmt.Errorf("failed to get local services: %w", err)
69+
for _, requestedService := range options.Services {
70+
if !slices.Contains(services, requestedService) {
71+
return nil, fmt.Errorf("local service %s does not exist", requestedService)
72+
}
7373
}
7474

75+
var results []DumpResult
7576
for _, serviceName := range services {
77+
if len(options.Services) > 0 && !slices.Contains(options.Services, serviceName) {
78+
continue
79+
}
80+
7681
logger.Debugf("Dump stack logs for %s", serviceName)
7782

78-
content, err := dockerComposeLogs(ctx, serviceName, options.Profile)
83+
content, err := dockerComposeLogsSince(ctx, serviceName, options.Profile, options.Since)
7984
if err != nil {
80-
logger.Errorf("can't fetch service logs (service: %s): %v", serviceName, err)
81-
} else {
82-
writeLogFiles(logsPath, serviceName, content)
85+
return nil, fmt.Errorf("can't fetch service logs (service: %s): %v", serviceName, err)
86+
}
87+
if options.Output == "" {
88+
results = append(results, DumpResult{
89+
ServiceName: serviceName,
90+
Logs: content,
91+
})
92+
continue
8393
}
8494

85-
err = copyDockerInternalLogs(serviceName, logsPath, options.Profile)
95+
result := DumpResult{
96+
ServiceName: serviceName,
97+
}
98+
99+
logsPath := filepath.Join(options.Output, "logs")
100+
err = os.MkdirAll(logsPath, 0755)
86101
if err != nil {
87-
logger.Errorf("can't copy internal logs (service: %s): %v", serviceName, err)
102+
return nil, fmt.Errorf("can't create output location (path: %s): %w", logsPath, err)
88103
}
104+
105+
logPath, err := writeLogFiles(logsPath, serviceName, content)
106+
if err != nil {
107+
return nil, fmt.Errorf("can't write log files for service %q: %w", serviceName, err)
108+
}
109+
result.LogsFile = logPath
110+
111+
switch serviceName {
112+
case elasticAgentService, fleetServerService:
113+
logPath, err := copyDockerInternalLogs(serviceName, logsPath, options.Profile)
114+
if err != nil {
115+
return nil, fmt.Errorf("can't copy internal logs (service: %s): %w", serviceName, err)
116+
}
117+
result.InternalLogsDir = logPath
118+
}
119+
120+
results = append(results, result)
89121
}
90-
return nil
122+
123+
return results, nil
91124
}
92125

93-
func writeLogFiles(logsPath, serviceName string, content []byte) {
94-
err := os.WriteFile(filepath.Join(logsPath, fmt.Sprintf("%s.log", serviceName)), content, 0644)
126+
func writeLogFiles(logsPath, serviceName string, content []byte) (string, error) {
127+
logPath := filepath.Join(logsPath, serviceName+".log")
128+
err := os.WriteFile(logPath, content, 0644)
95129
if err != nil {
96-
logger.Errorf("can't write service logs (service: %s): %v", serviceName, err)
130+
return "", fmt.Errorf("can't write service logs (service: %s): %v", serviceName, err)
97131
}
98-
}
99132

100-
// DumpLogsFile returns the file path to the logs of a given service
101-
func DumpLogsFile(options DumpOptions, serviceName string) string {
102-
return filepath.Join(options.Output, "logs", fmt.Sprintf("%s.log", serviceName))
133+
return logPath, nil
103134
}

internal/stack/errors.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,13 @@ func UndefinedEnvError(envName string) error {
2727

2828
// ErrUnavailableStack is an error about an unavailable Elastic stack.
2929
var ErrUnavailableStack = errors.New("the Elastic stack is unavailable, remember to start it with 'elastic-package stack up', or configure elastic-package with environment variables")
30+
31+
// ErrNotImplemented is an error about a feature not implemented in a stack provider.
32+
type ErrNotImplemented struct {
33+
Operation string
34+
Provider string
35+
}
36+
37+
func (err *ErrNotImplemented) Error() string {
38+
return fmt.Sprintf("operation not implemented in %q provider: %s", err.Provider, err.Operation)
39+
}

internal/stack/logs.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ import (
1616
"github.com/elastic/elastic-package/internal/profile"
1717
)
1818

19-
func dockerComposeLogs(ctx context.Context, serviceName string, profile *profile.Profile) ([]byte, error) {
20-
return dockerComposeLogsSince(ctx, serviceName, profile, time.Time{})
21-
}
22-
2319
func dockerComposeLogsSince(ctx context.Context, serviceName string, profile *profile.Profile, since time.Time) ([]byte, error) {
2420
appConfig, err := install.Configuration()
2521
if err != nil {
@@ -53,23 +49,17 @@ func dockerComposeLogsSince(ctx context.Context, serviceName string, profile *pr
5349
return out, nil
5450
}
5551

56-
func copyDockerInternalLogs(serviceName, outputPath string, profile *profile.Profile) error {
57-
switch serviceName {
58-
case elasticAgentService, fleetServerService:
59-
default:
60-
return nil // we need to pull internal logs only from Elastic-Agent and Fleets Server container
61-
}
62-
52+
func copyDockerInternalLogs(serviceName, outputPath string, profile *profile.Profile) (string, error) {
6353
p, err := compose.NewProject(DockerComposeProjectName(profile))
6454
if err != nil {
65-
return fmt.Errorf("could not create docker compose project: %w", err)
55+
return "", fmt.Errorf("could not create docker compose project: %w", err)
6656
}
6757

6858
outputPath = filepath.Join(outputPath, serviceName+"-internal")
6959
serviceContainer := p.ContainerName(serviceName)
7060
err = docker.Copy(serviceContainer, "/usr/share/elastic-agent/state/data/logs/", outputPath)
7161
if err != nil {
72-
return fmt.Errorf("docker copy failed: %w", err)
62+
return "", fmt.Errorf("docker copy failed: %w", err)
7363
}
74-
return nil
64+
return outputPath, nil
7565
}

internal/stack/providers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ type Provider interface {
4444
Update(context.Context, Options) error
4545

4646
// Dump dumps data for debug purpouses.
47-
Dump(context.Context, DumpOptions) (string, error)
47+
Dump(context.Context, DumpOptions) ([]DumpResult, error)
4848

4949
// Status obtains status information of the stack.
5050
Status(context.Context, Options) ([]ServiceStatus, error)
@@ -75,7 +75,7 @@ func (*composeProvider) Update(ctx context.Context, options Options) error {
7575
return Update(ctx, options)
7676
}
7777

78-
func (*composeProvider) Dump(ctx context.Context, options DumpOptions) (string, error) {
78+
func (*composeProvider) Dump(ctx context.Context, options DumpOptions) ([]DumpResult, error) {
7979
return Dump(ctx, options)
8080
}
8181

internal/stack/serverless.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,15 @@ func (sp *serverlessProvider) Update(ctx context.Context, options Options) error
410410
return fmt.Errorf("not implemented")
411411
}
412412

413-
func (sp *serverlessProvider) Dump(ctx context.Context, options DumpOptions) (string, error) {
413+
func (sp *serverlessProvider) Dump(ctx context.Context, options DumpOptions) ([]DumpResult, error) {
414+
for _, service := range options.Services {
415+
if service != "elastic-agent" {
416+
return nil, &ErrNotImplemented{
417+
Operation: fmt.Sprintf("logs dump for service %s", service),
418+
Provider: ProviderServerless,
419+
}
420+
}
421+
}
414422
return Dump(ctx, options)
415423
}
416424

internal/testrunner/runners/pipeline/runner.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ type runner struct {
4444
pipelines []ingest.Pipeline
4545

4646
runCompareResults bool
47+
48+
provider stack.Provider
4749
}
4850

4951
type IngestPipelineReroute struct {
@@ -78,6 +80,12 @@ func (r *runner) Run(ctx context.Context, options testrunner.TestOptions) ([]tes
7880
return nil, err
7981
}
8082

83+
provider, err := stack.BuildProvider(stackConfig.Provider, r.options.Profile)
84+
if err != nil {
85+
return nil, fmt.Errorf("failed to build stack provider: %w", err)
86+
}
87+
r.provider = provider
88+
8189
r.runCompareResults = true
8290
if stackConfig.Provider == stack.ProviderServerless {
8391
r.runCompareResults = true
@@ -204,16 +212,30 @@ func (r *runner) run(ctx context.Context) ([]testrunner.TestResult, error) {
204212
}
205213

206214
func (r *runner) checkElasticsearchLogs(ctx context.Context, startTesting time.Time) ([]testrunner.TestResult, error) {
207-
208215
startTime := time.Now()
209216

210217
testingTime := startTesting.Truncate(time.Second)
211218

212-
elasticsearchLogs, err := stack.GetServiceLogs(ctx, "elasticsearch", r.options.Profile, testingTime)
219+
dumpOptions := stack.DumpOptions{
220+
Profile: r.options.Profile,
221+
Services: []string{"elasticsearch"},
222+
Since: testingTime,
223+
}
224+
dump, err := r.provider.Dump(ctx, dumpOptions)
225+
var notImplementedErr *stack.ErrNotImplemented
226+
if errors.As(err, &notImplementedErr) {
227+
logger.Debugf("Not checking Elasticsearch logs: %s", err)
228+
return nil, nil
229+
}
213230
if err != nil {
214231
return nil, fmt.Errorf("error at getting the logs of elasticsearch: %w", err)
215232
}
216233

234+
if len(dump) != 1 || dump[0].ServiceName != "elasticsearch" {
235+
return nil, errors.New("expected elasticsearch logs in dump")
236+
}
237+
elasticsearchLogs := dump[0].Logs
238+
217239
seenWarnings := make(map[string]any)
218240
var processorRelatedWarnings []string
219241
err = stack.ParseLogsFromReader(bytes.NewReader(elasticsearchLogs), stack.ParseLogsOptions{

internal/testrunner/runners/system/runner.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -563,13 +563,26 @@ func (r *runner) run(ctx context.Context) (results []testrunner.TestResult, err
563563
}
564564
defer os.RemoveAll(tempDir)
565565

566-
dumpOptions := stack.DumpOptions{Output: tempDir, Profile: r.options.Profile}
567-
_, err = stack.Dump(context.WithoutCancel(ctx), dumpOptions)
566+
stackConfig, err := stack.LoadConfig(r.options.Profile)
567+
if err != nil {
568+
return nil, err
569+
}
570+
571+
provider, err := stack.BuildProvider(stackConfig.Provider, r.options.Profile)
572+
if err != nil {
573+
return nil, fmt.Errorf("failed to build stack provider: %w", err)
574+
}
575+
576+
dumpOptions := stack.DumpOptions{
577+
Output: tempDir,
578+
Profile: r.options.Profile,
579+
}
580+
dump, err := provider.Dump(context.WithoutCancel(ctx), dumpOptions)
568581
if err != nil {
569582
return nil, fmt.Errorf("dump failed: %w", err)
570583
}
571584

572-
logResults, err := r.checkAgentLogs(dumpOptions, startTesting, errorPatterns)
585+
logResults, err := r.checkAgentLogs(dump, startTesting, errorPatterns)
573586
if err != nil {
574587
return result.WithError(err)
575588
}
@@ -2016,11 +2029,17 @@ func (r *runner) checkNewAgentLogs(ctx context.Context, agent agentdeployer.Depl
20162029
return results, nil
20172030
}
20182031

2019-
func (r *runner) checkAgentLogs(dumpOptions stack.DumpOptions, startTesting time.Time, errorPatterns []logsByContainer) (results []testrunner.TestResult, err error) {
2032+
func (r *runner) checkAgentLogs(dump []stack.DumpResult, startTesting time.Time, errorPatterns []logsByContainer) (results []testrunner.TestResult, err error) {
20202033
for _, patternsContainer := range errorPatterns {
20212034
startTime := time.Now()
20222035

2023-
serviceLogsFile := stack.DumpLogsFile(dumpOptions, patternsContainer.containerName)
2036+
serviceDumpIndex := slices.IndexFunc(dump, func(d stack.DumpResult) bool {
2037+
return d.ServiceName == patternsContainer.containerName
2038+
})
2039+
if serviceDumpIndex < 0 {
2040+
return nil, fmt.Errorf("could not find logs dump for service %s", patternsContainer.containerName)
2041+
}
2042+
serviceLogsFile := dump[serviceDumpIndex].LogsFile
20242043

20252044
err = r.anyErrorMessages(serviceLogsFile, startTesting, patternsContainer.patterns)
20262045
if e, ok := err.(testrunner.ErrTestCaseFailed); ok {

internal/testrunner/runners/system/runner_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ func TestCheckAgentLogs(t *testing.T) {
357357
err = os.MkdirAll(filepath.Join(logsDirTemp, "logs"), 0755)
358358
require.NoError(t, err)
359359

360+
var dump []stack.DumpResult
360361
for service, logs := range tc.sampleLogs {
361362
logsFile := filepath.Join(logsDirTemp, "logs", fmt.Sprintf("%s.log", service))
362363
file, err := os.Create(logsFile)
@@ -365,6 +366,11 @@ func TestCheckAgentLogs(t *testing.T) {
365366
_, err = file.WriteString(strings.Join(logs, "\n"))
366367
require.NoError(t, err)
367368
file.Close()
369+
370+
dump = append(dump, stack.DumpResult{
371+
ServiceName: service,
372+
LogsFile: logsFile,
373+
})
368374
}
369375

370376
runner := runner{
@@ -375,13 +381,8 @@ func TestCheckAgentLogs(t *testing.T) {
375381
},
376382
},
377383
}
378-
379-
dumpOptions := stack.DumpOptions{
380-
Output: logsDirTemp,
381-
}
382-
results, err := runner.checkAgentLogs(dumpOptions, startTime, tc.errorPatterns)
384+
results, err := runner.checkAgentLogs(dump, startTime, tc.errorPatterns)
383385
require.NoError(t, err)
384-
385386
require.Len(t, results, tc.expectedErrors)
386387

387388
if tc.expectedErrors == 0 {

0 commit comments

Comments
 (0)