This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git
The following commit(s) were added to refs/heads/master by this push:
new 62423e4dfa Decode Istio metadata-exchange peer in Envoy ALS rendering;
harden proto-to-JSON (#13931)
62423e4dfa is described below
commit 62423e4dfa63c0dffad021a57c803bfb1c7c6e4f
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Tue Jun 30 11:18:56 2026 +0800
Decode Istio metadata-exchange peer in Envoy ALS rendering; harden
proto-to-JSON (#13931)
**Why the bug exists.** Envoy `HTTPAccessLogEntry`/`TCPAccessLogEntry`
carry the Istio metadata-exchange peer in
`common_properties.filter_state_objects`, which is `map<string,
google.protobuf.Any>`. The LAL live-debugger serialized the input via a
`JsonFormat` printer built **without** a `TypeRegistry`
(`LalPayloadDebugDump`), so it threw `Cannot find type for url: …` and the
INPUT sample showed an error envelope instead of the entry. Separately, the LAL
output builder's `bindInput` [...]
**How it's fixed.**
- `LalPayloadDebugDump` now uses a well-known-type `TypeRegistry` plus a
sanitizer, so a single un-printable field can no longer blank the whole entry:
an unresolvable / no-slash / corrupt-bytes `Any` degrades to an `@unresolved`
placeholder, and a non-finite `Value` double (`NaN`/`Infinity`, which
`JsonFormat` rejects) is rendered as a string.
- The predictable Istio peer is decoded into readable metadata (pod /
namespace / labels) — legacy Wasm `wasm.*_peer` (`Any{BytesValue}` →
FlatBuffer, reusing the existing `ServiceMetaInfoAdapter`) and modern `*_peer`
(`Any{Struct}`) — for both the live-debug INPUT sample and the persisted log
`content`, covering HTTP **and** TCP access logs.
- Decoding is wired through a new `LalInputDebugRenderer` SPI
(`EnvoyAlsHttpDebugRenderer` / `EnvoyAlsTcpDebugRenderer`), so `log-analyzer`
reaches the receiver-side decoders without depending on the Envoy receiver —
mirroring the existing `LALSourceTypeProvider` seam.
---
docs/en/changes/changes.md | 1 +
.../v2/dsldebug/LalInputDebugRenderers.java | 56 ++++
.../analyzer/v2/dsldebug/LalPayloadDebugDump.java | 245 ++++++++++++++++-
.../log/analyzer/v2/spi/LalInputDebugRenderer.java | 63 +++++
.../v2/dsldebug/LalPayloadDebugDumpTest.java | 154 +++++++++++
.../envoy/als/mx/EnvoyAlsHttpDebugRenderer.java | 47 ++++
.../receiver/envoy/als/mx/EnvoyAlsJsonUtils.java | 157 +++++++++++
.../envoy/als/mx/EnvoyAlsTcpDebugRenderer.java | 49 ++++
.../envoy/persistence/EnvoyAccessLogBuilder.java | 13 +-
...g.oap.log.analyzer.v2.spi.LalInputDebugRenderer | 20 ++
.../envoy/als/mx/EnvoyAlsDebugRendererTest.java | 300 +++++++++++++++++++++
.../persistence/EnvoyAccessLogBuilderTest.java | 70 +++++
12 files changed, 1167 insertions(+), 8 deletions(-)
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 2dd4f183ae..28f0c62c14 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -318,6 +318,7 @@
* Align the default BanyanDB stage `segmentInterval` values so each coarser
stage is an integer multiple of the finer one (`records` cold `3` → `4`,
`metricsMinute` cold `5` → `6`, `metricsHour` warm `7` → `10` and cold `15` →
`20`), keeping hot → warm → cold lifecycle migration on the cheap whole-segment
fast path.
* Fix: `layer-extensions.yml` is now excluded from the `skywalking-oap` jar
and shipped to the distribution `config/` directory, so an operator-edited
`config/layer-extensions.yml` is no longer shadowed by the empty template
bundled in the jar. Because the OAP launch script puts `oap-libs/*.jar` ahead
of `config/` on the classpath, `ResourceUtils.read("layer-extensions.yml")`
previously always resolved the jar-bundled `layers: []` and silently ignored
the operator's file — custom layers [...]
* Fix: the v2 MAL compiler now resolves custom layers referenced as
`Layer.NAME` in an expression. A custom layer declared through a
`layerDefinitions:` block (or `layer-extensions.yml` / the `LayerExtension`
SPI) has no generated `Layer.*` static field, so `service(['svc'],
Layer.IOT_FLEET)` previously failed code generation because `Layer` has no
`IOT_FLEET` field. The compiler now lowers every `Layer.NAME` static-field
reference to a runtime `Layer.nameOf("NAME")` registry lookup, so [...]
+* Fix Envoy ALS rendering for the LAL live-debugger and the persisted log
`content`: an Istio metadata-exchange peer in
`common_properties.filter_state_objects` (legacy Wasm `wasm.*_peer` =
`Any{BytesValue}` wrapping a FlatBuffer, or modern `*_peer` = `Any{Struct}`) is
now decoded into the readable peer metadata (pod / namespace / labels) instead
of an opaque `jsonformat-failed` envelope or base64. The serialization is
hardened so a single un-printable field can no longer blank the whole [...]
#### UI
* Add Airflow layer dashboards and menu i18n under Workflow Scheduler in
Horizon UI (SWIP-7).
diff --git
a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsldebug/LalInputDebugRenderers.java
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsldebug/LalInputDebugRenderers.java
new file mode 100644
index 0000000000..51f82b780b
--- /dev/null
+++
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsldebug/LalInputDebugRenderers.java
@@ -0,0 +1,56 @@
+/*
+ * 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.skywalking.oap.log.analyzer.v2.dsldebug;
+
+import com.google.protobuf.Message;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+import org.apache.skywalking.oap.log.analyzer.v2.spi.LalInputDebugRenderer;
+
+/**
+ * Indexes {@link LalInputDebugRenderer} SPI implementations by their declared
+ * input type. Discovery is pull-based via {@link ServiceLoader} (the same
+ * mechanism the {@code LALSourceTypeProvider} seam uses), so a receiver
+ * plugin's renderer is found on first use without any registration call.
+ */
+final class LalInputDebugRenderers {
+
+ private static final Map<Class<?>, LalInputDebugRenderer> BY_TYPE = load();
+
+ private LalInputDebugRenderers() {
+ }
+
+ private static Map<Class<?>, LalInputDebugRenderer> load() {
+ final Map<Class<?>, LalInputDebugRenderer> map = new HashMap<>();
+ for (final LalInputDebugRenderer renderer :
ServiceLoader.load(LalInputDebugRenderer.class)) {
+ map.put(renderer.inputType(), renderer);
+ }
+ return map;
+ }
+
+ /**
+ * Render {@code msg} via a registered renderer, or {@code null} when no
+ * renderer is registered for its type or the renderer declined.
+ */
+ static String render(final Message msg) {
+ final LalInputDebugRenderer renderer = BY_TYPE.get(msg.getClass());
+ return renderer == null ? null : renderer.render(msg);
+ }
+}
diff --git
a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsldebug/LalPayloadDebugDump.java
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsldebug/LalPayloadDebugDump.java
index c10db4954e..48d5a23d4d 100644
---
a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsldebug/LalPayloadDebugDump.java
+++
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsldebug/LalPayloadDebugDump.java
@@ -21,9 +21,28 @@ package org.apache.skywalking.oap.log.analyzer.v2.dsldebug;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
+import com.google.protobuf.Any;
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.BytesValue;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.DoubleValue;
+import com.google.protobuf.Duration;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.FloatValue;
+import com.google.protobuf.Int32Value;
+import com.google.protobuf.Int64Value;
+import com.google.protobuf.ListValue;
import com.google.protobuf.Message;
import com.google.protobuf.MessageOrBuilder;
+import com.google.protobuf.StringValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Timestamp;
+import com.google.protobuf.UInt32Value;
+import com.google.protobuf.UInt64Value;
+import com.google.protobuf.Value;
import com.google.protobuf.util.JsonFormat;
+import java.util.Base64;
import org.apache.skywalking.apm.network.logging.v3.LogData;
import org.apache.skywalking.oap.server.core.dsldebug.LogDataDebugDump;
import org.apache.skywalking.oap.server.core.dsldebug.ToJson;
@@ -58,8 +77,40 @@ import org.apache.skywalking.oap.server.core.dsldebug.ToJson;
*/
public final class LalPayloadDebugDump {
+ private static final String ANY_TYPE_NAME =
Any.getDescriptor().getFullName();
+
+ private static final String VALUE_TYPE_NAME =
Value.getDescriptor().getFullName();
+
+ /**
+ * Well-known type descriptors so {@link JsonFormat} can resolve and print
+ * {@code google.protobuf.Any}-typed fields (e.g. Envoy ALS
+ * {@code filter_state_objects}, which is {@code map<string, Any>}).
Without
+ * a registry a single unresolvable {@code Any} makes the whole message
+ * un-printable. Anything outside this set is handled by
+ * {@link #sanitizeUnresolvableAny(Message)} below.
+ */
+ private static final JsonFormat.TypeRegistry TYPE_REGISTRY =
+ JsonFormat.TypeRegistry.newBuilder()
+ .add(BytesValue.getDescriptor())
+ .add(StringValue.getDescriptor())
+ .add(BoolValue.getDescriptor())
+ .add(Int32Value.getDescriptor())
+ .add(Int64Value.getDescriptor())
+ .add(UInt32Value.getDescriptor())
+ .add(UInt64Value.getDescriptor())
+ .add(FloatValue.getDescriptor())
+ .add(DoubleValue.getDescriptor())
+ .add(Struct.getDescriptor())
+ .add(Value.getDescriptor())
+ .add(ListValue.getDescriptor())
+ .add(Timestamp.getDescriptor())
+ .add(Duration.getDescriptor())
+ .build();
+
private static final JsonFormat.Printer JSON_PRINTER =
- JsonFormat.printer().omittingInsignificantWhitespace();
+ JsonFormat.printer()
+ .usingTypeRegistry(TYPE_REGISTRY)
+ .omittingInsignificantWhitespace();
private LalPayloadDebugDump() {
}
@@ -83,8 +134,40 @@ public final class LalPayloadDebugDump {
return builder == null ? "null" :
LogDataDebugDump.toJson(builder.build());
}
- /** Generic protobuf renderer via {@link JsonFormat}. */
+ /**
+ * Generic protobuf renderer via {@link JsonFormat}. A receiver plugin can
+ * override the rendering for its own input type via the
+ * {@link
org.apache.skywalking.oap.log.analyzer.v2.spi.LalInputDebugRenderer}
+ * SPI (e.g. Envoy ALS decodes the Istio metadata-exchange peer instead of
+ * emitting opaque base64); when no renderer matches, the generic printer
+ * is used.
+ */
public static String toJson(final Message msg) {
+ if (msg == null) {
+ return "null";
+ }
+ final String custom = LalInputDebugRenderers.render(msg);
+ if (custom != null) {
+ return custom;
+ }
+ return renderProto(msg);
+ }
+
+ /**
+ * Robust generic protobuf render — the well-known {@link #TYPE_REGISTRY}
+ * plus the unresolvable-{@code Any} sanitizer — WITHOUT consulting the
+ * {@link
org.apache.skywalking.oap.log.analyzer.v2.spi.LalInputDebugRenderer}
+ * SPI. A receiver renderer that decodes its own sub-fields (e.g. the Envoy
+ * ALS metadata-exchange peer) calls this to serialize the decoded message
+ * and inherit the same robustness: the decoded fields stay readable AND
any
+ * unrelated unregistered {@code Any} elsewhere in the same message
degrades
+ * to a placeholder instead of failing the whole render. Never re-enters
the
+ * SPI, so a renderer calling it cannot recurse.
+ *
+ * @param msg the message to render.
+ * @return the proto3 JSON string; never {@code null}.
+ */
+ public static String renderProto(final Message msg) {
if (msg == null) {
return "null";
}
@@ -135,7 +218,8 @@ public final class LalPayloadDebugDump {
private static String printProtoOrError(final MessageOrBuilder msg, final
String typeLabel) {
try {
- return JSON_PRINTER.print(msg);
+ final Message message = toMessage(msg);
+ return JSON_PRINTER.print(message == null ? msg :
sanitizeUnresolvableAny(message));
} catch (final Exception e) {
final JsonObject obj = new JsonObject();
obj.addProperty("type", typeLabel);
@@ -144,4 +228,159 @@ public final class LalPayloadDebugDump {
return obj.toString();
}
}
+
+ private static Message toMessage(final MessageOrBuilder msg) {
+ if (msg instanceof Message) {
+ return (Message) msg;
+ }
+ if (msg instanceof Message.Builder) {
+ return ((Message.Builder) msg).build();
+ }
+ return null;
+ }
+
+ /**
+ * Recursively replace every {@code google.protobuf.Any} whose
+ * {@code type_url} the {@link #TYPE_REGISTRY} cannot resolve with a
+ * placeholder that preserves the type URL and the raw bytes. Because
+ * {@code Any}-valued fields (e.g. Envoy ALS {@code filter_state_objects})
+ * are open by design — any filter or operator config can inject arbitrary
+ * messages — a single unresolvable {@code Any} must not blank the whole
+ * input. The placeholder is itself an {@code Any} wrapping a
+ * {@code Struct} so it stays canonical proto3 JSON and prints natively.
+ * Returns the original message unchanged when nothing needs replacing.
+ */
+ private static Message sanitizeUnresolvableAny(final Message message) {
+ final Descriptor descriptor = message.getDescriptorForType();
+ Message.Builder builder = null;
+ for (final FieldDescriptor field : descriptor.getFields()) {
+ if (field.getJavaType() != FieldDescriptor.JavaType.MESSAGE) {
+ continue;
+ }
+ if (field.isMapField()) {
+ final FieldDescriptor valueField =
field.getMessageType().findFieldByName("value");
+ if (valueField == null || valueField.getJavaType() !=
FieldDescriptor.JavaType.MESSAGE) {
+ continue;
+ }
+ final int count = message.getRepeatedFieldCount(field);
+ for (int i = 0; i < count; i++) {
+ final Message entry = (Message)
message.getRepeatedField(field, i);
+ final Message value = (Message) entry.getField(valueField);
+ final Message fixed = fixMessage(value);
+ if (fixed != value) {
+ if (builder == null) {
+ builder = message.toBuilder();
+ }
+ builder.setRepeatedField(field, i,
+ entry.toBuilder().setField(valueField,
fixed).build());
+ }
+ }
+ } else if (field.isRepeated()) {
+ final int count = message.getRepeatedFieldCount(field);
+ for (int i = 0; i < count; i++) {
+ final Message element = (Message)
message.getRepeatedField(field, i);
+ final Message fixed = fixMessage(element);
+ if (fixed != element) {
+ if (builder == null) {
+ builder = message.toBuilder();
+ }
+ builder.setRepeatedField(field, i, fixed);
+ }
+ }
+ } else if (message.hasField(field)) {
+ final Message child = (Message) message.getField(field);
+ final Message fixed = fixMessage(child);
+ if (fixed != child) {
+ if (builder == null) {
+ builder = message.toBuilder();
+ }
+ builder.setField(field, fixed);
+ }
+ }
+ }
+ return builder == null ? message : builder.build();
+ }
+
+ private static Message fixMessage(final Message message) {
+ final String fullName = message.getDescriptorForType().getFullName();
+ if (ANY_TYPE_NAME.equals(fullName)) {
+ return fixAny(message);
+ }
+ if (VALUE_TYPE_NAME.equals(fullName)) {
+ return fixValue(message);
+ }
+ return sanitizeUnresolvableAny(message);
+ }
+
+ /**
+ * Neutralize an {@code Any} that {@link JsonFormat} cannot print: an
+ * unresolvable {@code type_url}, a no-slash {@code type_url} (the printer
+ * only accepts {@code .../type} form and throws otherwise — even for a
+ * well-known type), or a resolvable type whose packed bytes are corrupt.
+ * Each degrades to the {@code @unresolved} placeholder. A resolvable,
well-
+ * formed payload is recursively sanitized and re-packed under its original
+ * {@code type_url} so a bad value nested inside it (e.g. a non-finite
+ * {@code Value}) can't blank the whole message either.
+ */
+ private static Message fixAny(final Message message) {
+ final Any any;
+ try {
+ any = message instanceof Any ? (Any) message :
Any.parseFrom(message.toByteString());
+ } catch (final Exception e) {
+ return message;
+ }
+ final String typeUrl = any.getTypeUrl();
+ if (typeUrl.indexOf('/') < 0) {
+ return Any.pack(unresolvedPlaceholder(any));
+ }
+ final Descriptor descriptor =
TYPE_REGISTRY.find(stripTypeUrlPrefix(typeUrl));
+ if (descriptor == null) {
+ return Any.pack(unresolvedPlaceholder(any));
+ }
+ try {
+ final Message packed =
sanitizeUnresolvableAny(DynamicMessage.parseFrom(descriptor, any.getValue()));
+ return
Any.newBuilder().setTypeUrl(typeUrl).setValue(packed.toByteString()).build();
+ } catch (final Exception e) {
+ return Any.pack(unresolvedPlaceholder(any));
+ }
+ }
+
+ /**
+ * Replace a {@code Value} carrying a non-finite {@code number_value}
+ * (NaN / ±Infinity) — which {@link JsonFormat} refuses to encode and would
+ * otherwise blank the whole message — with the same number rendered as a
+ * string. Other {@code Value} kinds (struct / list) are recursed into so a
+ * non-finite double nested anywhere inside is scrubbed too.
+ */
+ private static Message fixValue(final Message message) {
+ final Value value;
+ try {
+ value = message instanceof Value ? (Value) message :
Value.parseFrom(message.toByteString());
+ } catch (final Exception e) {
+ return message;
+ }
+ if (value.getKindCase() == Value.KindCase.NUMBER_VALUE) {
+ final double number = value.getNumberValue();
+ if (Double.isNaN(number) || Double.isInfinite(number)) {
+ return
Value.newBuilder().setStringValue(Double.toString(number)).build();
+ }
+ return message;
+ }
+ return sanitizeUnresolvableAny(message);
+ }
+
+ private static Struct unresolvedPlaceholder(final Any any) {
+ return Struct.newBuilder()
+ .putFields("@type",
Value.newBuilder().setStringValue(any.getTypeUrl()).build())
+ .putFields("@unresolved",
Value.newBuilder().setBoolValue(true).build())
+ .putFields("value", Value.newBuilder()
+
.setStringValue(Base64.getEncoder().encodeToString(any.getValue().toByteArray()))
+ .build())
+ .build();
+ }
+
+ private static String stripTypeUrlPrefix(final String typeUrl) {
+ final int slash = typeUrl.lastIndexOf('/');
+ return slash < 0 ? typeUrl : typeUrl.substring(slash + 1);
+ }
}
diff --git
a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/spi/LalInputDebugRenderer.java
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/spi/LalInputDebugRenderer.java
new file mode 100644
index 0000000000..6e7b56973a
--- /dev/null
+++
b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/spi/LalInputDebugRenderer.java
@@ -0,0 +1,63 @@
+/*
+ * 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.skywalking.oap.log.analyzer.v2.spi;
+
+import com.google.protobuf.Message;
+
+/**
+ * SPI for receiver plugins to render an operator-readable debug JSON for a
+ * typed LAL input that the generic
+ * {@link com.google.protobuf.util.JsonFormat} printer cannot render
+ * meaningfully.
+ *
+ * <p>The motivating case is Envoy ALS: its
+ * {@code common_properties.filter_state_objects} carry the Istio
+ * metadata-exchange peer as a FlatBuffer wrapped in a
+ * {@code google.protobuf.BytesValue} (legacy Wasm form) or a
+ * {@code google.protobuf.Struct} (modern native form). The generic printer
+ * can at best emit opaque base64 for the FlatBuffer; a renderer decodes it
+ * into the readable peer metadata, reusing the receiver's own decoders.
+ *
+ * <p>Why an SPI rather than a direct call: the dispatch happens in
+ * {@code log-analyzer}, which the receiver plugins depend on (never the
+ * reverse), so {@code log-analyzer} cannot import a receiver's decoder. The
+ * receiver registers an implementation here, discovered via
+ * {@link java.util.ServiceLoader}, mirroring the existing
+ * {@link LALSourceTypeProvider} seam. Implementations register in
+ * {@code
META-INF/services/org.apache.skywalking.oap.log.analyzer.v2.spi.LalInputDebugRenderer}.
+ */
+public interface LalInputDebugRenderer {
+
+ /**
+ * The proto input type this renderer handles, e.g.
+ * {@code HTTPAccessLogEntry}. Matched against the input's runtime class.
+ *
+ * @return the concrete {@link Message} subtype this renderer renders.
+ */
+ Class<? extends Message> inputType();
+
+ /**
+ * Render an operator-readable JSON object string for {@code input}.
+ * Must not throw — return {@code null} on any failure so the framework
+ * falls back to its generic protobuf printer.
+ *
+ * @param input the LAL input message to render.
+ * @return a JSON object string, or {@code null} to fall back.
+ */
+ String render(Message input);
+}
diff --git
a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/dsldebug/LalPayloadDebugDumpTest.java
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/dsldebug/LalPayloadDebugDumpTest.java
new file mode 100644
index 0000000000..25eca4fb09
--- /dev/null
+++
b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/dsldebug/LalPayloadDebugDumpTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.skywalking.oap.log.analyzer.v2.dsldebug;
+
+import com.google.protobuf.Any;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.BytesValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.UInt32Value;
+import com.google.protobuf.Value;
+import io.envoyproxy.envoy.config.core.v3.Metadata;
+import io.envoyproxy.envoy.data.accesslog.v3.AccessLogCommon;
+import io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry;
+import io.envoyproxy.envoy.data.accesslog.v3.HTTPResponseProperties;
+import java.util.Base64;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies the generic protobuf input renderer no longer blanks the whole
+ * message on an {@code Any}-typed field. The Envoy ALS shape
+ * ({@code filter_state_objects} is {@code map<string, google.protobuf.Any>})
+ * is used because it is the reported failure. No Envoy receiver SPI is on this
+ * module's test classpath, so these exercise the generic path:
+ * {@code TypeRegistry} (well-known types) plus the unresolvable-{@code Any}
+ * sanitizer — not a {@code LalInputDebugRenderer} (tested in the envoy
module).
+ */
+class LalPayloadDebugDumpTest {
+
+ @Test
+ void wellKnownAnyRendersAsBase64InsteadOfFailing() {
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+
.setResponse(HTTPResponseProperties.newBuilder().setResponseCode(UInt32Value.of(503)))
+ .setCommonProperties(AccessLogCommon.newBuilder()
+ .putFilterStateObjects(
+ "some.bytes_state",
+
Any.pack(BytesValue.newBuilder().setValue(ByteString.copyFromUtf8("hello")).build())))
+ .build();
+
+ final String json = LalPayloadDebugDump.toJson(entry);
+
+ // Regression: the bare printer used to throw on the BytesValue Any.
+ assertFalse(json.contains("jsonformat-failed"), json);
+ // BytesValue renders as base64 in proto3 JSON ("hello" -> "aGVsbG8=").
+
assertTrue(json.contains(Base64.getEncoder().encodeToString("hello".getBytes())),
json);
+ // The rest of the entry still serialized.
+ assertTrue(json.contains("503"), json);
+ }
+
+ @Test
+ void unresolvableInjectedAnyDegradesToPlaceholderAndKeepsRest() {
+ final Any injected = Any.newBuilder()
+
.setTypeUrl("type.googleapis.com/test.skywalking.CustomFilterState")
+ .setValue(ByteString.copyFromUtf8("opaque-bytes"))
+ .build();
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+
.setResponse(HTTPResponseProperties.newBuilder().setResponseCode(UInt32Value.of(200)))
+ .setCommonProperties(AccessLogCommon.newBuilder()
+ .putFilterStateObjects("test.skywalking.injected", injected))
+ .build();
+
+ final String json = LalPayloadDebugDump.toJson(entry);
+
+ // No whole-message blanking despite an unregistered injected type.
+ assertFalse(json.contains("jsonformat-failed"), json);
+ // The placeholder preserves the original type URL and flags it.
+ assertTrue(json.contains("@unresolved"), json);
+ assertTrue(json.contains("test.skywalking.CustomFilterState"), json);
+
assertTrue(json.contains(Base64.getEncoder().encodeToString("opaque-bytes".getBytes())),
json);
+ // The rest of the entry still serialized.
+ assertTrue(json.contains("200"), json);
+ }
+
+ @Test
+ void nonFiniteValueInStructDoesNotBlankEntry() {
+ // metadata.filter_metadata is map<string, Struct>; a NaN/Infinity in a
+ // Value makes JsonFormat throw, which used to blank the whole entry.
+ final Struct meta = Struct.newBuilder()
+ .putFields("ratio",
Value.newBuilder().setNumberValue(Double.NaN).build())
+ .putFields("ceiling",
Value.newBuilder().setNumberValue(Double.POSITIVE_INFINITY).build())
+ .build();
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+
.setResponse(HTTPResponseProperties.newBuilder().setResponseCode(UInt32Value.of(200)))
+ .setCommonProperties(AccessLogCommon.newBuilder()
+
.setMetadata(Metadata.newBuilder().putFilterMetadata("istio.authz", meta)))
+ .build();
+
+ final String json = LalPayloadDebugDump.toJson(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("NaN"), json); // non-finite scrubbed
to a string
+ assertTrue(json.contains("Infinity"), json);
+ assertTrue(json.contains("200"), json); // rest of the entry
survived
+ }
+
+ @Test
+ void noSlashTypeUrlAnyDegradesToPlaceholder() {
+ // A no-slash type_url throws in JsonFormat.printAny even when the bare
+ // name resolves to a well-known type — must degrade, not blank.
+ final Any noSlash = Any.newBuilder()
+ .setTypeUrl("google.protobuf.BytesValue")
+ .setValue(ByteString.copyFromUtf8("x"))
+ .build();
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+
.setResponse(HTTPResponseProperties.newBuilder().setResponseCode(UInt32Value.of(200)))
+ .setCommonProperties(AccessLogCommon.newBuilder()
+ .putFilterStateObjects("no.slash", noSlash))
+ .build();
+
+ final String json = LalPayloadDebugDump.toJson(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("@unresolved"), json);
+ assertTrue(json.contains("200"), json);
+ }
+
+ @Test
+ void resolvableTypeUrlWithCorruptBytesDegradesToPlaceholder() {
+ // type_url resolves (Struct) but the packed bytes are garbage — the
+ // printer parses them and throws; must degrade, not blank.
+ final Any corrupt = Any.newBuilder()
+ .setTypeUrl("type.googleapis.com/google.protobuf.Struct")
+ .setValue(ByteString.copyFrom(new byte[] {(byte) 0xFF, (byte)
0xFF, (byte) 0xFF, (byte) 0xFF}))
+ .build();
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+
.setResponse(HTTPResponseProperties.newBuilder().setResponseCode(UInt32Value.of(200)))
+ .setCommonProperties(AccessLogCommon.newBuilder()
+ .putFilterStateObjects("corrupt", corrupt))
+ .build();
+
+ final String json = LalPayloadDebugDump.toJson(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("@unresolved"), json);
+ assertTrue(json.contains("200"), json);
+ }
+}
diff --git
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsHttpDebugRenderer.java
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsHttpDebugRenderer.java
new file mode 100644
index 0000000000..8a4295b0f2
--- /dev/null
+++
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsHttpDebugRenderer.java
@@ -0,0 +1,47 @@
+/*
+ * 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.skywalking.oap.server.receiver.envoy.als.mx;
+
+import com.google.protobuf.Message;
+import io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry;
+import org.apache.skywalking.oap.log.analyzer.v2.spi.LalInputDebugRenderer;
+
+/**
+ * Bridges LAL live-debug input rendering for Envoy {@link HTTPAccessLogEntry}
+ * to {@link EnvoyAlsJsonUtils}, so the Istio metadata-exchange peer in
+ * {@code filter_state_objects} is shown decoded rather than as opaque base64.
+ * Registered via {@code META-INF/services}; discovered by the framework's
+ * {@link LalInputDebugRenderer} SPI.
+ */
+public class EnvoyAlsHttpDebugRenderer implements LalInputDebugRenderer {
+
+ @Override
+ public Class<? extends Message> inputType() {
+ return HTTPAccessLogEntry.class;
+ }
+
+ @Override
+ public String render(final Message input) {
+ try {
+ return EnvoyAlsJsonUtils.toJSON((HTTPAccessLogEntry) input);
+ } catch (final Exception e) {
+ return null; // fall back to the framework's generic protobuf
printer
+ }
+ }
+}
diff --git
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsJsonUtils.java
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsJsonUtils.java
new file mode 100644
index 0000000000..1a21c094da
--- /dev/null
+++
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsJsonUtils.java
@@ -0,0 +1,157 @@
+/*
+ * 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.skywalking.oap.server.receiver.envoy.als.mx;
+
+import Wasm.Common.FlatNode;
+import com.google.protobuf.Any;
+import com.google.protobuf.BytesValue;
+import com.google.protobuf.Message;
+import com.google.protobuf.Struct;
+import io.envoyproxy.envoy.data.accesslog.v3.AccessLogCommon;
+import io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry;
+import io.envoyproxy.envoy.data.accesslog.v3.TCPAccessLogEntry;
+import java.nio.ByteBuffer;
+import java.util.Optional;
+import java.util.Set;
+import org.apache.skywalking.oap.log.analyzer.v2.dsldebug.LalPayloadDebugDump;
+
+/**
+ * Canonical "Envoy ALS entry to readable JSON" for SkyWalking, covering both
+ * the HTTP and TCP access-log shapes (both feed the LAL pipeline — see
+ * {@code LogsPersistence} / {@code TCPLogsPersistence}). The Istio
+ * metadata-exchange peer objects in {@code
common_properties.filter_state_objects}
+ * are decoded into readable {@code Struct}s instead of opaque base64, then the
+ * entry is serialized.
+ *
+ * <p>Two encodings are handled, both reusing the same decoders the runtime
+ * analyzer uses ({@link ServiceMetaInfoAdapter} / {@link
MetaExchangeALSHTTPAnalyzer}):
+ * <ul>
+ * <li>legacy Wasm ({@code wasm.upstream_peer} / {@code
wasm.downstream_peer}):
+ * {@code Any{BytesValue}} wrapping a {@code FlatNode} FlatBuffer.</li>
+ * <li>modern native ({@code upstream_peer} / {@code downstream_peer}):
+ * {@code Any{Struct}}.</li>
+ * </ul>
+ *
+ * <p>A decoded peer is re-packed as {@code Any{Struct}} so the canonical
+ * proto3 JSON shape is preserved, then the whole entry is serialized by
+ * {@link LalPayloadDebugDump#renderProto(com.google.protobuf.Message)} — the
+ * framework's robust printer (well-known {@code TypeRegistry} plus an
+ * unresolvable-{@code Any} sanitizer). This matters because
+ * {@code filter_state_objects} is open by design: a non-peer entry can carry
+ * an arbitrary unregistered type, and the robust printer degrades just that
+ * one field to a placeholder while keeping the decoded peer readable — rather
+ * than failing the whole entry and losing the peer decode.
+ */
+public final class EnvoyAlsJsonUtils {
+
+ private static final Set<String> PEER_KEYS = Set.of(
+ MetaExchangeALSHTTPAnalyzer.UPSTREAM_KEY, // "wasm.upstream_peer"
(legacy FlatBuffer)
+ MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_KEY, // "wasm.downstream_peer"
+ MetaExchangeALSHTTPAnalyzer.UPSTREAM_PEER, // "upstream_peer"
(modern Struct)
+ MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_PEER // "downstream_peer"
+ );
+
+ private EnvoyAlsJsonUtils() {
+ }
+
+ /**
+ * Serialize an HTTP ALS entry to JSON with metadata-exchange peers
decoded.
+ * Non-peer {@code filter_state_objects} of unregistered types do not fail
+ * the render — they degrade to a placeholder while the peer stays decoded.
+ *
+ * @param entry the HTTP access log entry to render.
+ * @return the entry as proto3 JSON, peers decoded where present.
+ */
+ public static String toJSON(final HTTPAccessLogEntry entry) {
+ if (!entry.hasCommonProperties()) {
+ return LalPayloadDebugDump.renderProto(entry);
+ }
+ return LalPayloadDebugDump.renderProto(
+
entry.toBuilder().setCommonProperties(decodePeers(entry.getCommonProperties())).build());
+ }
+
+ /**
+ * Serialize a TCP ALS entry to JSON with metadata-exchange peers decoded.
+ * TCP shares {@code AccessLogCommon} with HTTP, so the same peer keys and
+ * encodings apply.
+ *
+ * @param entry the TCP access log entry to render.
+ * @return the entry as proto3 JSON, peers decoded where present.
+ */
+ public static String toJSON(final TCPAccessLogEntry entry) {
+ if (!entry.hasCommonProperties()) {
+ return LalPayloadDebugDump.renderProto(entry);
+ }
+ return LalPayloadDebugDump.renderProto(
+
entry.toBuilder().setCommonProperties(decodePeers(entry.getCommonProperties())).build());
+ }
+
+ /**
+ * Dispatch overload for callers holding only a {@link Message} (e.g. the
+ * LAL output builder's {@code bindInput}). Routes ALS entries to the
+ * peer-decoding overloads above; any other message is rendered by the
+ * framework's robust printer so an unregistered {@code Any} still cannot
+ * throw.
+ *
+ * @param entry the message to render.
+ * @return proto3 JSON; peers decoded for ALS entries.
+ */
+ public static String toJSON(final Message entry) {
+ if (entry instanceof HTTPAccessLogEntry) {
+ return toJSON((HTTPAccessLogEntry) entry);
+ }
+ if (entry instanceof TCPAccessLogEntry) {
+ return toJSON((TCPAccessLogEntry) entry);
+ }
+ return LalPayloadDebugDump.renderProto(entry);
+ }
+
+ private static AccessLogCommon decodePeers(final AccessLogCommon common) {
+ if (common.getFilterStateObjectsCount() == 0) {
+ return common;
+ }
+ final AccessLogCommon.Builder builder = common.toBuilder();
+ // iterate the original (immutable) map; mutate the builder
+ common.getFilterStateObjectsMap().forEach((key, value) -> {
+ if (PEER_KEYS.contains(key)) {
+ decodePeer(key, value).ifPresent(struct ->
+ builder.putFilterStateObjects(key, Any.pack(struct)));
+ }
+ });
+ return builder.build();
+ }
+
+ private static Optional<Struct> decodePeer(final String key, final Any
value) {
+ try {
+ if (key.equals(MetaExchangeALSHTTPAnalyzer.UPSTREAM_KEY)
+ || key.equals(MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_KEY)) {
+ // legacy Wasm: Any{BytesValue} -> FlatBuffer -> Struct
+ final ByteBuffer buffer = ByteBuffer.wrap(
+
BytesValue.parseFrom(value.getValue()).getValue().toByteArray());
+ return Optional.of(
+ ServiceMetaInfoAdapter.extractStructFromNodeFlatBuffer(
+ FlatNode.getRootAsFlatNode(buffer)));
+ }
+ // modern native: Any{Struct}
+ return Optional.of(value.unpack(Struct.class));
+ } catch (final Exception e) {
+ return Optional.empty(); // leave the raw Any; the registry still
renders it as base64
+ }
+ }
+}
diff --git
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsTcpDebugRenderer.java
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsTcpDebugRenderer.java
new file mode 100644
index 0000000000..d91d7acd35
--- /dev/null
+++
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsTcpDebugRenderer.java
@@ -0,0 +1,49 @@
+/*
+ * 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.skywalking.oap.server.receiver.envoy.als.mx;
+
+import com.google.protobuf.Message;
+import io.envoyproxy.envoy.data.accesslog.v3.TCPAccessLogEntry;
+import org.apache.skywalking.oap.log.analyzer.v2.spi.LalInputDebugRenderer;
+
+/**
+ * Bridges LAL live-debug input rendering for Envoy {@link TCPAccessLogEntry}
+ * to {@link EnvoyAlsJsonUtils}. TCP access logs feed the LAL pipeline via
+ * {@code TCPLogsPersistence}, and share {@code AccessLogCommon} (hence the
same
+ * metadata-exchange peer keys and encodings) with HTTP, so the peer in
+ * {@code filter_state_objects} is shown decoded rather than as opaque base64.
+ * Registered via {@code META-INF/services}; discovered by the framework's
+ * {@link LalInputDebugRenderer} SPI.
+ */
+public class EnvoyAlsTcpDebugRenderer implements LalInputDebugRenderer {
+
+ @Override
+ public Class<? extends Message> inputType() {
+ return TCPAccessLogEntry.class;
+ }
+
+ @Override
+ public String render(final Message input) {
+ try {
+ return EnvoyAlsJsonUtils.toJSON((TCPAccessLogEntry) input);
+ } catch (final Exception e) {
+ return null; // fall back to the framework's generic protobuf
printer
+ }
+ }
+}
diff --git
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilder.java
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilder.java
index 2b0efb7f5a..e1eee5ba8c 100644
---
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilder.java
+++
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilder.java
@@ -20,13 +20,12 @@ package
org.apache.skywalking.oap.server.receiver.envoy.persistence;
import com.google.gson.JsonObject;
import com.google.protobuf.Message;
-import lombok.SneakyThrows;
import org.apache.skywalking.oap.server.core.query.type.ContentType;
import org.apache.skywalking.oap.server.core.source.Log;
import org.apache.skywalking.oap.server.core.source.LogBuilder;
import org.apache.skywalking.oap.server.core.source.LogMetadata;
import org.apache.skywalking.oap.server.library.module.ModuleManager;
-import org.apache.skywalking.oap.server.library.util.ProtoBufJsonUtils;
+import
org.apache.skywalking.oap.server.receiver.envoy.als.mx.EnvoyAlsJsonUtils;
/**
* LAL output builder for envoy access logs (both HTTP and TCP).
@@ -56,10 +55,15 @@ public class EnvoyAccessLogBuilder extends LogBuilder {
}
@Override
- @SneakyThrows
public void bindInput(final LogMetadata metadata, final Object input) {
if (input != null) {
- this.accessLogEntryJson = ProtoBufJsonUtils.toJSON((Message)
input);
+ // Robust, peer-aware ALS serialization: decodes the Istio
+ // metadata-exchange peer in filter_state_objects and degrades any
+ // unregistered Any to a placeholder. Must not throw — bindInput
+ // runs eagerly in the generated execute() (before any debug
+ // capture), so a throw here would abort the whole rule and drop
+ // the log.
+ this.accessLogEntryJson = EnvoyAlsJsonUtils.toJSON((Message)
input);
}
// Envoy ALS doesn't deliver LogData, so skip super.bindInput's LogData
// cast branch and just run the metadata-derived field population.
@@ -67,7 +71,6 @@ public class EnvoyAccessLogBuilder extends LogBuilder {
}
@Override
- @SneakyThrows
public void init(final LogMetadata metadata, final Object input,
final ModuleManager moduleManager) {
ensureInitialized(moduleManager);
diff --git
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/resources/META-INF/services/org.apache.skywalking.oap.log.analyzer.v2.spi.LalInputDebugRenderer
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/resources/META-INF/services/org.apache.skywalking.oap.log.analyzer.v2.spi.LalInputDebugRenderer
new file mode 100644
index 0000000000..0ddc3c96eb
--- /dev/null
+++
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/main/resources/META-INF/services/org.apache.skywalking.oap.log.analyzer.v2.spi.LalInputDebugRenderer
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+#
+
+org.apache.skywalking.oap.server.receiver.envoy.als.mx.EnvoyAlsHttpDebugRenderer
+org.apache.skywalking.oap.server.receiver.envoy.als.mx.EnvoyAlsTcpDebugRenderer
diff --git
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsDebugRendererTest.java
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsDebugRendererTest.java
new file mode 100644
index 0000000000..489321f54e
--- /dev/null
+++
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/als/mx/EnvoyAlsDebugRendererTest.java
@@ -0,0 +1,300 @@
+/*
+ * 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.skywalking.oap.server.receiver.envoy.als.mx;
+
+import Wasm.Common.FlatNode;
+import Wasm.Common.KeyVal;
+import com.google.flatbuffers.FlatBufferBuilder;
+import com.google.protobuf.Any;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.BytesValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.UInt32Value;
+import com.google.protobuf.Value;
+import io.envoyproxy.envoy.config.core.v3.Metadata;
+import io.envoyproxy.envoy.data.accesslog.v3.AccessLogCommon;
+import io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry;
+import io.envoyproxy.envoy.data.accesslog.v3.HTTPRequestProperties;
+import io.envoyproxy.envoy.data.accesslog.v3.HTTPResponseProperties;
+import io.envoyproxy.envoy.data.accesslog.v3.TCPAccessLogEntry;
+import org.apache.skywalking.oap.log.analyzer.v2.dsldebug.LalPayloadDebugDump;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies the Istio metadata-exchange peer injected into Envoy ALS
+ * {@code filter_state_objects} is decoded into readable metadata instead of
+ * opaque base64, for both encodings, and that the
+ * {@link EnvoyAlsHttpDebugRenderer} / {@link EnvoyAlsTcpDebugRenderer} SPI
+ * shims are wired into the framework dump path.
+ */
+class EnvoyAlsDebugRendererTest {
+
+ private static HTTPAccessLogEntry entryWith(final String key, final Any
peer) {
+ return HTTPAccessLogEntry.newBuilder()
+
.setResponse(HTTPResponseProperties.newBuilder().setResponseCode(UInt32Value.of(200)))
+
.setCommonProperties(AccessLogCommon.newBuilder().putFilterStateObjects(key,
peer))
+ .build();
+ }
+
+ private static TCPAccessLogEntry tcpEntryWith(final String key, final Any
peer) {
+ return TCPAccessLogEntry.newBuilder()
+
.setCommonProperties(AccessLogCommon.newBuilder().putFilterStateObjects(key,
peer))
+ .build();
+ }
+
+ /** Legacy Wasm form: Any{BytesValue} wrapping a FlatNode FlatBuffer. */
+ private static Any legacyWasmPeer(final String name, final String
namespace,
+ final String labelKey, final String
labelVal) {
+ final FlatBufferBuilder fbb = new FlatBufferBuilder();
+ final int nameOff = fbb.createString(name);
+ final int nsOff = fbb.createString(namespace);
+ final int keyOff = fbb.createString(labelKey);
+ final int valOff = fbb.createString(labelVal);
+ final int kvOff = KeyVal.createKeyVal(fbb, keyOff, valOff);
+ final int labelsOff = FlatNode.createLabelsVector(fbb, new int[]
{kvOff});
+ final int node = FlatNode.createFlatNode(fbb, nameOff, nsOff,
labelsOff, 0, 0, 0, 0, 0, 0, 0);
+ fbb.finish(node);
+ final BytesValue wrapped = BytesValue.newBuilder()
+ .setValue(ByteString.copyFrom(fbb.sizedByteArray()))
+ .build();
+ return Any.pack(wrapped);
+ }
+
+ /** Modern native form: Any{Struct}. */
+ private static Any modernStructPeer(final String name, final String
namespace) {
+ final Struct struct = Struct.newBuilder()
+ .putFields("NAME", Value.newBuilder().setStringValue(name).build())
+ .putFields("NAMESPACE",
Value.newBuilder().setStringValue(namespace).build())
+ .build();
+ return Any.pack(struct);
+ }
+
+ @Test
+ void decodesLegacyWasmFlatBufferPeer() throws Exception {
+ final HTTPAccessLogEntry entry = entryWith(
+ MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_KEY,
+ legacyWasmPeer("reviews-v3-pod", "bookinfo", "app", "reviews"));
+
+ final String json = EnvoyAlsJsonUtils.toJSON(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("reviews-v3-pod"), json);
+ assertTrue(json.contains("bookinfo"), json);
+ assertTrue(json.contains("reviews"), json);
+ assertTrue(json.contains("200"), json);
+ }
+
+ @Test
+ void decodesModernStructPeer() throws Exception {
+ final HTTPAccessLogEntry entry = entryWith(
+ MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_PEER,
+ modernStructPeer("ratings-v1-pod", "bookinfo"));
+
+ final String json = EnvoyAlsJsonUtils.toJSON(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("ratings-v1-pod"), json);
+ assertTrue(json.contains("bookinfo"), json);
+ }
+
+ /** The renderer is discovered via ServiceLoader and used by the framework
dump path. */
+ @Test
+ void spiWiredIntoFrameworkDumpPath() {
+ final HTTPAccessLogEntry entry = entryWith(
+ MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_KEY,
+ legacyWasmPeer("details-v1-pod", "bookinfo", "app", "details"));
+
+ final String json = LalPayloadDebugDump.toJson(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("details-v1-pod"), json);
+ }
+
+ @Test
+ void decodesBothUpstreamAndDownstreamLegacyPeers() {
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+ .setCommonProperties(AccessLogCommon.newBuilder()
+
.putFilterStateObjects(MetaExchangeALSHTTPAnalyzer.UPSTREAM_KEY,
+ legacyWasmPeer("reviews-v3-pod", "bookinfo", "app",
"reviews"))
+
.putFilterStateObjects(MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_KEY,
+ legacyWasmPeer("productpage-v1-pod", "bookinfo", "app",
"productpage")))
+ .build();
+
+ final String json = EnvoyAlsJsonUtils.toJSON(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("reviews-v3-pod"), json);
+ assertTrue(json.contains("productpage-v1-pod"), json);
+ }
+
+ @Test
+ void decodesBothUpstreamAndDownstreamModernPeers() {
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+ .setCommonProperties(AccessLogCommon.newBuilder()
+
.putFilterStateObjects(MetaExchangeALSHTTPAnalyzer.UPSTREAM_PEER,
+ modernStructPeer("reviews-v3-pod", "bookinfo"))
+
.putFilterStateObjects(MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_PEER,
+ modernStructPeer("productpage-v1-pod", "bookinfo")))
+ .build();
+
+ final String json = EnvoyAlsJsonUtils.toJSON(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("reviews-v3-pod"), json);
+ assertTrue(json.contains("productpage-v1-pod"), json);
+ }
+
+ /**
+ * Regression for the all-or-nothing bug: an unrelated unregistered
+ * {@code Any} in {@code filter_state_objects} must NOT make the decoded
+ * peer revert to opaque base64 — the peer stays decoded and the unknown
+ * type degrades to a placeholder, in the same output.
+ */
+ @Test
+ void peerStaysDecodedAlongsideUnregisteredFilterState() {
+ final Any unknown = Any.newBuilder()
+
.setTypeUrl("type.googleapis.com/test.skywalking.CustomFilterState")
+ .setValue(ByteString.copyFromUtf8("opaque-bytes"))
+ .build();
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+
.setResponse(HTTPResponseProperties.newBuilder().setResponseCode(UInt32Value.of(200)))
+ .setCommonProperties(AccessLogCommon.newBuilder()
+
.putFilterStateObjects(MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_KEY,
+ legacyWasmPeer("productpage-v1-pod", "bookinfo", "app",
"productpage"))
+ .putFilterStateObjects("test.skywalking.injected", unknown))
+ .build();
+
+ // Direct util.
+ final String json = EnvoyAlsJsonUtils.toJSON(entry);
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("productpage-v1-pod"), json); // peer still
decoded
+ assertTrue(json.contains("@unresolved"), json); // unknown
degraded
+ assertTrue(json.contains("test.skywalking.CustomFilterState"), json);
+ assertTrue(json.contains("200"), json); // standard
field intact
+
+ // Same through the framework SPI dump path.
+ final String viaFramework = LalPayloadDebugDump.toJson(entry);
+ assertTrue(viaFramework.contains("productpage-v1-pod"), viaFramework);
+ assertTrue(viaFramework.contains("@unresolved"), viaFramework);
+ }
+
+ /** A realistic full entry: standard scalar/wrapper fields, metadata
Struct, and a decoded peer. */
+ @Test
+ void rendersFullEntryWithPeerAndStandardFields() {
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+
.setRequest(HTTPRequestProperties.newBuilder().setPath("/productpage").setAuthority("productpage:9080"))
+
.setResponse(HTTPResponseProperties.newBuilder().setResponseCode(UInt32Value.of(200)))
+ .setCommonProperties(AccessLogCommon.newBuilder()
+
.setMetadata(Metadata.newBuilder().putFilterMetadata("istio.authz",
+ Struct.newBuilder()
+ .putFields("policy",
Value.newBuilder().setStringValue("allow").build())
+ .build()))
+
.putFilterStateObjects(MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_KEY,
+ legacyWasmPeer("productpage-v1-pod", "bookinfo",
"version", "v1")))
+ .build();
+
+ final String json = EnvoyAlsJsonUtils.toJSON(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("/productpage"), json); // scalar
+ assertTrue(json.contains("productpage:9080"), json); // scalar
+ assertTrue(json.contains("200"), json); // UInt32Value
wrapper
+ assertTrue(json.contains("allow"), json); // metadata
filter_metadata Struct
+ assertTrue(json.contains("productpage-v1-pod"), json); // decoded peer
+ assertTrue(json.contains("v1"), json); // decoded
peer label
+ }
+
+ // --- TCP access logs: TCPAccessLogEntry feeds LAL via
TCPLogsPersistence, shares AccessLogCommon ---
+
+ @Test
+ void decodesTcpLegacyWasmPeer() {
+ final TCPAccessLogEntry entry = tcpEntryWith(
+ MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_KEY,
+ legacyWasmPeer("ratings-v1-pod", "bookinfo", "app", "ratings"));
+
+ final String json = EnvoyAlsJsonUtils.toJSON(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("ratings-v1-pod"), json);
+ assertTrue(json.contains("bookinfo"), json);
+ }
+
+ @Test
+ void decodesTcpModernStructPeer() {
+ final TCPAccessLogEntry entry = tcpEntryWith(
+ MetaExchangeALSHTTPAnalyzer.UPSTREAM_PEER,
+ modernStructPeer("mysql-pod", "bookinfo"));
+
+ final String json = EnvoyAlsJsonUtils.toJSON(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("mysql-pod"), json);
+ }
+
+ /** The TCP renderer is discovered via ServiceLoader and used by the
framework dump path. */
+ @Test
+ void tcpPeerWiredIntoFrameworkDumpPath() {
+ final TCPAccessLogEntry entry = tcpEntryWith(
+ MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_KEY,
+ legacyWasmPeer("mongodb-pod", "bookinfo", "app", "mongodb"));
+
+ final String json = LalPayloadDebugDump.toJson(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("mongodb-pod"), json);
+ }
+
+ // --- decodePeer failure fallback: a malformed value under a real peer
key must degrade, not blank ---
+
+ @Test
+ void corruptLegacyWasmPeerDoesNotBlankEntry() {
+ // BytesValue parses, but the inner bytes are not a FlatNode
FlatBuffer:
+ // decodePeer catches and keeps the raw Any; the entry must still
render.
+ final Any corrupt = Any.pack(BytesValue.newBuilder()
+ .setValue(ByteString.copyFromUtf8("not-a-flatbuffer")).build());
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+
.setResponse(HTTPResponseProperties.newBuilder().setResponseCode(UInt32Value.of(200)))
+ .setCommonProperties(AccessLogCommon.newBuilder()
+
.putFilterStateObjects(MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_KEY, corrupt))
+ .build();
+
+ final String json = EnvoyAlsJsonUtils.toJSON(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("200"), json); // non-peer field survives
regardless of peer outcome
+ }
+
+ @Test
+ void modernPeerKeyWithNonStructValueDegradesGracefully() {
+ // A *_peer key whose value is not a Struct: value.unpack(Struct)
throws,
+ // decodePeer catches, the raw (unregistered) Any degrades to
@unresolved.
+ final Any notStruct = Any.newBuilder()
+ .setTypeUrl("type.googleapis.com/test.skywalking.NotAStruct")
+ .setValue(ByteString.copyFromUtf8("opaque"))
+ .build();
+ final HTTPAccessLogEntry entry =
entryWith(MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_PEER, notStruct);
+
+ final String json = EnvoyAlsJsonUtils.toJSON(entry);
+
+ assertFalse(json.contains("jsonformat-failed"), json);
+ assertTrue(json.contains("@unresolved"), json);
+ }
+}
diff --git
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilderTest.java
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilderTest.java
index 2c3b39536f..b7f71455f8 100644
---
a/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilderTest.java
+++
b/oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/src/test/java/org/apache/skywalking/oap/server/receiver/envoy/persistence/EnvoyAccessLogBuilderTest.java
@@ -18,7 +18,11 @@
package org.apache.skywalking.oap.server.receiver.envoy.persistence;
+import com.google.protobuf.Any;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.Struct;
import com.google.protobuf.UInt32Value;
+import com.google.protobuf.Value;
import io.envoyproxy.envoy.data.accesslog.v3.AccessLogCommon;
import io.envoyproxy.envoy.data.accesslog.v3.HTTPAccessLogEntry;
import io.envoyproxy.envoy.data.accesslog.v3.HTTPResponseProperties;
@@ -38,13 +42,16 @@ import
org.apache.skywalking.oap.server.core.config.group.EndpointNameGrouping;
import org.apache.skywalking.oap.server.core.query.type.ContentType;
import org.apache.skywalking.oap.server.core.source.Log;
import org.apache.skywalking.oap.server.core.source.SourceReceiver;
+import
org.apache.skywalking.oap.server.receiver.envoy.als.mx.MetaExchangeALSHTTPAnalyzer;
import org.apache.skywalking.oap.server.library.module.ModuleManager;
import org.apache.skywalking.oap.server.library.module.ModuleProviderHolder;
import org.apache.skywalking.oap.server.library.module.ModuleServiceHolder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
@@ -128,6 +135,69 @@ class EnvoyAccessLogBuilderTest {
assertEquals("EnvoyAccessLog", new EnvoyAccessLogBuilder().name());
}
+ /**
+ * {@code bindInput} runs eagerly in the generated {@code execute()}
(before
+ * any debug capture), so it must not throw on an unregistered {@code Any}
+ * in {@code filter_state_objects} — otherwise the whole rule aborts and
the
+ * log is dropped. Regression for the all-or-nothing serialization failure.
+ */
+ @Test
+ void unregisteredFilterStateAnyDoesNotAbortExecution() {
+ final EnvoyAccessLogBuilder builder = new EnvoyAccessLogBuilder();
+ final Any unknown = Any.newBuilder()
+
.setTypeUrl("type.googleapis.com/test.skywalking.CustomFilterState")
+ .setValue(ByteString.copyFromUtf8("opaque-bytes"))
+ .build();
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+
.setResponse(HTTPResponseProperties.newBuilder().setResponseCode(UInt32Value.of(200)))
+ .setCommonProperties(AccessLogCommon.newBuilder()
+ .putFilterStateObjects("test.skywalking.injected", unknown))
+ .build();
+ builder.setService("mesh-svc");
+ builder.setTimestamp(1L);
+ final LogMetadata metadata =
LogMetadata.builder().service("mesh-svc").timestamp(1L).build();
+
+ // Before the fix this throws "Cannot find type for url:
...CustomFilterState".
+ final Log log = assertDoesNotThrow(() -> {
+ builder.init(metadata, entry, moduleManager);
+ return builder.toLog();
+ });
+
+ final String c = log.getContent();
+ assertFalse(c.contains("jsonformat-failed"), c);
+ assertTrue(c.contains("@unresolved"), c); //
degraded, not fatal
+ assertTrue(c.contains("test.skywalking.CustomFilterState"), c);
+ assertTrue(c.contains("200"), c); //
standard field intact
+ }
+
+ /**
+ * End-to-end persistence path: a metadata-exchange peer in
+ * {@code filter_state_objects} is decoded into the readable peer metadata
+ * in the stored log {@code content}, not opaque base64.
+ */
+ @Test
+ void persistedContentContainsDecodedPeer() {
+ final EnvoyAccessLogBuilder builder = new EnvoyAccessLogBuilder();
+ final Any peer = Any.pack(Struct.newBuilder()
+ .putFields("NAME",
Value.newBuilder().setStringValue("productpage-v1-pod").build())
+ .putFields("NAMESPACE",
Value.newBuilder().setStringValue("bookinfo").build())
+ .build());
+ final HTTPAccessLogEntry entry = HTTPAccessLogEntry.newBuilder()
+ .setCommonProperties(AccessLogCommon.newBuilder()
+
.putFilterStateObjects(MetaExchangeALSHTTPAnalyzer.DOWNSTREAM_PEER, peer))
+ .build();
+ builder.setService("mesh-svc");
+ builder.setTimestamp(1L);
+ final LogMetadata metadata =
LogMetadata.builder().service("mesh-svc").timestamp(1L).build();
+
+ builder.init(metadata, entry, moduleManager);
+ final Log log = builder.toLog();
+
+ assertNotNull(log.getContent());
+ assertTrue(log.getContent().contains("productpage-v1-pod"),
log.getContent());
+ assertTrue(log.getContent().contains("bookinfo"), log.getContent());
+ }
+
/**
* Compiles and executes a LAL script that accesses both proto fields from
* the HTTPAccessLogEntry (via {@code parsed.*}) and entity fields from