Skip to content

feat(outputs.parquet)!: Use unique filenames to avoid filename collisions - #19558

Open
81reap wants to merge 4 commits into
influxdata:masterfrom
81reap:parquet-07-no-overwrite
Open

81reap wants to merge 4 commits into
influxdata:masterfrom
81reap:parquet-07-no-overwrite

Conversation

@81reap

@81reap 81reap commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

The current code creates files with a <measurement>-<YYYY-MM-DD>-<unix-seconds>.parquet pattern using the timestamp when Write was called. If the file already exists the existing file is renamed using a new, current timestamp and the same pattern.

At this, the code assumes only one writer per directory which should minimize the filename collisions to restarts of Telegraf. However, as #19563 describes, having multiple writers, i.e. multiple plugin instances and/or multiple telegraf instances, writing to the same directory will increase the risk of filename collisions and creates the risk of multiple renames target the same filename because they might happen at the same time. This leads to data loss as one writer will override the data of other writers on collision.

This PR changes the logic when creating a new writer to create files of the pattern <measurement>-<YYYYMMDDhhmmss>-<uuid v6>.parquet where an additional (time-based) UUID is added to the filename to ensure unique filenames. This also prevents the risk of filename collisions for multiple, independent writers.

Checklist

Related issues

resolves #19563

@telegraf-tiger telegraf-tiger Bot added fix pr to fix corresponding bug plugin/output 1. Request for new output plugins 2. Issues/PRs that are related to out plugins labels Aug 26, 2026
@81reap
81reap force-pushed the parquet-07-no-overwrite branch from 983bda9 to 2c4b79d Compare September 6, 2026 05:14
@81reap
81reap marked this pull request as ready for review September 6, 2026 05:49

@srebhan srebhan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please keep the schema of renaming the existing file? If you detect a collision )(e.g. myname-2026-09-07-1788773604.parquet), use filepath.Glob to find all renaming target files (e.g. myname-2026-09-07-1788773604_*.parquet), then detect the last entry (e.g. myname-2026-09-07-1788773604_004.parquet), parse the number (e.g. 004) and increase it. This is your new target. It's still racy though as another process could create that file between you creating the list and actually renaming the file but I guess that's only avoidable if you use a uuid or something as suffix...

@srebhan srebhan self-assigned this Sep 7, 2026
@81reap

81reap commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

well I was hoping we wouldn't need to add all that logic if we could just order the schema columns in a deterministic order #19556

@srebhan

srebhan commented Sep 8, 2026

Copy link
Copy Markdown
Member

Sorry, I do not understand what this has to do with column ordering. From what I understand Telegraf currently creates a new file if the file already exists and moves away the old one. You brought up a valid corner case that the renaming can fail as the logic to determine the target might lead to the same filename as the current (or another) file.

I thought this PR is fixing the name-collision issue, isn't it?

@81reap

81reap commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Can we please keep the schema of renaming the existing file?

instead of checking the schema of the existing file, the schema will match if we order the columns deterministically and if the schema columns haven't changed. which is why I said #19556 may fix the issue

From what I understand Telegraf currently creates a new file if the file already exists and moves away the old one.

so that's not what I saw during testing. this is running on rootless podman.

$ docker pull telegraf:1.39.3-alpine
[...]

$ cat telegraf.conf
[agent]
  interval = "100ms"
  flush_interval = "100ms"
  flush_jitter = "0s"
  collection_jitter = "0s"
  round_interval = false
  omit_hostname = true

[[inputs.mock]]
  metric_name = "cpu"
  [[inputs.mock.step]]
    name = "value"
    start = 0.0
    step = 1.0

[[outputs.parquet]]
  directory = "/out"

run one container

$ rm -rf ./out/*

$ podman run --rm --entrypoint telegraf \
  -v ./telegraf.conf:/etc/telegraf/telegraf.conf:ro \
  -v ./out:/out \
  docker.io/library/telegraf:1.39.3-alpine \
  --config /etc/telegraf/telegraf.conf --once
[...]

$ ls out/
cpu-2026-09-08-1788883487.parquet

10 runs

$ rm -rf ./out/*

$ for i in $(seq 1 10); do
  podman run --rm --entrypoint telegraf \
    -v ./telegraf.conf:/etc/telegraf/telegraf.conf:ro \
    -v ./out:/out \
    docker.io/library/telegraf:1.39.3-alpine \
    --config /etc/telegraf/telegraf.conf --once >/dev/null 2>&1
done
[...]

$ ls out/
cpu-2026-09-08-1788883594.parquet  cpu-2026-09-08-1788883595.parquet

10 runs this branch

$ rm -rf ./out/*

$ make build
CGO_ENABLED=0 go build -tags "" -ldflags " -X github.com/influxdata/telegraf/internal.Commit=1b9fbaee -X github.com/influxdata/telegraf/internal.Branch=parquet-07-no-overwrite -X github.com/influxdata/telegraf/internal.Version=1.41.0-1b9fbaee" ./cmd/telegraf

$ ./telegraf --version
Telegraf 1.41.0-1b9fbaee (git: parquet-07-no-overwrite@1b9fbaee)

$ cat telegraf.conf 
[agent]
  interval = "100ms"
  flush_interval = "100ms"
  flush_jitter = "0s"
  collection_jitter = "0s"
  round_interval = false
  omit_hostname = true

[[inputs.mock]]
  metric_name = "cpu"
  [[inputs.mock.step]]
    name = "value"
    start = 0.0
    step = 1.0

[[outputs.parquet]]
  directory = "./out"

$ for i in $(seq 1 10); do
  ./telegraf --config telegraf.conf --once >/dev/null 2>&1
done

$ ls ./out
cpu-2026-09-08-1788886996-1.parquet  cpu-2026-09-08-1788886996-4.parquet  cpu-2026-09-08-1788886996-7.parquet  cpu-2026-09-08-1788886996.parquet
cpu-2026-09-08-1788886996-2.parquet  cpu-2026-09-08-1788886996-5.parquet  cpu-2026-09-08-1788886996-8.parquet
cpu-2026-09-08-1788886996-3.parquet  cpu-2026-09-08-1788886996-6.parquet  cpu-2026-09-08-1788886996-9.parquet

@srebhan

srebhan commented Sep 15, 2026

Copy link
Copy Markdown
Member

@81reap I now see what you are talking about. What you want to do is to write to the same directory with multiple Telegraf agents. Is this correct?

@81reap

81reap commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

yep! that's why #19563 I try to have multiple writes writing to the same file and schema in the same timestamp. it would be like multiple machines (CPU being collected on each machine but replicated to one machine) or agents writing to the same thing (multiple NVIDA gpus on one machine).

that's why i've been trying to fix all these things in the stack where data isn't lost when it doesn't match the schema or isn't lost when multiple things are writing to it.

@srebhan srebhan changed the title fix(outputs.parquet): Never overwrite an existing parquet file fix(outputs.parquet): Allow multiple writers to the same directory Sep 16, 2026
@srebhan

srebhan commented Sep 16, 2026

Copy link
Copy Markdown
Member

@81reap I changed the PR title and description to reflect what you are trying to do. I suggest to change the logic for the filename to create files using a <measurement>-<YYYYMMDDhhmmss>-<uuid v6>.parquet pattern as the UUID minimizes the risk of ending up with the same filename in multiple readers. This also makes the renaming completely obsolete... What do you think?

For creating the UUID you can use e.g. this function and get the String from it...

@srebhan srebhan added the waiting for response waiting for response from contributor label Sep 17, 2026
@81reap
81reap force-pushed the parquet-07-no-overwrite branch from 1b9fbae to ca153a6 Compare September 17, 2026 14:21
@81reap

81reap commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

I didn't go with UUID as that's what the current README calls out as the expected behaviour. I suppose it does match Hive partitioning standards so end users may be more familiar with it. I've update the PR, although this is now a breaking change for end users depending on how they consume the output parquets.

But just to clarify multiple agents is only one way to trigger it and currently hasn't been supported by TeleGraf and is a feat and not a fix AFAICT.

The bigger issue is what the original title called out :: when TeleGraf rotates files, it looses data. I suppose using a for loop made that more confusing than clear. This time I use timeout -s INT so TeleGraf shuts down cleanly and flushes its last row group.

Hopefully this does a better job of showing how we're breaking this gurantee.

## File Rotation
If a file with the same target name exists at start, the existing file is
rotated to avoid over-writing it or conflicting schema.
File rotation is available via a time based interval that a user can optionally
set. Due to the usage of a buffered writer, a size based rotation is not
possible as the file may not actually get data at each interval.

one container

$ cat telegraf.conf
[agent]
  interval = "100ms"
  flush_interval = "100ms"
  flush_jitter = "0s"
  collection_jitter = "0s"
  round_interval = false
  omit_hostname = true

[[inputs.mock]]
  metric_name = "cpu"
  [[inputs.mock.step]]
    name = "value"
    start = 0.0
    step = 1.0

[[outputs.parquet]]
  directory = "/out"

$ rm -rf ./out/*

$ timeout -s INT 10 podman run --rm --entrypoint telegraf \
  -v ./telegraf.conf:/etc/telegraf/telegraf.conf:ro \
  -v ./out:/out \
  docker.io/library/telegraf:1.39.3-alpine \
  --config /etc/telegraf/telegraf.conf
[...]

$ ls out/
cpu-2026-09-17-1789651240.parquet

$ duckdb -c "select count(*) as rows, min(value) as first, max(value) as last from read_parquet('out/*.parquet')"
┌───────┬────────┬────────┐
│ rows  │ first  │  last  │
│ int64 │ double │ double │
├───────┼────────┼────────┤
│    99 │    0.0 │   98.0 │
└───────┴────────┴────────┘

99 rows, last value 98. nothing lost.

one container, rotation_interval = "100ms"

$ tail -3 telegraf.conf
[[outputs.parquet]]
  directory = "/out"
  rotation_interval = "100ms" # this is the new line

$ rm -rf ./out/*

$ timeout -s INT 10 podman run --rm --entrypoint telegraf \
  -v ./telegraf.conf:/etc/telegraf/telegraf.conf:ro \
  -v ./out:/out \
  docker.io/library/telegraf:1.39.3-alpine \
  --config /etc/telegraf/telegraf.conf
[...]

$ ls out/
cpu-2026-09-17-1789651255.parquet  cpu-2026-09-17-1789651260.parquet
cpu-2026-09-17-1789651256.parquet  cpu-2026-09-17-1789651261.parquet
cpu-2026-09-17-1789651257.parquet  cpu-2026-09-17-1789651262.parquet
cpu-2026-09-17-1789651258.parquet  cpu-2026-09-17-1789651263.parquet
cpu-2026-09-17-1789651259.parquet  cpu-2026-09-17-1789651264.parquet

$ duckdb -c "select count(*) as rows, min(value) as first, max(value) as last from read_parquet('out/*.parquet')"
┌───────┬────────┬────────┐
│ rows  │ first  │  last  │
│ int64 │ double │ double │
├───────┼────────┼────────┤
│    10 │   19.0 │   98.0 │
└───────┴────────┴────────┘

same 10 seconds, same one process. 10 rows out of 99.

@telegraf-tiger telegraf-tiger Bot removed the waiting for response waiting for response from contributor label Sep 17, 2026
@81reap 81reap changed the title fix(outputs.parquet): Allow multiple writers to the same directory fix(outputs.parquet): Never overwrite an existing parquet file Sep 17, 2026
@srebhan srebhan changed the title fix(outputs.parquet): Never overwrite an existing parquet file feat(outputs.parquet)!: Use unique filenames to avoid filename collisions Sep 17, 2026
@telegraf-tiger telegraf-tiger Bot added the feat Improvement on an existing feature such as adding a new setting/mode to an existing plugin label Sep 17, 2026
@srebhan srebhan added breaking change Improvement to Telegraf that requires changes to the plugin or agent; for minor/major releases and removed fix pr to fix corresponding bug labels Sep 17, 2026

@srebhan srebhan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @81reap! The code looks good. Please add am "Important Change" section to the CHANGELOG.md file mentioning this PR and the user-visible effect.

P.S.: I updated the title once more, hope this catches it.

@81reap
81reap force-pushed the parquet-07-no-overwrite branch from 79c3820 to 8ccd151 Compare September 17, 2026 23:44
@81reap

81reap commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

added another --fixup to include the change in CHANGELOG.md, will also include future PRs.

also aligned on the new PR title, I just wanted to make sure that I wasn't only communicating multiple writers but the a bigger dataloss issue

also since this uses --fixup, please rebase + merge. thanks in advance! :)

@srebhan srebhan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. Thanks @81reap! Just one small comment...

Comment thread plugins/outputs/parquet/parquet.go
Telegraf currently tries to move aside an existing file of the same name by adding the current time. This causes a conflict when both the moved file and the new file are named into the same second causing a no-op and `os.Create` would overwrite and lose data. Bad inputs that cause a restart loop would trigger this issue per cycle.

Files are now opened with `O_EXCL` and a numeric suffix is added until an unused name is found.
@81reap
81reap force-pushed the parquet-07-no-overwrite branch from 8ccd151 to 4070681 Compare September 19, 2026 22:07
@telegraf-tiger

Copy link
Copy Markdown
Contributor

Download PR build artifacts for linux_amd64.tar.gz, darwin_arm64.tar.gz, and windows_amd64.zip.
Downloads for additional architectures and packages are available below.

⚠️ This pull request increases the Telegraf binary size by 6.33 % for linux amd64 (new size: 327.7 MB, nightly size 308.2 MB)

📦 Click here to get additional PR build artifacts

Artifact URLs

. DEB . RPM . TAR . GZ . ZIP
amd64.deb aarch64.rpm darwin_amd64.tar.gz windows_amd64.zip
arm64.deb armel.rpm darwin_arm64.tar.gz windows_arm64.zip
armel.deb armv6hl.rpm freebsd_amd64.tar.gz windows_i386.zip
armhf.deb i386.rpm freebsd_armv7.tar.gz
i386.deb ppc64le.rpm freebsd_i386.tar.gz
mips.deb riscv64.rpm linux_amd64.tar.gz
mipsel.deb s390x.rpm linux_arm64.tar.gz
ppc64el.deb x86_64.rpm linux_armel.tar.gz
riscv64.deb linux_armhf.tar.gz
s390x.deb linux_i386.tar.gz
linux_mips.tar.gz
linux_mipsel.tar.gz
linux_ppc64le.tar.gz
linux_riscv64.tar.gz
linux_s390x.tar.gz

@81reap

81reap commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

ci failure is unrelated to my changes

2026/09/19 22:29:33 No image auth found for https://index.docker.io/v1/. Setting empty credentials for the image: vault:1.13.3. This is expected for public images. Details: credentials not found in native keychain
2026/09/19 22:29:34 🐳 Creating container for image vault:1.13.3
--- FAIL: TestIntegration (0.25s)
    vault_test.go:291: 
                Error Trace:    /home/circleci/project/plugins/inputs/vault/vault_test.go:291
                Error:          Received unexpected error:
                                container failed to start: create container: container create: Error response from daemon: No such image: vault:1.13.3
                Test:           TestIntegration
                Messages:       failed to start container

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Improvement to Telegraf that requires changes to the plugin or agent; for minor/major releases feat Improvement on an existing feature such as adding a new setting/mode to an existing plugin plugin/output 1. Request for new output plugins 2. Issues/PRs that are related to out plugins

Projects

None yet

Development

Successfully merging this pull request may close these issues.

outputs.parquet: fields that first appear after the initial flush are silently dropped, with no warning

2 participants