This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new b6b63149ef75 CAMEL-24377: Add JSON support to CamelEvent
b6b63149ef75 is described below

commit b6b63149ef75e57450dda50cdc94e8576e6e0845
Author: Omar Atie <[email protected]>
AuthorDate: Sat Sep 5 01:31:31 2026 -0700

    CAMEL-24377: Add JSON support to CamelEvent
    
    Adds structured JSON serialization to the CamelEvent SPI via two new
    default methods — asJSon() returning a Map of event metadata, and
    toJSon(int) returning a pretty-printed JSON string. The implementation
    is backed by CamelEventJsonSupport in camel-base, covering all standard
    event types (context, route, exchange, step, service, failure), and
    delegates exception serialization to MessageHelper for proper JSON
    escaping. EventConsole is updated to use asJSon() instead of ad hoc
    toString() fields. Default SPI methods remain backward-compatible;
    the default toJSon path uses StringQuoteHelper.jsonQuote to correctly
    escape special characters. Tests and event-notifier.adoc documentation
    are included.
    
    Closes #25495
    
    Co-authored-by: Cursor Agent <[email protected]>
---
 .../apache/camel/catalog/dev-consoles-openapi.json |  15 ++
 .../apache/camel/catalog/dev-consoles/event.json   |  15 ++
 .../main/java/org/apache/camel/spi/CamelEvent.java |  65 ++++++
 .../camel/impl/event/AbstractContextEvent.java     |  11 +
 .../camel/impl/event/AbstractExchangeEvent.java    |  11 +
 .../camel/impl/event/AbstractRouteEvent.java       |  11 +
 .../camel/impl/event/CamelEventJsonSupport.java    | 199 ++++++++++++++++
 .../impl/event/ServiceStartupFailureEvent.java     |  11 +
 .../camel/impl/event/ServiceStopFailureEvent.java  |  11 +
 .../org/apache/camel/dev-console/event.json        |  15 ++
 .../apache/camel/impl/console/EventConsole.java    |  23 +-
 .../camel/impl/event/CamelEventJsonTest.java       | 257 +++++++++++++++++++++
 .../org/apache/camel/util/StringQuoteHelper.java   |  41 ++++
 .../apache/camel/util/StringQuoteHelperTest.java   |   7 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  14 ++
 .../modules/ROOT/pages/event-notifier.adoc         |  17 ++
 16 files changed, 716 insertions(+), 7 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
index 3418a6261532..5a1454ef76ca 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
@@ -1856,6 +1856,11 @@
                                                                                
                        "message": {
                                                                                
                                "type": "string",
                                                                                
                                "description": "The event's string 
representation"
+                                                                               
                        },
+                                                                               
                        "details": {
+                                                                               
                                "type": "object",
+                                                                               
                                "additionalProperties": true,
+                                                                               
                                "description": "Structured event metadata as 
JSON"
                                                                                
                        }
                                                                                
                }
                                                                                
        },
@@ -1881,6 +1886,11 @@
                                                                                
                        "message": {
                                                                                
                                "type": "string",
                                                                                
                                "description": "The event's string 
representation"
+                                                                               
                        },
+                                                                               
                        "details": {
+                                                                               
                                "type": "object",
+                                                                               
                                "additionalProperties": true,
+                                                                               
                                "description": "Structured event metadata as 
JSON"
                                                                                
                        }
                                                                                
                }
                                                                                
        },
@@ -1906,6 +1916,11 @@
                                                                                
                        "message": {
                                                                                
                                "type": "string",
                                                                                
                                "description": "The event's string 
representation"
+                                                                               
                        },
+                                                                               
                        "details": {
+                                                                               
                                "type": "object",
+                                                                               
                                "additionalProperties": true,
+                                                                               
                                "description": "Structured event metadata as 
JSON"
                                                                                
                        }
                                                                                
                }
                                                                                
        },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/event.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/event.json
index 0d4154a8b543..c23b0d82f87f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/event.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/event.json
@@ -35,6 +35,11 @@
             "message": {
               "type": "string",
               "description": "The event's string representation"
+            },
+            "details": {
+              "type": "object",
+              "additionalProperties": true,
+              "description": "Structured event metadata as JSON"
             }
           }
         },
@@ -60,6 +65,11 @@
             "message": {
               "type": "string",
               "description": "The event's string representation"
+            },
+            "details": {
+              "type": "object",
+              "additionalProperties": true,
+              "description": "Structured event metadata as JSON"
             }
           }
         },
