-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add the
take_over
mode for filestream inputs (#34292)
If a `filestream` input has the configuration parameter `take_over` set to `true`, every `loginput` state record (in the registry) with the `source` that matches at least one of the `filestream`'s paths/globs will be taken over by this `filestream` input. This means the existing `loginput` state entry gets converted into a `filestream` entry (the `loginput` entry gets deleted). The purpose of this mode is to make migration from `loginput` to `filestream` as simple and smooth as possible by adding `take_over: true` to the new `filestream` configuration. All offsets for input files will be preserved and the `filestream` will continue ingesting the files at the same point where the `loginput` stopped. This solves the previously occurring data duplication (file re-ingestion) problem.
- Loading branch information
Showing
17 changed files
with
1,255 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// Licensed to Elasticsearch B.V. under one or more contributor | ||
// license agreements. See the NOTICE file distributed with | ||
// this work for additional information regarding copyright | ||
// ownership. Elasticsearch B.V. licenses this file to you under | ||
// the Apache License, Version 2.0 (the "License"); you may | ||
// not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the License is distributed on an | ||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
// KIND, either express or implied. See the License for the | ||
// specific language governing permissions and limitations | ||
// under the License. | ||
|
||
package backup | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
) | ||
|
||
const ( | ||
backupSuffix = ".bak" | ||
) | ||
|
||
// Backuper defines backup-related operations | ||
type Backuper interface { | ||
// Backup performs the backup | ||
Backup() error | ||
// Removes all backups created by this backuper | ||
Remove() error | ||
} | ||
|
||
// fileExists checks if the given file exists | ||
func fileExists(name string) (bool, error) { | ||
_, err := os.Stat(name) | ||
if err == nil { | ||
return true, nil | ||
} | ||
if errors.Is(err, os.ErrNotExist) { | ||
return false, nil | ||
} | ||
return false, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Licensed to Elasticsearch B.V. under one or more contributor | ||
// license agreements. See the NOTICE file distributed with | ||
// this work for additional information regarding copyright | ||
// ownership. Elasticsearch B.V. licenses this file to you under | ||
// the Apache License, Version 2.0 (the "License"); you may | ||
// not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the License is distributed on an | ||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
// KIND, either express or implied. See the License for the | ||
// specific language governing permissions and limitations | ||
// under the License. | ||
|
||
package backup | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"os" | ||
"time" | ||
|
||
"github.com/elastic/elastic-agent-libs/logp" | ||
) | ||
|
||
// NewFileBackuper creates a new backuper that creates backups for the given files. | ||
func NewFileBackuper(log *logp.Logger, files []string) Backuper { | ||
return &fileBackuper{ | ||
log: log, | ||
files: files, | ||
} | ||
} | ||
|
||
type fileBackuper struct { | ||
log *logp.Logger | ||
files []string | ||
backups []string | ||
} | ||
|
||
// Backup creates temporary backups for given files and returns a callback that | ||
// removes every created backup file | ||
func (fb *fileBackuper) Backup() error { | ||
var ( | ||
buf = make([]byte, 64*1024) // 64KB | ||
) | ||
|
||
for _, file := range fb.files { | ||
err := func() error { | ||
src, err := os.Open(file) | ||
if err != nil { | ||
return err | ||
} | ||
defer src.Close() | ||
|
||
// we must put the timestamp as a prefix, so after the restart the new backups don't override the previous ones | ||
backupFilename := fmt.Sprintf("%s-%d%s", file, time.Now().UnixNano(), backupSuffix) | ||
dst, err := os.OpenFile(backupFilename, os.O_CREATE|os.O_EXCL|os.O_APPEND|os.O_WRONLY, 0600) | ||
if err != nil { | ||
return err | ||
} | ||
defer dst.Close() | ||
|
||
_, err = io.CopyBuffer(dst, src, buf) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
fb.backups = append(fb.backups, backupFilename) | ||
return nil | ||
}() | ||
if err != nil { | ||
return fmt.Errorf("failed to backup a file %s: %w", file, err) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Remove removes all backups created by this backuper | ||
func (fb fileBackuper) Remove() error { | ||
fb.log.Infof("Removing backup files: %v...", fb.backups) | ||
|
||
var errs []error | ||
for _, backup := range fb.backups { | ||
err := os.Remove(backup) | ||
if err != nil { | ||
errs = append(errs, err) | ||
} | ||
} | ||
|
||
if len(errs) != 0 { | ||
return fmt.Errorf("failed to remove some backups: %v", errs) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// Licensed to Elasticsearch B.V. under one or more contributor | ||
// license agreements. See the NOTICE file distributed with | ||
// this work for additional information regarding copyright | ||
// ownership. Elasticsearch B.V. licenses this file to you under | ||
// the Apache License, Version 2.0 (the "License"); you may | ||
// not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the License is distributed on an | ||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
// KIND, either express or implied. See the License for the | ||
// specific language governing permissions and limitations | ||
// under the License. | ||
|
||
package backup | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
"time" | ||
|
||
"github.com/elastic/elastic-agent-libs/logp" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestFileBackup(t *testing.T) { | ||
log := logp.NewLogger("backup-test") | ||
files := createFiles(t, 3) | ||
|
||
backuper := NewFileBackuper(log, files) | ||
|
||
t.Run("creates exactly one backup per given file", func(t *testing.T) { | ||
err := backuper.Backup() | ||
require.NoError(t, err) | ||
requireBackups(t, files, 1) | ||
}) | ||
|
||
t.Run("creates second round of backups", func(t *testing.T) { | ||
// there is a unix time with nanosecond precision in the filename | ||
// we can create only one backup per nanosecond | ||
// if there is already a file created in the same nanosecond, the backup fails | ||
time.Sleep(time.Microsecond) | ||
|
||
err := backuper.Backup() | ||
require.NoError(t, err) | ||
|
||
requireBackups(t, files, 2) | ||
}) | ||
|
||
t.Run("removes all created backups", func(t *testing.T) { | ||
err := backuper.Remove() | ||
require.NoError(t, err) | ||
|
||
requireBackups(t, files, 0) | ||
}) | ||
} | ||
|
||
func createFiles(t *testing.T, count int) (created []string) { | ||
t.Helper() | ||
|
||
tmp := t.TempDir() | ||
|
||
for i := 0; i < count; i++ { | ||
file, err := os.CreateTemp(tmp, "file-*") | ||
require.NoError(t, err) | ||
_, err = file.WriteString(file.Name()) | ||
require.NoError(t, err) | ||
file.Close() | ||
created = append(created, file.Name()) | ||
} | ||
|
||
return created | ||
} | ||
|
||
func requireBackups(t *testing.T, files []string, expectedCount int) { | ||
for _, file := range files { | ||
matches, err := filepath.Glob(file + "-*" + backupSuffix) | ||
require.NoError(t, err) | ||
require.Len(t, matches, expectedCount, "expected a different amount of created backups") | ||
for _, match := range matches { | ||
content, err := os.ReadFile(match) | ||
require.NoError(t, err) | ||
require.Equal(t, file, string(content)) | ||
} | ||
} | ||
} |
Oops, something went wrong.