diff --git a/docs/docs/maintenance/metrics.md b/docs/docs/maintenance/metrics.md index ef764c13e148..1dffec46123b 100644 --- a/docs/docs/maintenance/metrics.md +++ b/docs/docs/maintenance/metrics.md @@ -37,7 +37,7 @@ Paimon has supported built-in metrics to measure operations of **commits**, **sc ## Metrics List -Below is lists of Paimon built-in metrics. They are summarized into types of scan metrics, commit metrics, write metrics, write buffer metrics and compaction metrics. +Below is lists of Paimon built-in metrics. They are summarized into types of scan metrics, commit metrics, write metrics, write buffer metrics, blob fetch metrics and compaction metrics. ### Scan Metrics @@ -226,6 +226,85 @@ Below is lists of Paimon built-in metrics. They are summarized into types of sca +### Blob Fetch Metrics + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Metrics NameTypeDescription
blobFetchTotalCounterTotal number of blob fetch outcomes handled during write, including successful fetches, failures, and missing resources written as NULL.
blobFetchSuccessCounterTotal number of blob fetches that completed successfully.
blobFetchSuccessBytesCounterTotal bytes written by successful blob fetches.
blobFetchNullWrittenCounterTotal number of NULL values written because a blob resource was missing or failed to fetch.
blobFetchMissingFileNullWrittenCounterTotal number of NULL values written because blob-write-null-on-missing-file handled a missing blob resource.
blobFetchFailureNullWrittenCounterTotal number of NULL values written because blob-write-null-on-fetch-failure handled a non-404 blob fetch failure.
blobFetchFailureCounterTotal number of blob fetch failures that were not written as NULL.
blobFetchHttpNotFoundCounterTotal number of HTTP 404 blob fetch failures.
blobFetchHttpClientErrorCounterTotal number of non-404 HTTP 4xx blob fetch failures.
blobFetchHttpServerErrorCounterTotal number of HTTP 5xx blob fetch failures.
blobFetchHttpOtherErrorCounterTotal number of blob fetch failures with HTTP status codes outside 4xx and 5xx.
blobFetchInvalidUriCounterTotal number of blob fetch failures caused by invalid URIs.
blobFetchOtherErrorCounterTotal number of blob fetch failures that are not classified as HTTP status or invalid URI failures.
+ ### Compaction Metrics @@ -375,6 +454,11 @@ From Flink Web-UI, go to the committer operator's metrics, it's shown as: + + + + + diff --git a/paimon-common/src/main/java/org/apache/paimon/data/BlobFetchMetricReporter.java b/paimon-common/src/main/java/org/apache/paimon/data/BlobFetchMetricReporter.java new file mode 100644 index 000000000000..0670c7098c71 --- /dev/null +++ b/paimon-common/src/main/java/org/apache/paimon/data/BlobFetchMetricReporter.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.apache.paimon.data; + +/** Reporter for blob fetch metrics. */ +public interface BlobFetchMetricReporter { + + BlobFetchMetricReporter NOOP = + new BlobFetchMetricReporter() { + @Override + public void recordSuccess(long bytes) {} + + @Override + public void recordMissingFileNullWritten(boolean httpNotFound) {} + + @Override + public void recordFetchFailureNullWritten(Throwable throwable) {} + + @Override + public void recordFetchFailure(Throwable throwable) {} + }; + + void recordSuccess(long bytes); + + void recordMissingFileNullWritten(boolean httpNotFound); + + void recordFetchFailureNullWritten(Throwable throwable); + + void recordFetchFailure(Throwable throwable); +} diff --git a/paimon-core/src/main/java/org/apache/paimon/append/DedicatedFormatRollingFileWriter.java b/paimon-core/src/main/java/org/apache/paimon/append/DedicatedFormatRollingFileWriter.java index e9ef1e15eedb..86df6b10b265 100644 --- a/paimon-core/src/main/java/org/apache/paimon/append/DedicatedFormatRollingFileWriter.java +++ b/paimon-core/src/main/java/org/apache/paimon/append/DedicatedFormatRollingFileWriter.java @@ -200,7 +200,8 @@ public DedicatedFormatRollingFileWriter( context.blobConsumer(), context.blobInlineFields(), context.writeNullOnMissingFile(), - context.writeNullOnFetchFailure()); + context.writeNullOnFetchFailure(), + context.blobFetchMetricReporter()); } else { this.blobWriterFactory = null; } diff --git a/paimon-core/src/main/java/org/apache/paimon/append/MultipleBlobFileWriter.java b/paimon-core/src/main/java/org/apache/paimon/append/MultipleBlobFileWriter.java index b8d2d1e6c549..d952018ee1f0 100644 --- a/paimon-core/src/main/java/org/apache/paimon/append/MultipleBlobFileWriter.java +++ b/paimon-core/src/main/java/org/apache/paimon/append/MultipleBlobFileWriter.java @@ -19,6 +19,7 @@ package org.apache.paimon.append; import org.apache.paimon.data.BlobConsumer; +import org.apache.paimon.data.BlobFetchMetricReporter; import org.apache.paimon.data.InternalRow; import org.apache.paimon.fileindex.FileIndexOptions; import org.apache.paimon.format.blob.BlobFileFormat; @@ -65,7 +66,8 @@ public MultipleBlobFileWriter( @Nullable BlobConsumer blobConsumer, Set blobInlineFields, boolean writeNullOnMissingFile, - boolean writeNullOnFetchFailure) { + boolean writeNullOnFetchFailure, + BlobFetchMetricReporter blobFetchMetricReporter) { RowType blobRowType = new RowType(fieldsInBlobFile(writeSchema, blobInlineFields)); this.blobWriters = new ArrayList<>(); for (String blobFieldName : blobRowType.getFieldNames()) { @@ -73,6 +75,7 @@ public MultipleBlobFileWriter( blobFileFormat.setWriteConsumer(blobConsumer); blobFileFormat.setWriteNullOnMissingFile(writeNullOnMissingFile); blobFileFormat.setWriteNullOnFetchFailure(writeNullOnFetchFailure); + blobFileFormat.setBlobFetchMetricReporter(blobFetchMetricReporter); blobWriters.add( new BlobProjectedFileWriter( () -> diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/BaseAppendFileStoreWrite.java b/paimon-core/src/main/java/org/apache/paimon/operation/BaseAppendFileStoreWrite.java index 574143192e19..24bdadc416b6 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/BaseAppendFileStoreWrite.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/BaseAppendFileStoreWrite.java @@ -35,6 +35,8 @@ import org.apache.paimon.io.DataFileMeta; import org.apache.paimon.io.RowDataRollingFileWriter; import org.apache.paimon.manifest.FileSource; +import org.apache.paimon.metrics.MetricRegistry; +import org.apache.paimon.operation.metrics.BlobFetchMetrics; import org.apache.paimon.reader.RecordReaderIterator; import org.apache.paimon.statistics.SimpleColStatsCollector; import org.apache.paimon.types.RowType; @@ -80,6 +82,7 @@ public abstract class BaseAppendFileStoreWrite extends MemoryFileStoreWrite writeCols; private boolean forceBufferSpill = false; @@ -117,6 +120,16 @@ public BaseAppendFileStoreWrite withBlobConsumer(BlobConsumer blobConsumer) { return this; } + @Override + public BaseAppendFileStoreWrite withMetricRegistry(MetricRegistry metricRegistry) { + super.withMetricRegistry(metricRegistry); + if (blobContext != null) { + blobFetchMetrics = new BlobFetchMetrics(metricRegistry, tableName); + blobContext = blobContext.withBlobFetchMetricReporter(blobFetchMetrics); + } + return this; + } + @Override protected RecordWriter createWriter( BinaryRow partition, @@ -179,6 +192,14 @@ private SimpleColStatsCollector.Factory[] statsCollectors() { return createStatsFactories(options.statsMode(), options, writeType.getFieldNames()); } + @Override + public void close() throws Exception { + super.close(); + if (blobFetchMetrics != null) { + blobFetchMetrics.close(); + } + } + protected abstract CompactManager getCompactManager( BinaryRow partition, int bucket, diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/BlobFileContext.java b/paimon-core/src/main/java/org/apache/paimon/operation/BlobFileContext.java index dd475da637bd..dc43824b6c0d 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/BlobFileContext.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/BlobFileContext.java @@ -20,6 +20,7 @@ import org.apache.paimon.CoreOptions; import org.apache.paimon.data.BlobConsumer; +import org.apache.paimon.data.BlobFetchMetricReporter; import org.apache.paimon.types.DataField; import org.apache.paimon.types.DataTypeRoot; import org.apache.paimon.types.RowType; @@ -39,6 +40,7 @@ public class BlobFileContext { private final boolean writeNullOnFetchFailure; private @Nullable BlobConsumer blobConsumer; + private BlobFetchMetricReporter blobFetchMetricReporter = BlobFetchMetricReporter.NOOP; private BlobFileContext( Set blobDescriptorFields, @@ -81,6 +83,12 @@ public BlobFileContext withBlobConsumer(BlobConsumer blobConsumer) { return this; } + public BlobFileContext withBlobFetchMetricReporter( + BlobFetchMetricReporter blobFetchMetricReporter) { + this.blobFetchMetricReporter = blobFetchMetricReporter; + return this; + } + public BlobFileContext withWriteType(RowType writeType) { if (writeType.getFieldTypes().stream().noneMatch(t -> t.is(BLOB))) { return null; @@ -108,4 +116,8 @@ public boolean writeNullOnMissingFile() { public boolean writeNullOnFetchFailure() { return writeNullOnFetchFailure; } + + public BlobFetchMetricReporter blobFetchMetricReporter() { + return blobFetchMetricReporter; + } } diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/metrics/BlobFetchMetrics.java b/paimon-core/src/main/java/org/apache/paimon/operation/metrics/BlobFetchMetrics.java new file mode 100644 index 000000000000..3f708a13884f --- /dev/null +++ b/paimon-core/src/main/java/org/apache/paimon/operation/metrics/BlobFetchMetrics.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.apache.paimon.operation.metrics; + +import org.apache.paimon.annotation.VisibleForTesting; +import org.apache.paimon.data.BlobFetchMetricReporter; +import org.apache.paimon.metrics.Counter; +import org.apache.paimon.metrics.MetricGroup; +import org.apache.paimon.metrics.MetricRegistry; +import org.apache.paimon.rest.HttpClientUtils; + +/** Metrics to measure blob fetches during write. */ +public class BlobFetchMetrics implements BlobFetchMetricReporter { + + public static final String GROUP_NAME = "blobFetch"; + + public static final String BLOB_FETCH_TOTAL = "blobFetchTotal"; + public static final String BLOB_FETCH_SUCCESS = "blobFetchSuccess"; + public static final String BLOB_FETCH_SUCCESS_BYTES = "blobFetchSuccessBytes"; + public static final String BLOB_FETCH_NULL_WRITTEN = "blobFetchNullWritten"; + public static final String BLOB_FETCH_MISSING_FILE_NULL_WRITTEN = + "blobFetchMissingFileNullWritten"; + public static final String BLOB_FETCH_FAILURE_NULL_WRITTEN = "blobFetchFailureNullWritten"; + public static final String BLOB_FETCH_FAILURE = "blobFetchFailure"; + public static final String BLOB_FETCH_HTTP_NOT_FOUND = "blobFetchHttpNotFound"; + public static final String BLOB_FETCH_HTTP_CLIENT_ERROR = "blobFetchHttpClientError"; + public static final String BLOB_FETCH_HTTP_SERVER_ERROR = "blobFetchHttpServerError"; + public static final String BLOB_FETCH_HTTP_OTHER_ERROR = "blobFetchHttpOtherError"; + public static final String BLOB_FETCH_INVALID_URI = "blobFetchInvalidUri"; + public static final String BLOB_FETCH_OTHER_ERROR = "blobFetchOtherError"; + + private final MetricGroup metricGroup; + private final Counter totalCounter; + private final Counter successCounter; + private final Counter successBytesCounter; + private final Counter nullWrittenCounter; + private final Counter missingFileNullWrittenCounter; + private final Counter fetchFailureNullWrittenCounter; + private final Counter failureCounter; + private final Counter httpNotFoundCounter; + private final Counter httpClientErrorCounter; + private final Counter httpServerErrorCounter; + private final Counter httpOtherErrorCounter; + private final Counter invalidUriCounter; + private final Counter otherErrorCounter; + + public BlobFetchMetrics(MetricRegistry registry, String tableName) { + this.metricGroup = registry.createTableMetricGroup(GROUP_NAME, tableName); + this.totalCounter = metricGroup.counter(BLOB_FETCH_TOTAL); + this.successCounter = metricGroup.counter(BLOB_FETCH_SUCCESS); + this.successBytesCounter = metricGroup.counter(BLOB_FETCH_SUCCESS_BYTES); + this.nullWrittenCounter = metricGroup.counter(BLOB_FETCH_NULL_WRITTEN); + this.missingFileNullWrittenCounter = + metricGroup.counter(BLOB_FETCH_MISSING_FILE_NULL_WRITTEN); + this.fetchFailureNullWrittenCounter = metricGroup.counter(BLOB_FETCH_FAILURE_NULL_WRITTEN); + this.failureCounter = metricGroup.counter(BLOB_FETCH_FAILURE); + this.httpNotFoundCounter = metricGroup.counter(BLOB_FETCH_HTTP_NOT_FOUND); + this.httpClientErrorCounter = metricGroup.counter(BLOB_FETCH_HTTP_CLIENT_ERROR); + this.httpServerErrorCounter = metricGroup.counter(BLOB_FETCH_HTTP_SERVER_ERROR); + this.httpOtherErrorCounter = metricGroup.counter(BLOB_FETCH_HTTP_OTHER_ERROR); + this.invalidUriCounter = metricGroup.counter(BLOB_FETCH_INVALID_URI); + this.otherErrorCounter = metricGroup.counter(BLOB_FETCH_OTHER_ERROR); + } + + @VisibleForTesting + public MetricGroup getMetricGroup() { + return metricGroup; + } + + @Override + public void recordSuccess(long bytes) { + totalCounter.inc(); + successCounter.inc(); + if (bytes > 0) { + successBytesCounter.inc(bytes); + } + } + + @Override + public void recordMissingFileNullWritten(boolean httpNotFound) { + totalCounter.inc(); + nullWrittenCounter.inc(); + missingFileNullWrittenCounter.inc(); + if (httpNotFound) { + httpNotFoundCounter.inc(); + } + } + + @Override + public void recordFetchFailureNullWritten(Throwable throwable) { + totalCounter.inc(); + nullWrittenCounter.inc(); + fetchFailureNullWrittenCounter.inc(); + recordFailureReason(throwable); + } + + @Override + public void recordFetchFailure(Throwable throwable) { + totalCounter.inc(); + failureCounter.inc(); + recordFailureReason(throwable); + } + + public void close() { + metricGroup.close(); + } + + private void recordFailureReason(Throwable throwable) { + Integer statusCode = HttpClientUtils.getHttpStatusCode(throwable); + if (statusCode != null) { + if (statusCode == 404) { + httpNotFoundCounter.inc(); + } else if (statusCode >= 400 && statusCode < 500) { + httpClientErrorCounter.inc(); + } else if (statusCode >= 500 && statusCode < 600) { + httpServerErrorCounter.inc(); + } else { + httpOtherErrorCounter.inc(); + } + return; + } + + if (HttpClientUtils.isInvalidUriException(throwable)) { + invalidUriCounter.inc(); + return; + } + + otherErrorCounter.inc(); + } +} diff --git a/paimon-core/src/test/java/org/apache/paimon/operation/metrics/BlobFetchMetricsTest.java b/paimon-core/src/test/java/org/apache/paimon/operation/metrics/BlobFetchMetricsTest.java new file mode 100644 index 000000000000..7fb53005fc49 --- /dev/null +++ b/paimon-core/src/test/java/org/apache/paimon/operation/metrics/BlobFetchMetricsTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.apache.paimon.operation.metrics; + +import org.apache.paimon.metrics.Counter; +import org.apache.paimon.metrics.Metric; +import org.apache.paimon.metrics.MetricGroup; +import org.apache.paimon.metrics.TestMetricRegistry; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** Tests for {@link BlobFetchMetrics}. */ +public class BlobFetchMetricsTest { + + private static final String TABLE_NAME = "myTable"; + + @Test + public void testMetricRegistration() { + BlobFetchMetrics metrics = new BlobFetchMetrics(new TestMetricRegistry(), TABLE_NAME); + MetricGroup metricGroup = metrics.getMetricGroup(); + + assertThat(metricGroup.getGroupName()).isEqualTo(BlobFetchMetrics.GROUP_NAME); + assertThat(metricGroup.getAllVariables()).containsEntry("table", TABLE_NAME); + assertThat(metricGroup.getMetrics().keySet()) + .containsExactlyInAnyOrder( + BlobFetchMetrics.BLOB_FETCH_TOTAL, + BlobFetchMetrics.BLOB_FETCH_SUCCESS, + BlobFetchMetrics.BLOB_FETCH_SUCCESS_BYTES, + BlobFetchMetrics.BLOB_FETCH_NULL_WRITTEN, + BlobFetchMetrics.BLOB_FETCH_MISSING_FILE_NULL_WRITTEN, + BlobFetchMetrics.BLOB_FETCH_FAILURE_NULL_WRITTEN, + BlobFetchMetrics.BLOB_FETCH_FAILURE, + BlobFetchMetrics.BLOB_FETCH_HTTP_NOT_FOUND, + BlobFetchMetrics.BLOB_FETCH_HTTP_CLIENT_ERROR, + BlobFetchMetrics.BLOB_FETCH_HTTP_SERVER_ERROR, + BlobFetchMetrics.BLOB_FETCH_HTTP_OTHER_ERROR, + BlobFetchMetrics.BLOB_FETCH_INVALID_URI, + BlobFetchMetrics.BLOB_FETCH_OTHER_ERROR); + } + + @Test + public void testMetricsAreUpdated() { + BlobFetchMetrics metrics = new BlobFetchMetrics(new TestMetricRegistry(), TABLE_NAME); + + metrics.recordSuccess(10); + metrics.recordMissingFileNullWritten(true); + metrics.recordMissingFileNullWritten(false); + metrics.recordFetchFailureNullWritten(new RuntimeException("HTTP error code: 500")); + metrics.recordFetchFailure(new RuntimeException("HTTP error code: 429")); + metrics.recordFetchFailure( + new IllegalArgumentException("Illegal character in path at index 1")); + metrics.recordFetchFailure(new RuntimeException("boom")); + + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_TOTAL, 7); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_SUCCESS, 1); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_SUCCESS_BYTES, 10); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_NULL_WRITTEN, 3); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_MISSING_FILE_NULL_WRITTEN, 2); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_FAILURE_NULL_WRITTEN, 1); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_FAILURE, 3); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_HTTP_NOT_FOUND, 1); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_HTTP_CLIENT_ERROR, 1); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_HTTP_SERVER_ERROR, 1); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_HTTP_OTHER_ERROR, 0); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_INVALID_URI, 1); + assertCounter(metrics, BlobFetchMetrics.BLOB_FETCH_OTHER_ERROR, 1); + } + + private static void assertCounter(BlobFetchMetrics metrics, String name, long expected) { + assertThat(counter(metrics, name)).isEqualTo(expected); + } + + private static long counter(BlobFetchMetrics metrics, String name) { + Map registeredMetrics = metrics.getMetricGroup().getMetrics(); + return ((Counter) registeredMetrics.get(name)).getCount(); + } +} diff --git a/paimon-format/src/main/java/org/apache/paimon/format/blob/BlobFileFormat.java b/paimon-format/src/main/java/org/apache/paimon/format/blob/BlobFileFormat.java index 66798c4bf389..afa8cdf232cf 100644 --- a/paimon-format/src/main/java/org/apache/paimon/format/blob/BlobFileFormat.java +++ b/paimon-format/src/main/java/org/apache/paimon/format/blob/BlobFileFormat.java @@ -19,6 +19,7 @@ package org.apache.paimon.format.blob; import org.apache.paimon.data.BlobConsumer; +import org.apache.paimon.data.BlobFetchMetricReporter; import org.apache.paimon.data.InternalRow; import org.apache.paimon.format.EmptyStatsExtractor; import org.apache.paimon.format.FileFormat; @@ -52,6 +53,7 @@ public class BlobFileFormat extends FileFormat { private final boolean blobAsDescriptor; private boolean writeNullOnMissingFile; private boolean writeNullOnFetchFailure; + private BlobFetchMetricReporter blobFetchMetricReporter = BlobFetchMetricReporter.NOOP; @Nullable public BlobConsumer writeConsumer; @@ -80,6 +82,10 @@ public void setWriteConsumer(@Nullable BlobConsumer writeConsumer) { this.writeConsumer = writeConsumer; } + public void setBlobFetchMetricReporter(BlobFetchMetricReporter blobFetchMetricReporter) { + this.blobFetchMetricReporter = blobFetchMetricReporter; + } + @Override public FormatReaderFactory createReaderFactory( RowType dataSchemaRowType, @@ -119,7 +125,12 @@ private BlobFormatWriterFactory(RowType type) { @Override public FormatWriter create(PositionOutputStream out, String compression) { return new BlobFormatWriter( - out, writeConsumer, type, writeNullOnMissingFile, writeNullOnFetchFailure); + out, + writeConsumer, + type, + writeNullOnMissingFile, + writeNullOnFetchFailure, + blobFetchMetricReporter); } } diff --git a/paimon-format/src/main/java/org/apache/paimon/format/blob/BlobFormatWriter.java b/paimon-format/src/main/java/org/apache/paimon/format/blob/BlobFormatWriter.java index 5b3e50537d64..23a0303ca734 100644 --- a/paimon-format/src/main/java/org/apache/paimon/format/blob/BlobFormatWriter.java +++ b/paimon-format/src/main/java/org/apache/paimon/format/blob/BlobFormatWriter.java @@ -21,6 +21,7 @@ import org.apache.paimon.data.Blob; import org.apache.paimon.data.BlobConsumer; import org.apache.paimon.data.BlobDescriptor; +import org.apache.paimon.data.BlobFetchMetricReporter; import org.apache.paimon.data.BlobPlaceholder; import org.apache.paimon.data.BlobRef; import org.apache.paimon.data.InternalRow; @@ -59,6 +60,7 @@ public class BlobFormatWriter implements FileAwareFormatWriter { private final PositionOutputStream out; @Nullable private final BlobConsumer writeConsumer; + private final BlobFetchMetricReporter blobFetchMetricReporter; private final String blobFieldName; private final boolean writeNullOnMissingFile; private final boolean writeNullOnFetchFailure; @@ -87,8 +89,25 @@ public BlobFormatWriter( RowType type, boolean writeNullOnMissingFile, boolean writeNullOnFetchFailure) { + this( + out, + writeConsumer, + type, + writeNullOnMissingFile, + writeNullOnFetchFailure, + BlobFetchMetricReporter.NOOP); + } + + public BlobFormatWriter( + PositionOutputStream out, + @Nullable BlobConsumer writeConsumer, + RowType type, + boolean writeNullOnMissingFile, + boolean writeNullOnFetchFailure, + BlobFetchMetricReporter blobFetchMetricReporter) { this.out = out; this.writeConsumer = writeConsumer; + this.blobFetchMetricReporter = blobFetchMetricReporter; this.writeNullOnMissingFile = writeNullOnMissingFile; this.writeNullOnFetchFailure = writeNullOnFetchFailure; checkArgument(type.getFieldCount() == 1, "BlobFormatWriter only support one field."); @@ -112,6 +131,7 @@ public boolean deleteFileUponAbort() { public void addElement(InternalRow element) throws IOException { checkArgument(element.getFieldCount() == 1, "BlobFormatWriter only support one field."); if (element.isNullAt(0)) { + recordPreCheckedMissingFileNull(element); writeNullElement(); return; } @@ -122,6 +142,7 @@ public void addElement(InternalRow element) throws IOException { if (tryWriteNullOnFetchFailure(e, null)) { return; } + blobFetchMetricReporter.recordFetchFailure(e); throw e; } if (blob == BlobPlaceholder.INSTANCE) { @@ -139,12 +160,14 @@ public void addElement(InternalRow element) throws IOException { blobUri(blob), blobFieldName, e); + blobFetchMetricReporter.recordMissingFileNullWritten(true); writeNullElement(); return; } if (tryWriteNullOnFetchFailure(e, blob)) { return; } + blobFetchMetricReporter.recordFetchFailure(e); throw e; } @@ -159,6 +182,9 @@ public void addElement(InternalRow element) throws IOException { blobLength += bytesRead; bytesRead = stream.read(tmpBuffer); } + } catch (IOException | RuntimeException e) { + blobFetchMetricReporter.recordFetchFailure(e); + throw e; } long binLength = blobLength + MAGIC_NUMBER_BYTES.length + 12; @@ -175,6 +201,7 @@ public void addElement(InternalRow element) throws IOException { out.flush(); } } + blobFetchMetricReporter.recordSuccess(blobLength); } private boolean tryWriteNullOnFetchFailure(Throwable e, @Nullable Blob blob) @@ -203,10 +230,34 @@ private boolean tryWriteNullOnFetchFailure(Throwable e, @Nullable Blob blob) blobFieldName, e); } + blobFetchMetricReporter.recordFetchFailureNullWritten(e); writeNullElement(); return true; } + private void recordPreCheckedMissingFileNull(InternalRow element) { + if (!writeNullOnMissingFile) { + return; + } + BlobDescriptor descriptor = tryGetBlobDescriptor(element); + if (descriptor != null) { + blobFetchMetricReporter.recordMissingFileNullWritten(isHttpUri(descriptor.uri())); + } + } + + @Nullable + private BlobDescriptor tryGetBlobDescriptor(InternalRow element) { + try { + byte[] bytes = element.getBinary(0); + if (bytes != null && BlobDescriptor.isBlobDescriptor(bytes)) { + return BlobDescriptor.deserialize(bytes); + } + } catch (RuntimeException ignored) { + // Null rows may come from normal user input, not from a pre-checked blob descriptor. + } + return null; + } + private void writeNullElement() throws IOException { lengths.add(NULL_LENGTH); if (writeConsumer != null) { @@ -221,6 +272,11 @@ private static String blobUri(@Nullable Blob blob) { return "unknown"; } + private static boolean isHttpUri(String uri) { + return uri.regionMatches(true, 0, "http://", 0, "http://".length()) + || uri.regionMatches(true, 0, "https://", 0, "https://".length()); + } + private void write(byte[] bytes) throws IOException { write(bytes, bytes.length); } diff --git a/paimon-format/src/test/java/org/apache/paimon/format/blob/BlobFormatWriterTest.java b/paimon-format/src/test/java/org/apache/paimon/format/blob/BlobFormatWriterTest.java index 90ded82a1952..e0a130c419c7 100644 --- a/paimon-format/src/test/java/org/apache/paimon/format/blob/BlobFormatWriterTest.java +++ b/paimon-format/src/test/java/org/apache/paimon/format/blob/BlobFormatWriterTest.java @@ -21,6 +21,7 @@ import org.apache.paimon.catalog.CatalogContext; import org.apache.paimon.data.Blob; import org.apache.paimon.data.BlobDescriptor; +import org.apache.paimon.data.BlobFetchMetricReporter; import org.apache.paimon.data.BlobRef; import org.apache.paimon.data.GenericRow; import org.apache.paimon.data.InternalRow; @@ -241,6 +242,92 @@ public void testHttpNotFoundWritesNullWhenMissingFileEnabled( } } + @Test + public void testBlobFetchMetricReporterForSuccessAndNullWritten( + @TempDir java.nio.file.Path tempDir) throws Exception { + RowType rowType = RowType.of(DataTypes.BLOB()); + TestingBlobFetchMetricReporter metricReporter = new TestingBlobFetchMetricReporter(); + BlobFormatWriter writer = + new BlobFormatWriter( + new LocalFileIO.LocalPositionOutputStream( + tempDir.resolve("blob.out").toFile()), + null, + rowType, + false, + true, + metricReporter); + + writer.addElement(GenericRow.of(Blob.fromData("image".getBytes()))); + writer.addElement( + GenericRow.of( + new BlobRef( + failingHttpReader(500), + new BlobDescriptor("https://example.com/error.jpg", 0, -1)))); + writer.close(); + + assertThat(metricReporter.success).isEqualTo(1); + assertThat(metricReporter.successBytes).isEqualTo(5); + assertThat(metricReporter.fetchFailureNullWritten).isEqualTo(1); + assertThat(metricReporter.failure).isEqualTo(0); + } + + @Test + public void testBlobFetchMetricReporterForUnhandledFailure(@TempDir java.nio.file.Path tempDir) + throws Exception { + RowType rowType = RowType.of(DataTypes.BLOB()); + TestingBlobFetchMetricReporter metricReporter = new TestingBlobFetchMetricReporter(); + BlobFormatWriter writer = + new BlobFormatWriter( + new LocalFileIO.LocalPositionOutputStream( + tempDir.resolve("blob.out").toFile()), + null, + rowType, + false, + false, + metricReporter); + + assertThatThrownBy( + () -> + writer.addElement( + GenericRow.of( + new BlobRef( + failingHttpReader(500), + new BlobDescriptor( + "https://example.com/error.jpg", + 0, + -1))))) + .isInstanceOf(RuntimeException.class) + .hasMessage("HTTP error code: 500"); + assertThat(metricReporter.failure).isEqualTo(1); + assertThat(metricReporter.fetchFailureNullWritten).isEqualTo(0); + } + + @Test + public void testBlobFetchMetricReporterForPreCheckedMissingFile( + @TempDir java.nio.file.Path tempDir) throws Exception { + RowType rowType = RowType.of(DataTypes.BLOB()); + UriReaderFactory uriReaderFactory = + new UriReaderFactory(CatalogContext.create(new Options())); + byte[] descriptorBytes = + new BlobDescriptor("https://example.com/missing.jpg", 0, -1).serialize(); + TestingBlobFetchMetricReporter metricReporter = new TestingBlobFetchMetricReporter(); + BlobFormatWriter writer = + new BlobFormatWriter( + new LocalFileIO.LocalPositionOutputStream( + tempDir.resolve("blob.out").toFile()), + null, + rowType, + true, + false, + metricReporter); + + writer.addElement(new DescriptorBytesRow(descriptorBytes, uriReaderFactory, true)); + writer.close(); + + assertThat(metricReporter.missingFileNullWritten).isEqualTo(1); + assertThat(metricReporter.httpNotFound).isEqualTo(1); + } + private static void assertBlobPayload(Blob blob, byte[] expected) throws Exception { try (SeekableInputStream blobIn = blob.newInputStream()) { byte[] actual = new byte[expected.length]; @@ -261,10 +348,17 @@ public SeekableInputStream newInputStream(String uri) { private static final class DescriptorBytesRow implements InternalRow { private final byte[] descriptorBytes; private final UriReaderFactory uriReaderFactory; + private final boolean nullAt; private DescriptorBytesRow(byte[] descriptorBytes, UriReaderFactory uriReaderFactory) { + this(descriptorBytes, uriReaderFactory, false); + } + + private DescriptorBytesRow( + byte[] descriptorBytes, UriReaderFactory uriReaderFactory, boolean nullAt) { this.descriptorBytes = descriptorBytes; this.uriReaderFactory = uriReaderFactory; + this.nullAt = nullAt; } @Override @@ -282,7 +376,7 @@ public void setRowKind(RowKind kind) {} @Override public boolean isNullAt(int pos) { - return false; + return nullAt; } @Override @@ -346,7 +440,7 @@ public org.apache.paimon.data.Timestamp getTimestamp(int pos, int precision) { @Override public byte[] getBinary(int pos) { - throw unsupported(); + return descriptorBytes; } @Override @@ -374,4 +468,38 @@ public InternalRow getRow(int pos, int numFields) { throw unsupported(); } } + + private static final class TestingBlobFetchMetricReporter implements BlobFetchMetricReporter { + + private int success; + private long successBytes; + private int missingFileNullWritten; + private int httpNotFound; + private int fetchFailureNullWritten; + private int failure; + + @Override + public void recordSuccess(long bytes) { + success++; + successBytes += bytes; + } + + @Override + public void recordMissingFileNullWritten(boolean httpNotFound) { + missingFileNullWritten++; + if (httpNotFound) { + this.httpNotFound++; + } + } + + @Override + public void recordFetchFailureNullWritten(Throwable throwable) { + fetchFailureNullWritten++; + } + + @Override + public void recordFetchFailure(Throwable throwable) { + failure++; + } + } }
<host>.taskmanager.<tm_id>.<job_name>.<writer_operator_name>.<subtask_index> paimon.table.<table_name>.writeBuffer
Blob Fetch Metrics<host>.taskmanager.<tm_id>.<job_name>.<writer_operator_name>.<subtask_index>paimon.table.<table_name>.blobFetch
Compaction Metrics <host>.taskmanager.<tm_id>.<job_name>.<writer_operator_name>.<subtask_index>