@@ -85,6 +95,11 @@
             "message": {
               "type": "string",
               "description": "The event's string representation"
+            },
+            "details": {
+              "type": "object",
+              "additionalProperties": true,
+              "description": "Structured event metadata as JSON"
             }
           }
         },
diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/CamelEvent.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/CamelEvent.java
index 485ca15839a5..821755ee5f6f 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/CamelEvent.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/CamelEvent.java
@@ -16,11 +16,15 @@
  */
 package org.apache.camel.spi;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
+
 import org.apache.camel.CamelContext;
 import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
 import org.apache.camel.Processor;
 import org.apache.camel.Route;
+import org.apache.camel.util.StringQuoteHelper;
 import org.jspecify.annotations.Nullable;
 
 /**
@@ -94,6 +98,67 @@ public interface CamelEvent {
 
     void setTimestamp(long timestamp);
 
+    /**
+     * Dumps the full event as a pretty-printed JSON string.
+     *
+     * The default implementation serializes flat scalar values ({@link 
Number}, {@link Boolean}, and {@link String})
+     * from {@link #asJSon()}. If a custom implementer overrides {@link 
#asJSon()} to include nested {@link Map} or
+     * {@link List} objects, they should also override this method to produce 
valid JSON.
+     *
+     * @param  indent number of spaces to indent
+     * @return        JSON representation of this event
+     * @since         4.23
+     */
+    default String toJSon(int indent) {
+        Map<String, Object> map = asJSon();
+        String indentText = indent > 0 ? " ".repeat(indent) : "";
+        StringBuilder sb = new StringBuilder(128);
+        sb.append('{');
+        boolean first = true;
+        for (Map.Entry<String, Object> entry : map.entrySet()) {
+            if (!first) {
+                sb.append(',');
+            }
+            first = false;
+            if (indent > 0) {
+                sb.append('\n').append(indentText);
+            }
+            sb.append(StringQuoteHelper.jsonQuote(entry.getKey())).append(':');
+            if (indent > 0) {
+                sb.append(' ');
+            }
+            Object value = entry.getValue();
+            if (value instanceof Number || value instanceof Boolean) {
+                sb.append(value);
+            } else {
+                sb.append(StringQuoteHelper.jsonQuote(String.valueOf(value)));
+            }
+        }
+        if (indent > 0) {
+            sb.append('\n');
+        }
+        sb.append('}');
+        return sb.toString();
+    }
+
+    /**
+     * The full event as a {@link Map} suitable for JSON serialization, 
containing structured metadata for this event.
+     *
+     * The default implementation returns a minimal map with {@code type}, 
optional {@code timestamp}, and
+     * {@code message}. Camel's built-in event classes override this to 
provide richer structured metadata.
+     *
+     * @since 4.23
+     */
+    default Map<String, Object> asJSon() {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("type", getType().name());
+        if (getTimestamp() > 0) {
+            map.put("timestamp", getTimestamp());
+        }
+        map.put("message", toString());
+        return map;
+    }
+
     /**
      * This interface is implemented by all events that contain an exception 
and is used to retrieve the exception in a
      * universal way.
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractContextEvent.java
 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractContextEvent.java
index cf77d7be8fdd..a35b45167a7b 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractContextEvent.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractContextEvent.java
@@ -18,6 +18,7 @@ package org.apache.camel.impl.event;
 
 import java.io.Serial;
 import java.util.EventObject;
+import java.util.Map;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.spi.CamelEvent.CamelContextEvent;
@@ -49,4 +50,14 @@ public abstract class AbstractContextEvent extends 
EventObject implements CamelC
     public void setTimestamp(long timestamp) {
         this.timestamp = timestamp;
     }
+
+    @Override
+    public Map<String, Object> asJSon() {
+        return CamelEventJsonSupport.asJSon(this);
+    }
+
+    @Override
+    public String toJSon(int indent) {
+        return CamelEventJsonSupport.toJSon(this, indent);
+    }
 }
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractExchangeEvent.java
 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractExchangeEvent.java
index 7c7802d07da7..fa95aec83834 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractExchangeEvent.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractExchangeEvent.java
@@ -18,6 +18,7 @@ package org.apache.camel.impl.event;
 
 import java.io.Serial;
 import java.util.EventObject;
+import java.util.Map;
 
 import org.apache.camel.Exchange;
 import org.apache.camel.spi.CamelEvent.ExchangeEvent;
@@ -50,4 +51,14 @@ public abstract class AbstractExchangeEvent extends 
EventObject implements Excha
         this.timestamp = timestamp;
     }
 
+    @Override
+    public Map<String, Object> asJSon() {
+        return CamelEventJsonSupport.asJSon(this);
+    }
+
+    @Override
+    public String toJSon(int indent) {
+        return CamelEventJsonSupport.toJSon(this, indent);
+    }
+
 }
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractRouteEvent.java
 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractRouteEvent.java
index c169c340316d..68eb1d1cee38 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractRouteEvent.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/AbstractRouteEvent.java
@@ -18,6 +18,7 @@ package org.apache.camel.impl.event;
 
 import java.io.Serial;
 import java.util.EventObject;
+import java.util.Map;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.Route;
@@ -51,4 +52,14 @@ public abstract class AbstractRouteEvent extends EventObject 
implements RouteEve
         this.timestamp = timestamp;
     }
 
+    @Override
+    public Map<String, Object> asJSon() {
+        return CamelEventJsonSupport.asJSon(this);
+    }
+
+    @Override
+    public String toJSon(int indent) {
+        return CamelEventJsonSupport.toJSon(this, indent);
+    }
+
 }
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/CamelEventJsonSupport.java
 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/CamelEventJsonSupport.java
new file mode 100644
index 000000000000..0af93aad9ecc
--- /dev/null
+++ 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/CamelEventJsonSupport.java
@@ -0,0 +1,199 @@
+/*
+ * 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.camel.impl.event;
+
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePropertyKey;
+import org.apache.camel.Processor;
+import org.apache.camel.Route;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.support.MessageHelper;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.URISupport;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Support class for serializing {@link CamelEvent} instances to JSON.
+ */
+public final class CamelEventJsonSupport {
+
+    private CamelEventJsonSupport() {
+    }
+
+    public static Map<String, Object> asJSon(CamelEvent event) {
+        JsonObject jo = new JsonObject();
+        jo.put("type", event.getType().name());
+        jo.put("eventClass", event.getClass().getSimpleName());
+        if (event.getTimestamp() > 0) {
+            jo.put("timestamp", event.getTimestamp());
+        }
+        jo.put("message", event.toString());
+
+        if (event instanceof CamelEvent.CamelContextEvent contextEvent) {
+            appendContext(jo, contextEvent.getContext());
+        }
+        if (event instanceof CamelEvent.RouteEvent routeEvent) {
+            appendRoute(jo, routeEvent.getRoute());
+        }
+        if (event instanceof CamelEvent.ExchangeEvent exchangeEvent) {
+            appendExchange(jo, exchangeEvent.getExchange());
+        }
+        if (event instanceof CamelEvent.StepEvent stepEvent) {
+            jo.put("stepId", stepEvent.getStepId());
+        }
+        if (event instanceof CamelEvent.ExchangeSendingEvent sendingEvent) {
+            appendEndpoint(jo, "endpointUri", sendingEvent.getEndpoint());
+        }
+        if (event instanceof CamelEvent.ExchangeSentEvent sentEvent) {
+            appendEndpoint(jo, "endpointUri", sentEvent.getEndpoint());
+            jo.put("timeTaken", sentEvent.getTimeTaken());
+        }
+        if (event instanceof CamelEvent.ExchangeRedeliveryEvent 
redeliveryEvent) {
+            jo.put("attempt", redeliveryEvent.getAttempt());
+            appendException(jo, redeliveryEvent.getExchange().getException());
+        }
+        if (event instanceof CamelEvent.ExchangeFailureEvent failureEvent) {
+            appendFailureHandling(jo, failureEvent);
+        }
+        if (event instanceof CamelEvent.RouteReloadedEvent reloadedEvent) {
+            jo.put("index", reloadedEvent.getIndex());
+            jo.put("total", reloadedEvent.getTotal());
+        }
+        if (event instanceof CamelEvent.RouteRestartingEvent restartingEvent) {
+            jo.put("attempt", restartingEvent.getAttempt());
+        }
+        if (event instanceof CamelEvent.RouteRestartingFailureEvent 
restartingFailureEvent) {
+            jo.put("attempt", restartingFailureEvent.getAttempt());
+            jo.put("exhausted", restartingFailureEvent.isExhausted());
+        }
+        if (event instanceof CamelEvent.ServiceEvent serviceEvent) {
+            appendService(jo, serviceEvent.getService());
+        }
+        if (event instanceof ServiceStartupFailureEvent startupFailureEvent) {
+            appendContext(jo, startupFailureEvent.getContext());
+        } else if (event instanceof ServiceStopFailureEvent stopFailureEvent) {
+            appendContext(jo, stopFailureEvent.getContext());
+        }
+        if (event instanceof CamelEvent.FailureEvent failureEvent) {
+            appendException(jo, failureEvent.getCause());
+        } else if (event instanceof CamelEvent.ExchangeFailureEvent) {
+            Exchange exchange = ((CamelEvent.ExchangeFailureEvent) 
event).getExchange();
+            appendException(jo, exchange.getException());
+        }
+
+        return jo;
+    }
+
+    public static String toJSon(CamelEvent event, int indent) {
+        JsonObject jo = (JsonObject) asJSon(event);
+        if (indent > 0) {
+            return Jsoner.prettyPrint(jo.toJson(), indent);
+        }
+        return Jsoner.prettyPrint(jo.toJson());
+    }
+
+    private static void appendContext(JsonObject jo, CamelContext context) {
+        if (context == null) {
+            return;
+        }
+        jo.put("contextName", context.getName());
+    }
+
+    private static void appendRoute(JsonObject jo, Route route) {
+        if (route == null) {
+            return;
+        }
+        jo.put("routeId", route.getRouteId());
+        if (ObjectHelper.isNotEmpty(route.getGroup())) {
+            jo.put("routeGroup", route.getGroup());
+        }
+        if (route.getConsumer() != null && route.getConsumer().getEndpoint() 
!= null) {
+            jo.put("fromEndpointUri", 
sanitizeUri(route.getConsumer().getEndpoint().getEndpointUri()));
+        }
+    }
+
+    private static void appendExchange(JsonObject jo, Exchange exchange) {
+        if (exchange == null) {
+            return;
+        }
+        jo.put("exchangeId", exchange.getExchangeId());
+        if (exchange.getFromRouteId() != null) {
+            jo.put("fromRouteId", exchange.getFromRouteId());
+        }
+        String routeId = 
exchange.getProperty(ExchangePropertyKey.FAILURE_ROUTE_ID, String.class);
+        if (routeId == null) {
+            routeId = exchange.getFromRouteId();
+        }
+        if (routeId != null) {
+            jo.put("routeId", routeId);
+        }
+    }
+
+    private static void appendEndpoint(JsonObject jo, String name, Endpoint 
endpoint) {
+        if (endpoint != null) {
+            jo.put(name, sanitizeUri(endpoint.getEndpointUri()));
+        }
+    }
+
+    private static void appendFailureHandling(JsonObject jo, 
CamelEvent.ExchangeFailureEvent failureEvent) {
+        jo.put("deadLetterChannel", failureEvent.isDeadLetterChannel());
+        if (failureEvent.getDeadLetterUri() != null) {
+            jo.put("deadLetterUri", 
sanitizeUri(failureEvent.getDeadLetterUri()));
+        }
+        Processor failureHandler = failureEvent.getFailureHandler();
+        if (failureHandler != null) {
+            jo.put("failureHandler", failureHandler.getClass().getName());
+        }
+    }
+
+    private static void appendService(JsonObject jo, Object service) {
+        if (service != null) {
+            if (service instanceof String stringService) {
+                jo.put("service", stringService);
+            } else {
+                jo.put("service", service.getClass().getName());
+            }
+        }
+    }
+
+    private static void appendException(JsonObject jo, Throwable cause) {
+        if (cause == null) {
+            return;
+        }
+        try {
+            jo.put("exception", 
MessageHelper.dumpExceptionAsJSonObject(cause).get("exception"));
+        } catch (Exception e) {
+            jo.put("exceptionMessage", cause.getMessage());
+        }
+    }
+
+    private static String sanitizeUri(String uri) {
+        if (uri == null) {
+            return null;
+        }
+        try {
+            return URISupport.sanitizeUri(uri);
+        } catch (Exception e) {
+            return uri;
+        }
+    }
+}
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/ServiceStartupFailureEvent.java
 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/ServiceStartupFailureEvent.java
