Skip to content

Add format auto-detection for zero-config S3 ingestion - #6990

Open
Davidding4718 wants to merge 1 commit into
opensearch-project:mainfrom
Davidding4718:format-detection
Open

Davidding4718 wants to merge 1 commit into
opensearch-project:mainfrom
Davidding4718:format-detection

Conversation

@Davidding4718

@Davidding4718 Davidding4718 commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Description

Add a format auto-detection library for Intelligent Ingestion that automatically identifies data format and compression from raw S3 object bytes, eliminating the need for customers to specify a codec in their pipeline configuration.

Key features:

  • Standalone format detection module (data-prepper-plugins/format-detection/) with minimal dependencies (snappy-java for decompression)
  • Detects compression: gzip, snappy, zstd (via magic bytes)
  • Detects binary formats: Parquet, Avro, ORC, PDF, images (via magic bytes)
  • Detects text formats: JSON arrays, ND-JSON, CSV, TSV, XML, plain text (via heuristics)
  • Handles edge cases: UTF-8 BOM, quoted CSV fields, binary garbage detection, truncated JSON arrays
  • Gzip and Snappy decompression for format detection on compressed content

S3 source integration:

  • When no codec is specified in pipeline config, the S3 source enters auto-detect mode
  • AutoDetectS3ObjectWorker reads first 64KB of each object, detects format, and selects the appropriate codec dynamically
  • AutoDetectCodecFactory maps detected format to codec plugin (ndjson, json, csv, parquet, avro, newline)
  • DetectionMetrics emits CloudWatch-compatible metrics via Micrometer with combined format+compression names and per-type duration timers

Performance: Average detection time is 1.1ms per file (tested with 5,000 files). Detection overhead is < 0.1% of total pipeline processing time.


Testing

Unit Tests (57 tests):

  • Compression detection: gzip, zstd, snappy magic bytes
  • Binary format detection: Parquet, Avro, ORC, PDF, images
  • Text format heuristics: JSON arrays, NDJSON, CSV, TSV, XML, plain text
  • Edge cases: empty files, binary garbage, UTF-8 BOM, quoted CSV fields, truncated JSON, large first lines
  • Gzip and Snappy decompression integration

End-to-End Pipeline Testing (S3 → auto-detect → OpenSearch):

Tested with real S3 bucket using both S3 scan and SQS event-driven modes, with results indexed into an OpenSearch domain.

File Type Detected Format Compression Confidence Result
.ndjson NDJSON NONE HIGH ✅ Fields parsed individually (timestamp, level, service, etc.)
.ndjson.gz NDJSON GZIP HIGH ✅ Decompressed + parsed
.ndjson.snappy NDJSON SNAPPY HIGH ✅ Decompressed + parsed
.json (array) JSON NONE HIGH ✅ Each array element indexed as document
.csv CSV NONE MEDIUM ✅ Header row used as field names
.csv.gz CSV GZIP MEDIUM ✅ Decompressed + parsed
.csv.snappy CSV SNAPPY MEDIUM ✅ Decompressed + parsed
.tsv TSV NONE MEDIUM ✅ Tab-delimited parsed
.psv (pipe) CSV NONE MEDIUM ✅ Pipe delimiter detected
.parquet PARQUET NONE HIGH ✅ Detected and parsed
.pdf PDF NONE HIGH ✅ Detected, skipped (unsupported codec)
.log (plain text) TEXT NONE LOW ✅ Each line as one event
Binary garbage UNKNOWN NONE LOW ✅ Skipped, not indexed
Empty file UNKNOWN NONE LOW ✅ Skipped

Load Test (5,000 files, ~500MB):

  • Detection: 0 failures, average 1.1ms per file, max 3.6ms
  • Total detection overhead: 5.6 seconds (< 0.1% of 13 min total pipeline time)
  • All 5,000 NDJSON files correctly detected and indexed

