Skip to content

Commit 058c4e4

Browse files
committed
Enforce static schemas at preprocessor level
1 parent 8fb5918 commit 058c4e4

7 files changed

Lines changed: 467 additions & 13 deletions

File tree

astra/src/main/java/com/slack/astra/bulkIngestApi/BulkIngestApi.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.linecorp.armeria.common.HttpStatus;
77
import com.linecorp.armeria.server.annotation.Post;
88
import com.slack.astra.bulkIngestApi.opensearch.BulkApiRequestParser;
9+
import com.slack.astra.proto.config.AstraConfigs;
910
import com.slack.astra.proto.schema.Schema;
1011
import com.slack.service.murron.trace.Trace;
1112
import io.micrometer.core.instrument.Counter;
@@ -25,8 +26,13 @@
2526
*/
2627
public class BulkIngestApi {
2728
private static final Logger LOG = LoggerFactory.getLogger(BulkIngestApi.class);
29+
30+
public static final String SCHEMA_ENFORCEMENT_FLAG = "astra.schema.enforcement.enabled";
31+
2832
private final BulkIngestKafkaProducer bulkIngestKafkaProducer;
2933
private final DatasetRateLimitingService datasetRateLimitingService;
34+
private final Schema.IngestSchema schema;
35+
private final AstraConfigs.SchemaMode schemaMode;
3036
private final MeterRegistry meterRegistry;
3137
private final Counter incomingByteTotal;
3238
private final Counter incomingDocsTotal;
@@ -36,7 +42,6 @@ public class BulkIngestApi {
3642
private final String BULK_INGEST_ERROR = "astra_preprocessor_error";
3743
private final String BULK_INGEST_TIMER = "astra_preprocessor_bulk_ingest";
3844
private final int rateLimitExceededErrorCode;
39-
private final Schema.IngestSchema schema;
4045

4146
private final Counter bulkIngestErrorCounter;
4247

@@ -45,10 +50,13 @@ public BulkIngestApi(
4550
DatasetRateLimitingService datasetRateLimitingService,
4651
MeterRegistry meterRegistry,
4752
int rateLimitExceededErrorCode,
48-
Schema.IngestSchema schema) {
53+
Schema.IngestSchema schema,
54+
AstraConfigs.SchemaMode schemaMode) {
4955

5056
this.bulkIngestKafkaProducer = bulkIngestKafkaProducer;
5157
this.datasetRateLimitingService = datasetRateLimitingService;
58+
this.schema = schema;
59+
this.schemaMode = schemaMode;
5260
this.meterRegistry = meterRegistry;
5361
this.incomingByteTotal = meterRegistry.counter(BULK_INGEST_INCOMING_BYTE_TOTAL);
5462
this.incomingDocsTotal = meterRegistry.counter(BULK_INGEST_INCOMING_BYTE_DOCS);
@@ -58,7 +66,6 @@ public BulkIngestApi(
5866
} else {
5967
this.rateLimitExceededErrorCode = rateLimitExceededErrorCode;
6068
}
61-
this.schema = schema;
6269
this.bulkIngestErrorCounter = meterRegistry.counter(BULK_INGEST_ERROR);
6370
}
6471

@@ -75,7 +82,11 @@ public HttpResponse addDocument(String bulkRequest) {
7582
incomingByteTotal.increment(bulkRequestBytes.length);
7683
Map<String, List<Trace.Span>> docs = Map.of();
7784
try {
78-
docs = BulkApiRequestParser.parseRequest(bulkRequestBytes, schema);
85+
if (Boolean.getBoolean(SCHEMA_ENFORCEMENT_FLAG)) {
86+
docs = BulkApiRequestParser.parseRequest(bulkRequestBytes, schema, schemaMode);
87+
} else {
88+
docs = BulkApiRequestParser.parseRequest(bulkRequestBytes, schema);
89+
}
7990
} catch (Exception e) {
8091
LOG.error("Request failed ", e);
8192
bulkIngestErrorCounter.increment();

astra/src/main/java/com/slack/astra/bulkIngestApi/opensearch/BulkApiRequestParser.java

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import com.google.common.annotations.VisibleForTesting;
44
import com.google.protobuf.ByteString;
5+
import com.slack.astra.bulkIngestApi.BulkIngestApi;
56
import com.slack.astra.logstore.LogMessage;
67
import com.slack.astra.logstore.schema.ReservedFields;
8+
import com.slack.astra.proto.config.AstraConfigs;
79
import com.slack.astra.proto.schema.Schema;
810
import com.slack.astra.writer.SpanFormatter;
911
import com.slack.service.murron.trace.Trace;
@@ -37,7 +39,13 @@ public class BulkApiRequestParser {
3739

3840
public static Map<String, List<Trace.Span>> parseRequest(
3941
byte[] postBody, Schema.IngestSchema schema) throws IOException {
40-
return convertIndexRequestToTraceFormat(parseBulkRequest(postBody), schema);
42+
return parseRequest(postBody, schema, AstraConfigs.SchemaMode.SCHEMA_MODE_DYNAMIC);
43+
}
44+
45+
public static Map<String, List<Trace.Span>> parseRequest(
46+
byte[] postBody, Schema.IngestSchema schema, AstraConfigs.SchemaMode schemaMode)
47+
throws IOException {
48+
return convertIndexRequestToTraceFormat(parseBulkRequest(postBody), schema, schemaMode);
4149
}
4250

4351
/**
@@ -64,6 +72,14 @@ public static long getTimestampFromIngestDocument(Map<String, Object> sourceAndM
6472
@VisibleForTesting
6573
public static Trace.Span fromIngestDocument(
6674
IngestDocument ingestDocument, Schema.IngestSchema schema) {
75+
return fromIngestDocument(ingestDocument, schema, AstraConfigs.SchemaMode.SCHEMA_MODE_DYNAMIC);
76+
}
77+
78+
@VisibleForTesting
79+
public static Trace.Span fromIngestDocument(
80+
IngestDocument ingestDocument,
81+
Schema.IngestSchema schema,
82+
AstraConfigs.SchemaMode schemaMode) {
6783

6884
Map<String, Object> sourceAndMetadata = ingestDocument.getSourceAndMetadata();
6985

@@ -140,8 +156,21 @@ public static Trace.Span fromIngestDocument(
140156
sourceAndMetadata.remove(IngestDocument.Metadata.ID.getFieldName());
141157
sourceAndMetadata.remove(IngestDocument.Metadata.INDEX.getFieldName());
142158

159+
boolean dropUnknown =
160+
Boolean.getBoolean(BulkIngestApi.SCHEMA_ENFORCEMENT_FLAG)
161+
&& schemaMode == AstraConfigs.SchemaMode.SCHEMA_MODE_DROP_UNKNOWN;
162+
143163
boolean tagsContainServiceName = false;
144164
for (Map.Entry<String, Object> kv : sourceAndMetadata.entrySet()) {
165+
if (dropUnknown) {
166+
if (!schema.containsFields(kv.getKey())) {
167+
continue;
168+
}
169+
Schema.SchemaField fieldDef = schema.getFieldsMap().get(kv.getKey());
170+
if (!SpanFormatter.isTypeCompatible(kv.getValue(), fieldDef.getType())) {
171+
continue;
172+
}
173+
}
145174
if (!tagsContainServiceName && kv.getKey().equals(SERVICE_NAME_KEY)) {
146175
tagsContainServiceName = true;
147176
}
@@ -151,7 +180,7 @@ public static Trace.Span fromIngestDocument(
151180
spanBuilder.addAllTags(tags);
152181
}
153182
}
154-
if (!tagsContainServiceName) {
183+
if (!tagsContainServiceName && (!dropUnknown || schema.containsFields(SERVICE_NAME_KEY))) {
155184
spanBuilder.addTags(
156185
Trace.KeyValue.newBuilder()
157186
.setKey(SERVICE_NAME_KEY)
@@ -165,6 +194,14 @@ public static Trace.Span fromIngestDocument(
165194

166195
protected static Map<String, List<Trace.Span>> convertIndexRequestToTraceFormat(
167196
List<IndexRequest> indexRequests, Schema.IngestSchema schema) {
197+
return convertIndexRequestToTraceFormat(
198+
indexRequests, schema, AstraConfigs.SchemaMode.SCHEMA_MODE_DYNAMIC);
199+
}
200+
201+
protected static Map<String, List<Trace.Span>> convertIndexRequestToTraceFormat(
202+
List<IndexRequest> indexRequests,
203+
Schema.IngestSchema schema,
204+
AstraConfigs.SchemaMode schemaMode) {
168205
// key - index. value - list of docs to be indexed
169206
Map<String, List<Trace.Span>> indexDocs = new HashMap<>();
170207

@@ -175,7 +212,7 @@ protected static Map<String, List<Trace.Span>> convertIndexRequestToTraceFormat(
175212
}
176213
IngestDocument ingestDocument = convertRequestToDocument(indexRequest);
177214
List<Trace.Span> docs = indexDocs.computeIfAbsent(index, key -> new ArrayList<>());
178-
docs.add(BulkApiRequestParser.fromIngestDocument(ingestDocument, schema));
215+
docs.add(BulkApiRequestParser.fromIngestDocument(ingestDocument, schema, schemaMode));
179216
}
180217
return indexDocs;
181218
}

astra/src/main/java/com/slack/astra/server/Astra.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,8 @@ private static Set<Service> getServices(
601601
datasetRateLimitingService,
602602
meterRegistry,
603603
preprocessorConfig.getRateLimitExceededErrorCode(),
604-
schema);
604+
schema,
605+
preprocessorConfig.getSchemaMode());
605606
armeriaServiceBuilder.withAnnotatedService(openSearchBulkApiService);
606607
services.add(armeriaServiceBuilder.build());
607608
}

astra/src/main/java/com/slack/astra/writer/SpanFormatter.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,41 @@ public static Timestamp parseDate(String dateStr, Schema.SchemaFieldType type) {
3939
.build();
4040
}
4141

42+
public static boolean isTypeCompatible(Object value, Schema.SchemaFieldType type) {
43+
try {
44+
switch (type) {
45+
case KEYWORD, TEXT, IP, BINARY, BOOLEAN -> {
46+
return true;
47+
}
48+
case DATE -> {
49+
Instant.parse(value.toString());
50+
return true;
51+
}
52+
case DOUBLE -> {
53+
Double.parseDouble(value.toString());
54+
return true;
55+
}
56+
case FLOAT, HALF_FLOAT -> {
57+
Float.parseFloat(value.toString());
58+
return true;
59+
}
60+
case INTEGER, SHORT, BYTE -> {
61+
Integer.parseInt(value.toString());
62+
return true;
63+
}
64+
case LONG, SCALED_LONG -> {
65+
Long.parseLong(value.toString());
66+
return true;
67+
}
68+
default -> {
69+
return true;
70+
}
71+
}
72+
} catch (Exception e) {
73+
return false;
74+
}
75+
}
76+
4277
public static Trace.KeyValue makeTraceKV(String key, Object value, Schema.SchemaFieldType type) {
4378
Trace.KeyValue.Builder tagBuilder = Trace.KeyValue.newBuilder();
4479
tagBuilder.setKey(key);

astra/src/main/proto/astra_configs.proto

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ enum NodeRole {
1414
PREPROCESSOR = 5;
1515
};
1616

17+
// Schema enforcement mode for the preprocessor.
18+
// Controls how fields not defined in the schema are handled during ingestion.
19+
enum SchemaMode {
20+
// Default: infer types for unknown fields using defaults/heuristics
21+
SCHEMA_MODE_DYNAMIC = 0;
22+
// Drop unknown fields silently - only index fields defined in schema
23+
SCHEMA_MODE_DROP_UNKNOWN = 1;
24+
};
25+
1726
// Astra is a single binary consisting of multiple components.
1827
// AstraConfig is the uber config object for all of Astra.
1928
// This config object controls the role a node plays and it's config.
@@ -328,4 +337,9 @@ message PreprocessorConfig {
328337
// refresh of the dataset rate limits, whether or not a ZK event
329338
// has triggered it
330339
int32 dataset_rate_limit_period_secs = 14;
340+
341+
// Schema enforcement mode - controls how unknown fields are handled
342+
// SCHEMA_MODE_DYNAMIC (default): infer types for unknown fields
343+
// SCHEMA_MODE_DROP_UNKNOWN: drop fields not defined in schema
344+
SchemaMode schema_mode = 15;
331345
}

0 commit comments

Comments
 (0)