index 8160ac103e4f..2359093dba6d 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/ServiceStartupFailureEvent.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/ServiceStartupFailureEvent.java
@@ -18,6 +18,7 @@ package org.apache.camel.impl.event;
 
 import java.io.Serial;
 import java.util.EventObject;
+import java.util.Map;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.spi.CamelEvent;
@@ -61,6 +62,16 @@ public class ServiceStartupFailureEvent extends EventObject 
implements CamelEven
         this.timestamp = timestamp;
     }
 
+    @Override
+    public Map<String, Object> asJSon() {
+        return CamelEventJsonSupport.asJSon(this);
+    }
+
+    @Override
+    public String toJSon(int indent) {
+        return CamelEventJsonSupport.toJSon(this, indent);
+    }
+
     @Override
     public String toString() {
         return "Service startup failure: " + service + " due to " + 
cause.getMessage();
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/ServiceStopFailureEvent.java
 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/ServiceStopFailureEvent.java
index aeeb118a4e71..a44cad0d5d71 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/impl/event/ServiceStopFailureEvent.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/impl/event/ServiceStopFailureEvent.java
@@ -18,6 +18,7 @@ package org.apache.camel.impl.event;
 
 import java.io.Serial;
 import java.util.EventObject;
+import java.util.Map;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.spi.CamelEvent;
@@ -61,6 +62,16 @@ public class ServiceStopFailureEvent extends EventObject 
implements CamelEvent.S
         this.timestamp = timestamp;
     }
 
+    @Override
+    public Map<String, Object> asJSon() {
+        return CamelEventJsonSupport.asJSon(this);
+    }
+
+    @Override
+    public String toJSon(int indent) {
+        return CamelEventJsonSupport.toJSon(this, indent);
+    }
+
     @Override
     public String toString() {
         return "Service stop failure: " + service + " due to " + 
cause.getMessage();
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/event.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/event.json
index 0d4154a8b543..c23b0d82f87f 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/event.json
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/event.json
@@ -35,6 +35,11 @@
             "message": {
               "type": "string",
               "description": "The event's string representation"
+            },
+            "details": {
+              "type": "object",
+              "additionalProperties": true,
+              "description": "Structured event metadata as JSON"
             }
           }
         },
@@ -60,6 +65,11 @@
             "message": {
               "type": "string",
               "description": "The event's string representation"
+            },
+            "details": {
+              "type": "object",
+              "additionalProperties": true,
+              "description": "Structured event metadata as JSON"
             }
           }
         },
@@ -85,6 +95,11 @@
             "message": {
               "type": "string",
               "description": "The event's string representation"
+            },
+            "details": {
+              "type": "object",
+              "additionalProperties": true,
+              "description": "Structured event metadata as JSON"
             }
           }
         },
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/EventConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/EventConsole.java
index 2371f8680c64..af8a56eec145 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/EventConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/EventConsole.java
@@ -39,7 +39,8 @@ public class EventConsole extends AbstractDevConsole {
             @Metadata(description = "The event type") String type,
             @Metadata(description = "Epoch time in milliseconds (only present 
when known)") Long timestamp,
             @Metadata(description = "The exchange ID (only present for 
exchange events)") String exchangeId,
-            @Metadata(description = "The event's string representation") 
String message) {
+            @Metadata(description = "The event's string representation") 
String message,
+            @Metadata(description = "Structured event metadata as JSON") 
Map<String, Object> details) {
     }
 
     public record Response(
@@ -160,13 +161,21 @@ public class EventConsole extends AbstractDevConsole {
         CamelEvent event = events[cursor];
         while (pos < capacity) {
             if (event != null) {
-                Long timestamp = event.getTimestamp() > 0 ? 
event.getTimestamp() : null;
-                String exchangeId = null;
-                if (event instanceof CamelEvent.ExchangeEvent) {
-                    CamelEvent.ExchangeEvent ee = (CamelEvent.ExchangeEvent) 
event;
-                    exchangeId = ee.getExchange().getExchangeId();
+                Map<String, Object> json = event.asJSon();
+                Long timestamp = null;
+                Object ts = json.get("timestamp");
+                if (ts instanceof Number number && number.longValue() > 0) {
+                    timestamp = number.longValue();
                 }
-                arr.add(new EventEntry(event.getType().toString(), timestamp, 
exchangeId, event.toString()));
+                String exchangeId = (String) json.get("exchangeId");
+                Object type = json.get("type");
+                String message = (String) json.get("message");
+                arr.add(new EventEntry(
+                        type != null ? type.toString() : 
event.getType().toString(),
+                        timestamp,
+                        exchangeId,
+                        message,
+                        json));
             }
             // move to next
             pos++;
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/event/CamelEventJsonTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/event/CamelEventJsonTest.java
new file mode 100644
index 000000000000..f270405f7120
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/event/CamelEventJsonTest.java
@@ -0,0 +1,257 @@
+/*
+ * 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.camel.impl.event;
+
+import java.util.Map;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.support.EventNotifierSupport;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CamelEventJsonTest extends ContextTestSupport {
+
+    @Test
+    void testContextStartedEventAsJson() throws Exception {
+        CamelEvent event = new CamelContextStartedEvent(context);
+        event.setTimestamp(123456789L);
+
+        Map<String, Object> json = event.asJSon();
+
+        assertThat(json)
+                .containsEntry("type", "CamelContextStarted")
+                .containsEntry("eventClass", "CamelContextStartedEvent")
+                .containsEntry("timestamp", 123456789L)
+                .containsEntry("contextName", context.getName())
+                .containsKey("message");
+        assertThat(event.toJSon(2)).contains("\"type\": 
\"CamelContextStarted\"");
+    }
+
+    @Test
+    void testExchangeFailedEventAsJson() throws Exception {
+        getMockEndpoint("mock:result").expectedMessageCount(0);
+
+        context.getManagementStrategy().addEventNotifier(new 
EventNotifierSupport() {
+            @Override
+            public void notify(CamelEvent event) {
+                if (event instanceof CamelEvent.ExchangeFailedEvent 
failedEvent) {
+                    Map<String, Object> json = failedEvent.asJSon();
+                    assertThat(json)
+                            .containsEntry("type", "ExchangeFailed")
+                            .containsKey("exchangeId")
+                            .containsKey("routeId")
+                            .containsKey("message");
+                    assertThat(json.get("exception")).isInstanceOf(Map.class);
+                    @SuppressWarnings("unchecked")
+                    Map<String, Object> exception = (Map<String, Object>) 
json.get("exception");
+                    assertThat(exception)
+                            .containsEntry("type", 
"java.lang.IllegalArgumentException")
+                            .containsEntry("message", "boom");
+                }
+            }
+        });
+
+        try {
+            template.sendBody("direct:fail", "Hello");
+        } catch (Exception e) {
+            // expected
+        }
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void testExchangeSentEventAsJson() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        ExchangeSentEvent event = new ExchangeSentEvent(
+                template.send("direct:sent", exchange -> 
exchange.getMessage().setBody("Hello")),
+                context.getEndpoint("mock:result"),
+                42);
+        event.setTimestamp(999L);
+
+        Map<String, Object> json = event.asJSon();
+
+        assertThat(json)
+                .containsEntry("type", "ExchangeSent")
+                .containsEntry("timestamp", 999L)
+                .containsEntry("timeTaken", 42L)
+                .containsKey("exchangeId")
+                .containsKey("endpointUri");
+    }
+
+    @Test
+    void testExchangeFailureHandlingEventAsJson() throws Exception {
+        Exchange exchange = createExchangeWithBody("payload");
+        exchange.setException(new RuntimeException("failed"));
+
+        ExchangeFailureHandlingEvent event = new ExchangeFailureHandlingEvent(
+                exchange,
+                ex -> {
+                },
+                true,
+                "mock:dead");
+
+        Map<String, Object> json = event.asJSon();
+
+        assertThat(json)
+                .containsEntry("type", "ExchangeFailureHandling")
+                .containsEntry("deadLetterChannel", true)
+                .containsEntry("deadLetterUri", "mock:dead")
+                .containsKey("failureHandler")
+                .containsKey("exception");
+    }
+
+    @Test
+    void testRouteReloadedEventAsJson() {
+        Route route = context.getRoute("jsonRoute");
+        RouteReloadedEvent event = new RouteReloadedEvent(route, 2, 5);
+
+        Map<String, Object> json = event.asJSon();
+
+        assertThat(json)
+                .containsEntry("type", "RouteReloaded")
+                .containsEntry("routeId", "jsonRoute")
+                .containsEntry("index", 2)
+                .containsEntry("total", 5);
+    }
+
+    @Test
+    void testRouteRestartingFailureEventAsJson() {
+        Route route = context.getRoute("jsonRoute");
+        RouteRestartingFailureEvent event
+                = new RouteRestartingFailureEvent(route, 3, new 
IllegalStateException("restart"), true);
+
+        Map<String, Object> json = event.asJSon();
+
+        assertThat(json)
+                .containsEntry("type", "RouteRestartingFailure")
+                .containsEntry("attempt", 3L)
+                .containsEntry("exhausted", true)
+                .containsKey("exception");
+    }
+
+    @Test
+    void testStepFailedEventAsJson() {
+        Exchange exchange = createExchangeWithBody("step-body");
+        exchange.setException(new IllegalStateException("step failed"));
+
+        StepFailedEvent event = new StepFailedEvent(exchange, "myStep");
+
+        Map<String, Object> json = event.asJSon();
+
+        assertThat(json)
+                .containsEntry("type", "StepFailed")
+                .containsEntry("stepId", "myStep")
+                .containsKey("exchangeId")
+                .containsKey("exception");
+    }
+
+    @Test
+    void testServiceStartupFailureEventAsJson() {
+        ServiceStartupFailureEvent event
+                = new ServiceStartupFailureEvent(context, "my-service", new 
RuntimeException("startup failed"));
+
+        Map<String, Object> json = event.asJSon();
+
+        assertThat(json)
+                .containsEntry("type", "ServiceStartupFailure")
+                .containsEntry("service", "my-service")
+                .containsEntry("contextName", context.getName())
+                .containsKey("exception");
+        assertThat(json.get("exception")).isInstanceOf(Map.class);
+    }
+
+    @Test
+    void testExchangeRedeliveryEventAsJson() {
+        Exchange exchange = createExchangeWithBody("payload");
+        exchange.setException(new RuntimeException("redelivery cause"));
+
+        ExchangeRedeliveryEvent event = new ExchangeRedeliveryEvent(exchange, 
2);
+
+        Map<String, Object> json = event.asJSon();
+
+        assertThat(json)
+                .containsEntry("type", "ExchangeRedelivery")
+                .containsEntry("attempt", 2)
+                .containsKey("exception");
+    }
+
+    @Test
+    void testDefaultCamelEventToJsonEscapesSpecialCharacters() {
+        CamelEvent event = new CamelEvent() {
+            @Override
+            public Type getType() {
+                return Type.Custom;
+            }
+
+            @Override
+            public Object getSource() {
+                return "source";
+            }
+
+            @Override
+            public long getTimestamp() {
+                return 42;
+            }
+
+            @Override
+            public void setTimestamp(long timestamp) {
+            }
+
+            @Override
+            public String toString() {
+                return "Route \"my-route\" failed\nline2";
+            }
+        };
+
+        String json = event.toJSon(0);
+
+        assertThat(json)
+                
.isEqualTo("{\"type\":\"Custom\",\"timestamp\":42,\"message\":\"Route 
\\\"my-route\\\" failed\\nline2\"}");
+        assertThat(json).doesNotContain("\"my-route\" failed");
+    }
+
+    @Test
+    void testAsJsonReturnsJsonObjectCompatibleMap() {
+        CamelEvent event = new CamelContextInitializedEvent(context);
+
+        Map<String, Object> json = event.asJSon();
+
+        assertThat(json).isInstanceOf(JsonObject.class);
+        assertThat(new JsonObject(json)).containsEntry("type", 
"CamelContextInitialized");
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:fail").routeId("jsonRoute").throwException(new 
IllegalArgumentException("boom"));
+                from("direct:sent").to("mock:result");
+            }
+        };
+    }
+}
diff --git 
a/core/camel-util/src/main/java/org/apache/camel/util/StringQuoteHelper.java 
b/core/camel-util/src/main/java/org/apache/camel/util/StringQuoteHelper.java
index 83ba2bfe3ae5..2544c8f608fa 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/StringQuoteHelper.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/StringQuoteHelper.java
@@ -34,6 +34,47 @@ public final class StringQuoteHelper {
         return quote(text, "\"");
     }
 
+    /**
+     * Returns the given text as a JSON string literal with proper escaping of 
quotes, backslashes and control
+     * characters.
+     */
+    public static String jsonQuote(String text) {
+        if (text == null) {
+            return "null";
+        }
+        StringBuilder sb = new StringBuilder(text.length() + 8);
+        sb.append('"');
+        for (int i = 0; i < text.length(); i++) {
+            char c = text.charAt(i);
+            switch (c) {
+                case '"':
+                    sb.append("\\\"");
+                    break;
+                case '\\':
+                    sb.append("\\\\");
+                    break;
+                case '\n':
+                    sb.append("\\n");
+                    break;
+                case '\r':
+                    sb.append("\\r");
+                    break;
+                case '\t':
+                    sb.append("\\t");
+                    break;
+                default:
+                    if (c < 0x20) {
+                        sb.append("\\u");
+                        sb.append(String.format("%04x", (int) c));
+                    } else {
+                        sb.append(c);
+                    }
+            }
+        }
+        sb.append('"');
+        return sb.toString();
+    }
+
     /**
      * Returns the text wrapped single quotes
      */
diff --git 
a/core/camel-util/src/test/java/org/apache/camel/util/StringQuoteHelperTest.java
 
b/core/camel-util/src/test/java/org/apache/camel/util/StringQuoteHelperTest.java
index b480035ebc78..883e2a9e479a 100644
--- 
a/core/camel-util/src/test/java/org/apache/camel/util/StringQuoteHelperTest.java
+++ 
b/core/camel-util/src/test/java/org/apache/camel/util/StringQuoteHelperTest.java
@@ -21,6 +21,13 @@ import org.junit.jupiter.api.Test;
 
 public class StringQuoteHelperTest {
 
+    @Test
+    public void testJsonQuoteEscapesSpecialCharacters() {
+        Assertions.assertEquals("\"He said \\\"hi\\\"\"", 
StringQuoteHelper.jsonQuote("He said \"hi\""));
+        Assertions.assertEquals("\"line1\\nline2\"", 
StringQuoteHelper.jsonQuote("line1\nline2"));
+        Assertions.assertEquals("\"a\\\\b\"", 
StringQuoteHelper.jsonQuote("a\\b"));
+    }
+
     @Test
     public void testSplitBeanParametersTrim() throws Exception {
         String[] arr = StringQuoteHelper.splitSafeQuote("String.class ${body}, 
String.class Mars", ',', true, true);
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 25d87474a783..05f4623af1b8 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -59,6 +59,20 @@ packages not inferred from the configured protocol.
 
 Do not use `*`, as it disables Avro's class-loading protection.
 
+=== CamelEvent JSON serialization
+
+`CamelEvent` now provides `asJSon()` and `toJSon(int indent)` with default 
implementations that
+return a minimal JSON map (`type`, optional `timestamp`, and `message`). 
Camel's built-in event
+classes override these methods to include structured metadata such as 
exchange, route, and
+exception details.
+
+Custom `CamelEvent` implementations continue to compile without changes. 
Override the new methods
+only if you need richer JSON output than the default 
`type`/`timestamp`/`message` map.
+
+The Event developer console now exposes the full structured JSON payload in 
the `details` field of
+each event entry, while keeping the existing flat `type`, `timestamp`, 
`exchangeId`, and
+`message` fields for backwards compatibility.
+
 === camel-dynamic-router
 
 The `dynamic-router-control` endpoint no longer takes the subscription 
`predicate`, or the
diff --git a/docs/user-manual/modules/ROOT/pages/event-notifier.adoc 
b/docs/user-manual/modules/ROOT/pages/event-notifier.adoc
index bc4057f6ae15..53bee56f20be 100644
--- a/docs/user-manual/modules/ROOT/pages/event-notifier.adoc
+++ b/docs/user-manual/modules/ROOT/pages/event-notifier.adoc
@@ -147,3 +147,20 @@ Timestamps can be enabled from the CamelContext as follows:
 ----
 context.getManagementStrategy().getEventFactory().setTimestampEnabled(true);
 ----
+
+== Event JSON serialization
+
+Since Camel 4.23, every `CamelEvent` can be serialized to structured JSON 
using `asJSon()` and `toJSon(int indent)`.
+This provides the same kind of structured metadata access as 
xref:backlog-tracer.adoc[Backlog Tracer] and the Error Registry backlog 
messages, but for live EventNotifier events.
+
+[source,java]
+----
+public void notify(CamelEvent event) throws Exception {
+    Map<String, Object> json = event.asJSon();
+    String pretty = event.toJSon(2);
+    log.info("Event JSON: {}", pretty);
+}
+----
+
+The JSON map includes common fields such as `type`, `timestamp`, `message`, 
and type-specific metadata (for example `exchangeId`, `routeId`, `endpointUri`, 
`exception`, `stepId`).
+The developer console event page exposes this structured JSON in the `details` 
field of each event entry.

Reply via email to