Mixed Format Test (3,600 files — NDJSON + CSV + JSON + compressed variants):

  • All formats correctly detected and routed to appropriate codecs
  • Per-type duration metrics confirm compression adds ~1-18ms overhead depending on algorithm
  • CloudWatch metrics published successfully under dataprepper namespace

CloudWatch Metrics & Observability

Format detection emits metrics via Micrometer (Prometheus + CloudWatch compatible):

Per format+compression type:

  • formatDetection.ndjson / formatDetection.ndjson.gzip / formatDetection.csv.snappy — count + duration per type
  • formatDetection.total — aggregate count
  • formatDetection.failed — unsupported/unknown files
  • formatDetection.confidence.high/medium/low — confidence breakdown
  • formatDetection.duration — overall detection timer

Prometheus endpoint sample (after 5,000 file load test):

s3_auto_detect_pipeline_s3_formatDetection_total 5000.0
s3_auto_detect_pipeline_s3_formatDetection_failed_total 0.0
s3_auto_detect_pipeline_s3_formatDetection_ndjson_total 5000.0
s3_auto_detect_pipeline_s3_formatDetection_duration_seconds_sum 5.631845329
s3_auto_detect_pipeline_s3_formatDetection_confidence_high_total 5000.0

Periodic log summary (every 60 seconds during activity, with final summary when ingestion stops):

╔══════════════════════════════════════════════════╗
║  Format Detection Summary                        ║
╠══════════════════════════════════════════════════╣
║  Total objects processed: 3600                   ║
║  Failed detections:       0                      ║
╠══════════════════════════════════════════════════╣
║  Type Distribution:                              ║
║    ndjson               1000  ( 27.8%)            ║
║    csv                  1000  ( 27.8%)            ║
║    json                 1000  ( 27.8%)            ║
║    ndjson.gzip           200  (  5.6%)            ║
║    csv.gzip              200  (  5.6%)            ║
║    ndjson.snappy         100  (  2.8%)            ║
║    csv.snappy            100  (  2.8%)            ║
╠══════════════════════════════════════════════════╣
║  Confidence:                                     ║
║    HIGH         2300  ( 63.9%)                  ║
║    MEDIUM       1300  ( 36.1%)                  ║
╚══════════════════════════════════════════════════╝

Issues Resolved

N/A

Check List

  • New functionality includes testing.
  • New functionality has a documentation issue. Please link to it in this PR.
    • New functionality has javadoc added
  • Commits are signed with a real name per the DCO

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

@github-actions

github-actions Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

✅ License Header Check Passed

All newly added files have proper license headers. Great work! 🎉

@dlvenable dlvenable 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 @Davidding4718 for this contribution!

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.

Let's not check in the compressed files. You can use the existing files like sample.ndjson and then compress them as part of the test setup.

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 generate the Parquet as part of the test setup as well? Binary files are hard to manage in a project.

throw context.weirdStringException(value, Map.class,
"Empty string is not allowed for plugin '" + pluginName + "'. Use null, empty (no value), or {} instead.");
// Treat empty string same as null (YAML bare keys like "stdout:" parse as "" in Jackson 2.13+)
isNull = true;

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.

It looks like you have some files from other PRs. Please be sure this PR only has the code for auto-detection. You may need to rebase from upstream/main.

Comment thread data-prepper-main/build.gradle Outdated
}


task runDataPrepper(type: JavaExec) {

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!


private static final int SAMPLE_SIZE = 65536;

public static void main(final String[] args) throws IOException {

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.

Overall, this should provide a new codec rather than a new application.

Also, we should not have another main() method in the plugins.

// Auto-detect mode: no codec specified, detect format per-object
LOG.info("No codec configured — using auto-detect mode");
final AutoDetectCodecFactory autoDetectCodecFactory = new AutoDetectCodecFactory(pluginFactory);
final S3ObjectRequest s3ObjectRequest = s3ObjectRequestBuilder

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.

Let's not create a split.

A better design would be to include a Function<S3InputFile, InputCodec>. Then change the builder to take that instead.

Your code would look more like this:

Function<S3InputFile, InputCodec> inputCodecProvider;
if (codecConfiguration != null) {
  inputCodecProvider = new AutoDetectCodecFactory(pluginFactory);
} else {
  final InputCodec codec = pluginFactory.loadPlugin(InputCodec.class, codecPluginSettings);
  inputCodecProvider = s3InputFile -> codec;
}
final S3ObjectRequest s3ObjectRequest = s3ObjectRequestBuilder
  .bucketOwnerProvider(bucketOwnerProvider)
  .codecProvider(inputCodecProvider)
  ...

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.

I took another look and see that S3InputFile has already loaded the object. Maybe we should make this a Function<S3ObjectReference, InputCodec>.

* S3ObjectHandler that auto-detects format and compression before processing.
* Used when no codec is explicitly configured in the pipeline.
*/
class AutoDetectS3ObjectWorker implements S3ObjectHandler {

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.

We should not fork this. The primary difference is that we need to sample the document.

Instead of create a new S3ObjectHandler using a Function<S3InputFile, InputCodec> should work. I gave some ideas on this below.

* @param sample raw bytes from the beginning of the data source
* @return detection result with compression, format, and confidence
*/
public FormatDetectionResult detect(final byte[] sample) {

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.

We should use Apache Tika to detect formats.

https://tika.apache.org/

final AcknowledgementSet acknowledgementSet,
final SourceCoordinator<S3SourceProgressState> sourceCoordinator,
final String partitionKey) throws IOException {
final S3InputFile inputFile = new S3InputFile(

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.

We may want to load the object using two requests. Otherwise you've pushed the stream forward. You could use a buffered input stream.

If you do two requests, you should specify a byte range. This may be better anyway to constrain the request length given.

@github-actions

github-actions Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 4d2a1bc.

⛔ Hard block: Issues at High severity or above will block this PR from merging.

PathLineSeverityDescription
data-prepper-plugins/format-detection/build.gradle15highNew dependency added: org.apache.tika:tika-core:2.9.2. Dependency authenticity cannot be verified from the diff alone; maintainers must confirm this artifact matches the expected provenance.
data-prepper-plugins/format-detection/build.gradle16highNew dependency added: org.xerial.snappy:snappy-java:1.1.10.8. Dependency authenticity cannot be verified from the diff alone; maintainers must confirm artifact provenance.
data-prepper-plugins/format-detection/build.gradle17highNew test dependency added: org.junit.jupiter:junit-jupiter:5.10.2. Must be verified against expected artifact hash/provenance.
data-prepper-plugins/format-detection/build.gradle18highNew test dependency added: org.assertj:assertj-core:3.25.3. Must be verified against expected artifact hash/provenance.
data-prepper-plugins/format-detection/build.gradle19highNew test runtime dependency added: org.junit.platform:junit-platform-launcher (no version pinned). Unpinned versions resolve at build time and are susceptible to dependency confusion or version-range hijacking.

The table above displays the top 10 most important findings.

Total: 5 | Critical: 0 | High: 5 | Medium: 0 | Low: 0


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

Add a format auto-detection library that identifies data format and
compression from S3 object bytes, so customers do not need to specify a
codec in their pipeline configuration.

- New format-detection module using Apache Tika for MIME/format detection,
  with lightweight text heuristics to distinguish JSON arrays, ND-JSON,
  CSV, and TSV. Detects gzip/snappy/zstd compression and decompresses
  gzip/snappy samples.
- S3 source integration via a Function<S3ObjectReference, InputCodec>
  codec provider. When no codec is configured, the source reads a
  byte-range sample (bytes=0-65535) per object, detects the format, and
  selects the matching codec. No forked object worker; the existing
  S3ObjectWorker resolves the codec through the provider.
- Codec is now optional in S3SourceConfig (auto-detect when absent).

Includes unit tests covering compression detection, binary/text formats,
edge cases (BOM, quoted CSV, truncated JSON, binary garbage), and
gzip integration. Compressed test inputs are generated at test time.

Signed-off-by: Siqi Ding <dingdd@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants