diff --git a/exceptiontrace/exceptiontrace-collector/pom.xml b/exceptiontrace/exceptiontrace-collector/pom.xml
new file mode 100644
index 0000000000000..53fadedc39dc5
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/pom.xml
@@ -0,0 +1,46 @@
+
+
+
+ pinpoint-exceptiontrace-module
+ com.navercorp.pinpoint
+ 2.5.1-SNAPSHOT
+
+ 4.0.0
+
+ pinpoint-exceptiontrace-collector
+
+
+ 11
+ ${env.JAVA_11_HOME}
+
+
+
+
+ com.navercorp.pinpoint
+ pinpoint-pinot-kafka
+
+
+ com.navercorp.pinpoint
+ pinpoint-exceptiontrace-common
+
+
+ com.navercorp.pinpoint
+ pinpoint-metric
+
+
+ com.navercorp.pinpoint
+ pinpoint-commons-server
+
+
+ org.springframework.kafka
+ spring-kafka
+ 2.9.4
+
+
+ com.navercorp.pinpoint
+ pinpoint-collector
+
+
+
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/ExceptionTraceCollectorConfig.java b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/ExceptionTraceCollectorConfig.java
new file mode 100644
index 0000000000000..478ca388aa5c1
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/ExceptionTraceCollectorConfig.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.collector;
+
+import com.navercorp.pinpoint.exceptiontrace.collector.config.ExceptionMetricKafkaConfiguration;
+import com.navercorp.pinpoint.pinot.config.PinotConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Profile;
+import org.springframework.context.annotation.PropertySource;
+
+/**
+ * @author intr3p1d
+ */
+@Profile("exception")
+@Configuration
+@Import({PinotConfiguration.class, ExceptionMetricKafkaConfiguration.class})
+@ComponentScan({"com.navercorp.pinpoint.exceptiontrace.collector.service", "com.navercorp.pinpoint.exceptiontrace.collector.dao"})
+@PropertySource({ExceptionTraceCollectorConfig.KAFKA_TOPIC_PROPERTIES})
+public class ExceptionTraceCollectorConfig {
+ public static final String KAFKA_TOPIC_PROPERTIES = "classpath:kafka-topic.properties";
+}
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/ExceptionTraceCollectorPropertySources.java b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/ExceptionTraceCollectorPropertySources.java
new file mode 100644
index 0000000000000..423bbdb300ddb
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/ExceptionTraceCollectorPropertySources.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.collector;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceCollectorPropertySources {
+}
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/config/ExceptionMetricKafkaConfiguration.java b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/config/ExceptionMetricKafkaConfiguration.java
new file mode 100644
index 0000000000000..2efdba2d37dfb
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/config/ExceptionMetricKafkaConfiguration.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.collector.config;
+
+import com.navercorp.pinpoint.exceptiontrace.collector.model.SpanEventExceptionVo;
+import com.navercorp.pinpoint.pinot.kafka.KafkaConfiguration;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.core.ProducerFactory;
+
+/**
+ * @author intr3p1d
+ */
+@Configuration
+@Import({KafkaConfiguration.class})
+public class ExceptionMetricKafkaConfiguration {
+
+ @Bean
+ public KafkaTemplate kafkaSpanEventExceptionTemplate(@Qualifier("kafkaProducerFactory") ProducerFactory producerFactory) {
+ return new KafkaTemplate<>(producerFactory);
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/dao/ExceptionTraceDao.java b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/dao/ExceptionTraceDao.java
new file mode 100644
index 0000000000000..8a21bdae21275
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/dao/ExceptionTraceDao.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.collector.dao;
+
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public interface ExceptionTraceDao {
+ void insert(List spanEventException);
+}
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/dao/PinotExceptionTraceDao.java b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/dao/PinotExceptionTraceDao.java
new file mode 100644
index 0000000000000..0f23adae6adb8
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/dao/PinotExceptionTraceDao.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.collector.dao;
+
+import com.navercorp.pinpoint.exceptiontrace.collector.model.SpanEventExceptionVo;
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import com.navercorp.pinpoint.exceptiontrace.common.util.StringPrecondition;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author intr3p1d
+ */
+@Repository
+public class PinotExceptionTraceDao implements ExceptionTraceDao {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ private final KafkaTemplate kafkaSpanEventExceptionTemplate;
+
+ private final String topic;
+
+ public PinotExceptionTraceDao(@Qualifier("kafkaSpanEventExceptionTemplate") KafkaTemplate kafkaSpanEventExceptionTemplate,
+ @Value("${kafka.exception.topic}") String topic) {
+ this.kafkaSpanEventExceptionTemplate = Objects.requireNonNull(kafkaSpanEventExceptionTemplate, "kafkaSpanEventExceptionTemplate");
+ this.topic = StringPrecondition.requireHasLength(topic, "topic");
+ }
+
+ @Override
+ public void insert(List spanEventExceptions) {
+ Objects.requireNonNull(spanEventExceptions);
+ logger.info("Pinot data insert: {}", spanEventExceptions.toString());
+
+ for (SpanEventException spanEventException : spanEventExceptions) {
+ this.kafkaSpanEventExceptionTemplate.send(
+ topic, SpanEventExceptionVo.valueOf(spanEventException)
+ );
+ }
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/model/SpanEventExceptionVo.java b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/model/SpanEventExceptionVo.java
new file mode 100644
index 0000000000000..2b4bc48714952
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/model/SpanEventExceptionVo.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.collector.model;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import com.navercorp.pinpoint.exceptiontrace.common.model.StackTraceElementWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public class SpanEventExceptionVo {
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private final long timestamp;
+
+ private final String transactionId;
+ private final long spanId;
+ private final long exceptionId;
+
+ private final String applicationServiceType;
+ private final String applicationName;
+ private final String agentId;
+
+ private final String errorClassName;
+ private final String errorMessage;
+ private final int exceptionDepth;
+ private final List stackTrace;
+ private final String stackTraceHash;
+
+
+ public SpanEventExceptionVo(long timestamp, String transactionId, long spanId, long exceptionId, String applicationServiceType, String applicationName, String agentId, String errorClassName, String errorMessage, int exceptionDepth, List stackTrace, String stackTraceHash) {
+ this.timestamp = timestamp;
+ this.transactionId = transactionId;
+ this.spanId = spanId;
+ this.exceptionId = exceptionId;
+ this.applicationServiceType = applicationServiceType;
+ this.applicationName = applicationName;
+ this.agentId = agentId;
+ this.errorClassName = errorClassName;
+ this.errorMessage = errorMessage;
+ this.exceptionDepth = exceptionDepth;
+ this.stackTrace = stackTrace;
+ this.stackTraceHash = stackTraceHash;
+ }
+
+
+ public static SpanEventExceptionVo valueOf(SpanEventException spanEventException) {
+ return new SpanEventExceptionVo(
+ spanEventException.getTimestamp(),
+ spanEventException.getTransactionId(),
+ spanEventException.getSpanId(),
+ spanEventException.getExceptionId(),
+ spanEventException.getApplicationServiceType(),
+ spanEventException.getApplicationName(),
+ spanEventException.getAgentId(),
+ spanEventException.getErrorClassName(),
+ spanEventException.getErrorMessage(),
+ spanEventException.getExceptionDepth(),
+ toJsonString(spanEventException.getStackTrace()),
+ spanEventException.getStackTraceHash()
+ );
+ }
+
+
+ private static List toJsonString(List stackTrace) {
+ List strings = new ArrayList<>();
+ stackTrace.forEach(
+ (StackTraceElementWrapper s) -> {
+ try {
+ strings.add(OBJECT_MAPPER.writeValueAsString(s));
+ } catch (JsonProcessingException ignored) {
+ // do nothing
+ }
+ }
+ );
+ return strings;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getTransactionId() {
+ return transactionId;
+ }
+
+ public long getSpanId() {
+ return spanId;
+ }
+
+ public long getExceptionId() {
+ return exceptionId;
+ }
+
+ public String getApplicationServiceType() {
+ return applicationServiceType;
+ }
+
+ public String getApplicationName() {
+ return applicationName;
+ }
+
+ public String getAgentId() {
+ return agentId;
+ }
+
+ public String getErrorClassName() {
+ return errorClassName;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public int getExceptionDepth() {
+ return exceptionDepth;
+ }
+
+ public List getStackTrace() {
+ return stackTrace;
+ }
+
+ public String getStackTraceHash() {
+ return stackTraceHash;
+ }
+
+ @Override
+ public String toString() {
+ return "SpanEventExceptionVo{" +
+ "timestamp=" + timestamp +
+ ", transactionId='" + transactionId + '\'' +
+ ", spanId=" + spanId +
+ ", exceptionId=" + exceptionId +
+ ", applicationServiceType='" + applicationServiceType + '\'' +
+ ", applicationName='" + applicationName + '\'' +
+ ", agentId='" + agentId + '\'' +
+ ", errorClassName='" + errorClassName + '\'' +
+ ", errorMessage='" + errorMessage + '\'' +
+ ", exceptionDepth=" + exceptionDepth +
+ ", stackTrace=" + stackTrace +
+ ", stackTraceHash='" + stackTraceHash + '\'' +
+ '}';
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/service/PinotExceptionTraceService.java b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/service/PinotExceptionTraceService.java
new file mode 100644
index 0000000000000..c4ee8bd8b5bbd
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/service/PinotExceptionTraceService.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.collector.service;
+
+import com.navercorp.pinpoint.collector.service.ExceptionTraceService;
+import com.navercorp.pinpoint.common.profiler.util.TransactionId;
+import com.navercorp.pinpoint.common.profiler.util.TransactionIdUtils;
+import com.navercorp.pinpoint.common.server.bo.exception.ExceptionWrapperBo;
+import com.navercorp.pinpoint.common.server.bo.exception.SpanEventExceptionBo;
+import com.navercorp.pinpoint.common.trace.ServiceType;
+import com.navercorp.pinpoint.exceptiontrace.collector.dao.ExceptionTraceDao;
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author intr3p1d
+ */
+@Service
+@Profile("metric")
+public class PinotExceptionTraceService implements ExceptionTraceService {
+ private final ExceptionTraceDao exceptionTraceDao;
+
+ public PinotExceptionTraceService(ExceptionTraceDao exceptionTraceDao) {
+ this.exceptionTraceDao = Objects.requireNonNull(exceptionTraceDao, "exceptionTraceDao");
+ }
+
+ @Override
+ public void save(List spanEventExceptionBoList, ServiceType applicationServiceType, String applicationId, String agentId, TransactionId transactionId, long spanId) {
+ List spanEventExceptions = new ArrayList<>();
+ for (SpanEventExceptionBo spanEventExceptionBo : spanEventExceptionBoList) {
+ spanEventExceptions.addAll(toSpanEventExceptions(spanEventExceptionBo, applicationServiceType, applicationId, agentId, transactionId, spanId));
+ }
+ exceptionTraceDao.insert(spanEventExceptions);
+ }
+
+ private static List toSpanEventExceptions(
+ SpanEventExceptionBo spanEventExceptionBo,
+ ServiceType applicationServiceType, String applicationId, String agentId,
+ TransactionId transactionId, long spanId) {
+ List spanEventExceptions = new ArrayList<>();
+ List exceptions = spanEventExceptionBo.getExceptionWrappers();
+ for (int i = 0; i < exceptions.size(); i++) {
+ spanEventExceptions.add(
+ toSpanEventException(
+ exceptions.get(i), i,
+ applicationServiceType, applicationId, agentId,
+ transactionId, spanId,
+ spanEventExceptionBo.getExceptionId(),
+ spanEventExceptionBo.getStartTime()
+ )
+ );
+ }
+ return spanEventExceptions;
+ }
+
+ private static SpanEventException toSpanEventException(
+ ExceptionWrapperBo exceptionWrapperBo,
+ int exceptionDepth,
+ ServiceType applicationServiceType, String applicationId, String agentId,
+ TransactionId transactionId, long spanId,
+ long exceptionId,
+ long startTime) {
+ return SpanEventException.valueOf(
+ startTime,
+ transactionIdToString(transactionId),
+ spanId,
+ exceptionId,
+ applicationServiceType.getName(),
+ applicationId,
+ agentId,
+ exceptionWrapperBo.getExceptionClassName(),
+ exceptionWrapperBo.getExceptionMessage(),
+ exceptionDepth,
+ exceptionWrapperBo.getStackTraceElements()
+ );
+ }
+
+ private static String transactionIdToString(TransactionId transactionId) {
+ return TransactionIdUtils.formatString(transactionId);
+ }
+
+}
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/resources/kafka-topic.properties b/exceptiontrace/exceptiontrace-collector/src/main/resources/kafka-topic.properties
new file mode 100644
index 0000000000000..e99d3db765d76
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/resources/kafka-topic.properties
@@ -0,0 +1 @@
+kafka.exception.topic=exception-trace
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-common/pom.xml b/exceptiontrace/exceptiontrace-common/pom.xml
new file mode 100644
index 0000000000000..19e23083f2ea7
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/pom.xml
@@ -0,0 +1,40 @@
+
+
+
+ pinpoint-exceptiontrace-module
+ com.navercorp.pinpoint
+ 2.5.1-SNAPSHOT
+
+ 4.0.0
+
+ pinpoint-exceptiontrace-common
+
+
+ 11
+ ${env.JAVA_11_HOME}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ compile
+
+
+ org.springframework
+ spring-core
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+ com.navercorp.pinpoint
+ pinpoint-commons-server
+
+
+
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/model/SpanEventException.java b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/model/SpanEventException.java
new file mode 100644
index 0000000000000..080ff2f84c6f9
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/model/SpanEventException.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.common.model;
+
+import com.navercorp.pinpoint.common.server.bo.exception.StackTraceElementWrapperBo;
+import com.navercorp.pinpoint.exceptiontrace.common.util.HashUtils;
+import com.navercorp.pinpoint.exceptiontrace.common.util.StringPrecondition;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author intr3p1d
+ */
+public class SpanEventException {
+
+ private final long timestamp;
+
+ private final String transactionId;
+ private final long spanId;
+ private final long exceptionId;
+
+ private final String applicationServiceType;
+ private final String applicationName;
+ private final String agentId;
+
+ private final String errorClassName;
+ private final String errorMessage;
+ private final int exceptionDepth;
+ private final List stackTrace;
+
+ private final String stackTraceHash;
+
+ public SpanEventException(
+ long timestamp,
+ String transactionId,
+ long spanId,
+ long exceptionId,
+ String applicationServiceType,
+ String applicationName,
+ String agentId,
+ String errorClassName,
+ String errorMessage,
+ int exceptionDepth,
+ List stackTrace,
+ String stackTraceHash
+ ) {
+ this.timestamp = timestamp;
+ this.transactionId = StringPrecondition.requireHasLength(transactionId, "transactionId");
+ this.spanId = spanId;
+ this.exceptionId = exceptionId;
+ this.applicationServiceType = StringPrecondition.requireHasLength(applicationServiceType, "applicationServiceType");
+ this.applicationName = StringPrecondition.requireHasLength(applicationName, "applicationName");
+ this.agentId = StringPrecondition.requireHasLength(agentId, "agentId");
+ this.errorClassName = StringPrecondition.requireHasLength(errorClassName, "errorClassName");
+ this.errorMessage = StringPrecondition.requireHasLength(errorMessage, "errorMessage");
+ this.exceptionDepth = exceptionDepth;
+ this.stackTrace = stackTrace;
+ this.stackTraceHash = stackTraceHash;
+ }
+
+ public SpanEventException(
+ long timestamp,
+ String transactionId,
+ long spanId,
+ long exceptionId,
+ String applicationServiceType,
+ String applicationName,
+ String agentId,
+ String errorClassName,
+ String errorMessage,
+ int exceptionDepth,
+ String stackTraceHash
+ ) {
+ this.timestamp = timestamp;
+ this.transactionId = StringPrecondition.requireHasLength(transactionId, "transactionId");
+ this.spanId = spanId;
+ this.exceptionId = exceptionId;
+ this.applicationServiceType = StringPrecondition.requireHasLength(applicationServiceType, "applicationServiceType");
+ this.applicationName = StringPrecondition.requireHasLength(applicationName, "applicationName");
+ this.agentId = StringPrecondition.requireHasLength(agentId, "agentId");
+ this.errorClassName = StringPrecondition.requireHasLength(errorClassName, "errorClassName");
+ this.errorMessage = StringPrecondition.requireHasLength(errorMessage, "errorMessage");
+ this.exceptionDepth = exceptionDepth;
+ this.stackTrace = Collections.emptyList();
+ this.stackTraceHash = stackTraceHash;
+ }
+
+ public static SpanEventException valueOf(long timestamp, String transactionId, long spanId, long exceptionId,
+ String applicationServiceType, String applicationName, String agentId,
+ String errorClassName, String errorMessage, int exceptionDepth,
+ List stackTraceElementWrapperBos) {
+ List wrappers = toStackTrace(stackTraceElementWrapperBos);
+
+ return new SpanEventException(
+ timestamp,
+ transactionId,
+ spanId,
+ exceptionId,
+ applicationServiceType,
+ applicationName,
+ agentId,
+ errorClassName,
+ errorMessage,
+ exceptionDepth,
+ wrappers,
+ toStackTraceHash(wrappers)
+ );
+ }
+
+ public static List toStackTrace(List stackTraceElementWrapperBos) {
+ return stackTraceElementWrapperBos.stream().map(
+ (StackTraceElementWrapperBo s) -> new StackTraceElementWrapper(s.getClassName(), s.getFileName(), s.getLineNumber(), s.getMethodName())
+ ).collect(Collectors.toList());
+ }
+
+ public static String toStackTraceHash(List stackTraceElementWrappers) {
+ return HashUtils.wrappersToHashString(stackTraceElementWrappers);
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getTransactionId() {
+ return transactionId;
+ }
+
+ public long getSpanId() {
+ return spanId;
+ }
+
+ public long getExceptionId() {
+ return exceptionId;
+ }
+
+ public String getApplicationServiceType() {
+ return applicationServiceType;
+ }
+
+ public String getApplicationName() {
+ return applicationName;
+ }
+
+ public String getAgentId() {
+ return agentId;
+ }
+
+ public String getErrorClassName() {
+ return errorClassName;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public int getExceptionDepth() {
+ return exceptionDepth;
+ }
+
+ public List getStackTrace() {
+ return stackTrace;
+ }
+
+ public String getStackTraceHash() {
+ return stackTraceHash;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SpanEventException)) return false;
+
+ SpanEventException that = (SpanEventException) o;
+
+ if (timestamp != that.timestamp) return false;
+ if (spanId != that.spanId) return false;
+ if (!transactionId.equals(that.transactionId)) return false;
+ if (!applicationServiceType.equals(that.applicationServiceType)) return false;
+ if (!applicationName.equals(that.applicationName)) return false;
+ if (!agentId.equals(that.agentId)) return false;
+ if (!errorClassName.equals(that.errorClassName)) return false;
+ if (!errorMessage.equals(that.errorMessage)) return false;
+ return stackTrace.equals(that.stackTrace);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(applicationName, agentId, errorClassName, errorMessage, stackTrace);
+ }
+
+ @Override
+ public String toString() {
+ return "SpanEventException{" +
+ "timestamp=" + timestamp +
+ ", transactionId='" + transactionId + '\'' +
+ ", spanId=" + spanId +
+ ", applicationServiceType='" + applicationServiceType + '\'' +
+ ", applicationName='" + applicationName + '\'' +
+ ", agentId='" + agentId + '\'' +
+ ", errorClassName='" + errorClassName + '\'' +
+ ", errorMessage='" + errorMessage + '\'' +
+ ", exceptionDepth=" + exceptionDepth +
+ ", stackTrace=" + stackTrace +
+ ", stackTraceHash='" + stackTraceHash + '\'' +
+ '}';
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/model/StackTraceElementWrapper.java b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/model/StackTraceElementWrapper.java
new file mode 100644
index 0000000000000..17bd1a6375b9f
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/model/StackTraceElementWrapper.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.common.model;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Objects;
+
+/**
+ * @author intr3p1d
+ */
+@JsonAutoDetect
+public class StackTraceElementWrapper {
+ private String className;
+ private String fileName;
+ private int lineNumber;
+ private String methodName;
+
+ public StackTraceElementWrapper() {
+ }
+
+ public StackTraceElementWrapper(@JsonProperty("className") String className,
+ @JsonProperty("fileName") String fileName,
+ @JsonProperty("lineNumber") int lineNumber,
+ @JsonProperty("methodName") String methodName) {
+ this.className = Objects.requireNonNull(className, "className");
+ this.fileName = Objects.requireNonNull(fileName, "fileName");
+ this.lineNumber = lineNumber;
+ this.methodName = Objects.requireNonNull(methodName, "methodName");
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public void setClassName(String className) {
+ this.className = className;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public int getLineNumber() {
+ return lineNumber;
+ }
+
+ public void setLineNumber(int lineNumber) {
+ this.lineNumber = lineNumber;
+ }
+
+ public String getMethodName() {
+ return methodName;
+ }
+
+ public void setMethodName(String methodName) {
+ this.methodName = methodName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof StackTraceElementWrapper)) return false;
+
+ StackTraceElementWrapper that = (StackTraceElementWrapper) o;
+
+ if (lineNumber != that.lineNumber) return false;
+ if (!className.equals(that.className)) return false;
+ if (!fileName.equals(that.fileName)) return false;
+ return methodName.equals(that.methodName);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = className.hashCode();
+ result = 31 * result + fileName.hashCode();
+ result = 31 * result + lineNumber;
+ result = 31 * result + methodName.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "StackTraceElementWrapper{" +
+ "className='" + className + '\'' +
+ ", fileName='" + fileName + '\'' +
+ ", lineNumber=" + lineNumber +
+ ", methodName='" + methodName + '\'' +
+ '}';
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/util/HashUtils.java b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/util/HashUtils.java
new file mode 100644
index 0000000000000..48589fe418e4f
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/util/HashUtils.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.common.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.hadoop.hbase.shaded.org.apache.commons.codec.binary.Hex;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public class HashUtils {
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ public static String wrappersToHashString(List objects) {
+ String s = objectsToJsonString(objects);
+ return toHexString(
+ toHashBytes(s)
+ );
+ }
+
+ private static String objectsToJsonString(List objects) {
+ try {
+ return OBJECT_MAPPER.writeValueAsString(objects);
+ } catch (JsonProcessingException ignored) {
+ // do nothing
+ }
+ return "";
+ }
+
+ private static byte[] toHashBytes(String string) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ return digest.digest(
+ string.getBytes(StandardCharsets.UTF_8)
+ );
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ }
+
+ private static String toHexString(byte[] bytes) {
+ return Hex.encodeHexString(bytes);
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/util/StringPrecondition.java b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/util/StringPrecondition.java
new file mode 100644
index 0000000000000..3eb64d35fd791
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/util/StringPrecondition.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.common.util;
+
+import org.springframework.util.StringUtils;
+
+public final class StringPrecondition {
+ private StringPrecondition() {
+ }
+
+ public static String requireHasLength(String str, String variableName) {
+ if (StringUtils.hasLength(str)) {
+ return str;
+ }
+ throw new IllegalArgumentException(variableName + " must not be empty");
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-common/src/main/pinot/pinot-exceptionTrace-schema.json b/exceptiontrace/exceptiontrace-common/src/main/pinot/pinot-exceptionTrace-schema.json
new file mode 100644
index 0000000000000..bb4716a2f9ae8
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/pinot/pinot-exceptionTrace-schema.json
@@ -0,0 +1,58 @@
+{
+ "schemaName": "exceptionTrace",
+ "dimensionFieldSpecs": [
+ {
+ "name": "transactionId",
+ "dataType": "STRING"
+ },
+ {
+ "name": "spanId",
+ "dataType": "LONG"
+ },
+ {
+ "name": "exceptionId",
+ "dataType": "LONG"
+ },
+ {
+ "name": "applicationServiceType",
+ "dataType": "STRING"
+ },
+ {
+ "name": "applicationName",
+ "dataType": "STRING"
+ },
+ {
+ "name": "agentId",
+ "dataType": "STRING"
+ },
+ {
+ "name": "errorClassName",
+ "dataType": "STRING"
+ },
+ {
+ "name": "errorMessage",
+ "dataType": "STRING"
+ },
+ {
+ "name": "exceptionDepth",
+ "dataType": "INT"
+ },
+ {
+ "name": "stackTrace",
+ "dataType": "STRING",
+ "singleValueField": false
+ },
+ {
+ "name": "stackTraceHash",
+ "dataType": "BYTES"
+ }
+ ],
+ "dateTimeFieldSpecs": [
+ {
+ "name": "timestamp",
+ "dataType": "TIMESTAMP",
+ "format": "1:MILLISECONDS:EPOCH",
+ "granularity": "1:MILLISECONDS"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-common/src/main/pinot/pinot-exceptionTrace-table.json b/exceptiontrace/exceptiontrace-common/src/main/pinot/pinot-exceptionTrace-table.json
new file mode 100644
index 0000000000000..77d203ccc8280
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/pinot/pinot-exceptionTrace-table.json
@@ -0,0 +1,55 @@
+{
+ "REALTIME": {
+ "tableName": "exceptionTrace_REALTIME",
+ "tableType": "REALTIME",
+ "segmentsConfig": {
+ "schemaName": "exceptionTrace",
+ "replication": "1",
+ "replicasPerPartition": "1",
+ "timeColumnName": "timestamp",
+ "minimizeDataMovement": false
+ },
+ "tenants": {
+ "broker": "DefaultTenant",
+ "server": "DefaultTenant",
+ "tagOverrideConfig": {}
+ },
+ "tableIndexConfig": {
+ "invertedIndexColumns": [],
+ "noDictionaryColumns": [],
+ "streamConfigs": {
+ "streamType": "kafka",
+ "stream.kafka.topic.name": "exception-trace",
+ "stream.kafka.broker.list": "localhost:19092",
+ "stream.kafka.consumer.type": "lowlevel",
+ "stream.kafka.consumer.prop.auto.offset.reset": "smallest",
+ "stream.kafka.consumer.factory.class.name": "org.apache.pinot.plugin.stream.kafka20.KafkaConsumerFactory",
+ "stream.kafka.decoder.class.name": "org.apache.pinot.plugin.stream.kafka.KafkaJSONMessageDecoder",
+ "realtime.segment.flush.threshold.rows": "0",
+ "realtime.segment.flush.threshold.time": "24h",
+ "realtime.segment.flush.segment.size": "100M"
+ },
+ "rangeIndexColumns": [],
+ "rangeIndexVersion": 2,
+ "sortedColumn": [],
+ "bloomFilterColumns": [],
+ "loadMode": "MMAP",
+ "onHeapDictionaryColumns": [],
+ "enableDefaultStarTree": false,
+ "aggregateMetrics": false,
+ "nullHandlingEnabled": false,
+ "autoGeneratedInvertedIndex": false,
+ "varLengthDictionaryColumns": [],
+ "enableDynamicStarTreeCreation": false,
+ "optimizeDictionaryForMetrics": false,
+ "noDictionarySizeRatioThreshold": 0,
+ "createInvertedIndexDuringSegmentGeneration": false
+ },
+ "metadata": {},
+ "quota": {},
+ "routing": {},
+ "query": {},
+ "ingestionConfig": {},
+ "isDimTable": false
+ }
+}
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-web/pom.xml b/exceptiontrace/exceptiontrace-web/pom.xml
new file mode 100644
index 0000000000000..ccc514bcbd726
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/pom.xml
@@ -0,0 +1,36 @@
+
+
+
+ pinpoint-exceptiontrace-module
+ com.navercorp.pinpoint
+ 2.5.1-SNAPSHOT
+
+ 4.0.0
+
+ pinpoint-exceptiontrace-web
+
+
+
+ 11
+ ${env.JAVA_11_HOME}
+
+
+
+
+ com.navercorp.pinpoint
+ pinpoint-commons
+
+
+ com.navercorp.pinpoint
+ pinpoint-metric
+
+
+ com.navercorp.pinpoint
+ pinpoint-exceptiontrace-common
+ 2.5.1-SNAPSHOT
+
+
+
+
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/ExceptionTraceWebConfig.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/ExceptionTraceWebConfig.java
new file mode 100644
index 0000000000000..0087336abf766
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/ExceptionTraceWebConfig.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web;
+
+import com.navercorp.pinpoint.exceptiontrace.web.config.ExceptionTracePinotDaoConfiguration;
+import com.navercorp.pinpoint.pinot.config.PinotConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Profile;
+
+/**
+ * @author intr3p1d
+ */
+@Configuration
+@ComponentScan(basePackages = "com.navercorp.pinpoint.exceptiontrace.web")
+@Import({ExceptionTraceWebPropertySources.class, ExceptionTracePinotDaoConfiguration.class, PinotConfiguration.class})
+@Profile("exception")
+public class ExceptionTraceWebConfig {
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/ExceptionTraceWebPropertySources.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/ExceptionTraceWebPropertySources.java
new file mode 100644
index 0000000000000..40d98b7fc9715
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/ExceptionTraceWebPropertySources.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web;
+
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.annotation.PropertySources;
+
+/**
+ * @author intr3p1d
+ */
+@PropertySources({
+ @PropertySource(name = "ExceptionTracePropertySources", value = {ExceptionTraceWebPropertySources.EXCEPTION_TRACE}),
+})
+public class ExceptionTraceWebPropertySources {
+ public static final String EXCEPTION_TRACE = "classpath:profiles/${pinpoint.profiles.active:release}/pinpoint-web-exceptiontrace.properties";
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/config/ExceptionTracePinotDaoConfiguration.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/config/ExceptionTracePinotDaoConfiguration.java
new file mode 100644
index 0000000000000..fd9d9589d321c
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/config/ExceptionTracePinotDaoConfiguration.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.config;
+
+import com.navercorp.pinpoint.metric.collector.config.MyBatisRegistryHandler;
+import com.navercorp.pinpoint.pinot.mybatis.MyBatisConfiguration;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.transaction.managed.ManagedTransactionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.SqlSessionTemplate;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.io.Resource;
+
+import javax.sql.DataSource;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTracePinotDaoConfiguration {
+
+ @Bean
+ public SqlSessionFactory exceptionTracePinotSessionFactory(
+ @Qualifier("pinotDataSource") DataSource dataSource,
+ @Value("classpath:mapper/exceptiontrace/*Mapper.xml") Resource[] mappers
+ ) throws Exception {
+ SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
+
+ sessionFactoryBean.setDataSource(dataSource);
+
+ sessionFactoryBean.setConfiguration(newConfiguration());
+ sessionFactoryBean.setMapperLocations(mappers);
+ sessionFactoryBean.setFailFast(true);
+ sessionFactoryBean.setTransactionFactory(transactionFactory());
+
+ return sessionFactoryBean.getObject();
+ }
+
+ private ManagedTransactionFactory transactionFactory() {
+ return new ManagedTransactionFactory();
+ }
+
+ private Configuration newConfiguration() {
+ Configuration config = MyBatisConfiguration.defaultConfiguration();
+
+ MyBatisRegistryHandler registryHandler = registryHandler();
+ registryHandler.registerTypeAlias(config.getTypeAliasRegistry());
+ registryHandler.registerTypeHandler(config.getTypeHandlerRegistry());
+ return config;
+ }
+
+ private MyBatisRegistryHandler registryHandler() {
+ return new ExceptionTraceRegistryHandler();
+ }
+
+ @Bean
+ public SqlSessionTemplate exceptionTracePinotSessionTemplate(
+ @Qualifier("exceptionTracePinotSessionFactory") SqlSessionFactory sessionFactory
+ ) {
+ return new SqlSessionTemplate(sessionFactory);
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/config/ExceptionTraceRegistryHandler.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/config/ExceptionTraceRegistryHandler.java
new file mode 100644
index 0000000000000..c7d06b9e3bbde
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/config/ExceptionTraceRegistryHandler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.config;
+
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import com.navercorp.pinpoint.exceptiontrace.common.model.StackTraceElementWrapper;
+import com.navercorp.pinpoint.exceptiontrace.web.mapper.StackTraceTypeHandler;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.util.ExceptionTraceQueryParameter;
+import com.navercorp.pinpoint.metric.collector.config.MyBatisRegistryHandler;
+import org.apache.ibatis.type.TypeAliasRegistry;
+import org.apache.ibatis.type.TypeHandlerRegistry;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceRegistryHandler implements MyBatisRegistryHandler {
+ @Override
+ public void registerTypeAlias(TypeAliasRegistry typeAliasRegistry) {
+ typeAliasRegistry.registerAlias("SpanEventException", SpanEventException.class);
+ typeAliasRegistry.registerAlias("StackTraceElementWrapper", StackTraceElementWrapper.class);
+ typeAliasRegistry.registerAlias("ExceptionTraceSummary", ExceptionTraceSummary.class);
+ typeAliasRegistry.registerAlias("StackTraceTypeHandler", StackTraceTypeHandler.class);
+ typeAliasRegistry.registerAlias("ExceptionTraceQueryParameter", ExceptionTraceQueryParameter.class);
+ }
+
+ @Override
+ public void registerTypeHandler(TypeHandlerRegistry typeHandlerRegistry) {
+
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/controller/ExceptionTraceController.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/controller/ExceptionTraceController.java
new file mode 100644
index 0000000000000..cd8b5c9ff9738
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/controller/ExceptionTraceController.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.controller;
+
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.service.ExceptionTraceService;
+import com.navercorp.pinpoint.exceptiontrace.web.util.ExceptionTraceQueryParameter;
+import com.navercorp.pinpoint.metric.web.util.Range;
+import com.navercorp.pinpoint.metric.web.util.TimePrecision;
+import com.navercorp.pinpoint.metric.web.util.TimeWindow;
+import com.navercorp.pinpoint.metric.web.util.TimeWindowSampler;
+import com.navercorp.pinpoint.metric.web.util.TimeWindowSlotCentricSampler;
+import com.navercorp.pinpoint.exceptiontrace.web.view.ExceptionTraceView;
+import com.navercorp.pinpoint.pinot.tenant.TenantProvider;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author intr3p1d
+ */
+@RestController
+@RequestMapping(value = "/exceptionTrace")
+public class ExceptionTraceController {
+
+ private static final TimePrecision DETAILED_TIME_PRECISION = TimePrecision.newTimePrecision(TimeUnit.MILLISECONDS, 1);
+
+ private final ExceptionTraceService exceptionTraceService;
+
+ private final Logger logger = LogManager.getLogger(this.getClass());
+ private final TimeWindowSampler DEFAULT_TIME_WINDOW_SAMPLER = new TimeWindowSlotCentricSampler(30000L, 200);
+ private final TenantProvider tenantProvider;
+
+ public ExceptionTraceController(ExceptionTraceService exceptionTraceService, TenantProvider tenantProvider) {
+ this.exceptionTraceService = Objects.requireNonNull(exceptionTraceService, "exceptionTraceService");
+ this.tenantProvider = Objects.requireNonNull(tenantProvider, "tenantProvider");
+ }
+
+ @GetMapping("/transactionInfo")
+ public List getSpanEventExceptionFromTransactionId(
+ @RequestParam("applicationName") String applicationName,
+ @RequestParam("agentId") String agentId,
+ @RequestParam("traceId") String traceId,
+ @RequestParam("spanId") long spanId,
+ @RequestParam("exceptionId") long exceptionId
+ ) {
+ ExceptionTraceQueryParameter queryParameter = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setTransactionId(traceId)
+ .setSpanId(spanId)
+ .setExceptionId(exceptionId)
+ .setTimePrecision(DETAILED_TIME_PRECISION)
+ .build();
+ return exceptionTraceService.getTransactionExceptions(
+ queryParameter
+ );
+ }
+
+ @GetMapping("/errorList")
+ public List getListOfSpanEventExceptionByGivenRange(
+ @RequestParam("applicationName") String applicationName,
+ @RequestParam(value = "agentId", required = false) String agentId,
+ @RequestParam("from") long from,
+ @RequestParam("to") long to
+ ) {
+ ExceptionTraceQueryParameter queryParameter = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setRange(Range.newRange(from, to))
+ .setTimePrecision(DETAILED_TIME_PRECISION)
+ .build();
+ return exceptionTraceService.getExceptionsInRange(
+ queryParameter
+ );
+ }
+
+ @GetMapping("/errorList/similar")
+ public List getListOfSpanEventExceptionByGivenRange(
+ @RequestParam("applicationName") String applicationName,
+ @RequestParam(value = "agentId", required = false) String agentId,
+ @RequestParam("from") long from,
+ @RequestParam("to") long to,
+
+ @RequestParam("traceId") String traceId,
+ @RequestParam("spanId") long spanId,
+ @RequestParam("exceptionId") long exceptionId,
+ @RequestParam("exceptionDepth") int exceptionDepth
+ ) {
+ ExceptionTraceQueryParameter targetQuery = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setTransactionId(traceId)
+ .setSpanId(spanId)
+ .setExceptionId(exceptionId)
+ .setExceptionDepth(exceptionDepth).build();
+
+ ExceptionTraceQueryParameter.Builder queryBuilder = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setRange(Range.newRange(from, to))
+ .setTimePrecision(DETAILED_TIME_PRECISION);
+
+ return exceptionTraceService.getSimilarExceptions(
+ targetQuery, queryBuilder
+ );
+ }
+
+ @GetMapping("/chart")
+ public ExceptionTraceView getCollectedSpanEventExceptionByGivenRange(
+ @RequestParam("applicationName") String applicationName,
+ @RequestParam(value = "agentId", required = false) String agentId,
+ @RequestParam("from") long from,
+ @RequestParam("to") long to
+ ) {
+
+ TimeWindow timeWindow = new TimeWindow(Range.newRange(from, to), DEFAULT_TIME_WINDOW_SAMPLER);
+ ExceptionTraceQueryParameter queryParameter = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setRange(Range.newRange(from, to))
+ .setTimePrecision(TimePrecision.newTimePrecision(TimeUnit.MILLISECONDS, (int) timeWindow.getWindowSlotSize()))
+ .build();
+ List exceptionTraceSummaries = exceptionTraceService.getSummaryInRange(
+ queryParameter
+ );
+ return ExceptionTraceView.newViewFromSummaries("", timeWindow, exceptionTraceSummaries);
+ }
+
+ @GetMapping("/chart/similar")
+ public ExceptionTraceView getCollectedSpanEventExceptionByGivenRange(
+ @RequestParam("applicationName") String applicationName,
+ @RequestParam(value = "agentId", required = false) String agentId,
+ @RequestParam("from") long from,
+ @RequestParam("to") long to,
+
+ @RequestParam("traceId") String traceId,
+ @RequestParam("spanId") long spanId,
+ @RequestParam("exceptionId") long exceptionId,
+ @RequestParam("exceptionDepth") int exceptionDepth
+ ) {
+ TimeWindow timeWindow = new TimeWindow(Range.newRange(from, to), DEFAULT_TIME_WINDOW_SAMPLER);
+ ExceptionTraceQueryParameter targetQuery = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setTransactionId(traceId)
+ .setSpanId(spanId)
+ .setExceptionId(exceptionId)
+ .setExceptionDepth(exceptionDepth).build();
+
+ ExceptionTraceQueryParameter.Builder queryBuilder = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setRange(Range.newRange(from, to))
+ .setTimePrecision(TimePrecision.newTimePrecision(TimeUnit.MILLISECONDS, (int) timeWindow.getWindowSlotSize()));
+
+ List exceptionTraceSummaries = exceptionTraceService.getSummaryOfSimilarExceptions(
+ targetQuery, queryBuilder
+ );
+ return ExceptionTraceView.newViewFromSummaries("", timeWindow, exceptionTraceSummaries);
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/dao/ExceptionTraceDao.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/dao/ExceptionTraceDao.java
new file mode 100644
index 0000000000000..6e85e313dec0f
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/dao/ExceptionTraceDao.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.dao;
+
+
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.util.ExceptionTraceQueryParameter;
+
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public interface ExceptionTraceDao {
+ List getExceptions(ExceptionTraceQueryParameter exceptionTraceQueryParameter);
+ List getSimpleExceptions(ExceptionTraceQueryParameter exceptionTraceQueryParameter);
+ SpanEventException getException(ExceptionTraceQueryParameter exceptionTraceQueryParameter);
+ List getSummaries(ExceptionTraceQueryParameter exceptionTraceQueryParameter);
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/dao/PinotExceptionTraceDao.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/dao/PinotExceptionTraceDao.java
new file mode 100644
index 0000000000000..c9418d6095956
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/dao/PinotExceptionTraceDao.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.dao;
+
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.util.ExceptionTraceQueryParameter;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.mybatis.spring.SqlSessionTemplate;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author intr3p1d
+ */
+@Repository
+public class PinotExceptionTraceDao implements ExceptionTraceDao {
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ private static final String NAMESPACE = PinotExceptionTraceDao.class.getName() + ".";
+
+ private static final String SELECT_QUERY = "selectExceptions";
+ private static final String SELECT_SIMPLE_QUERY = "selectSimpleExceptions";
+ private static final String SELECT_EXACT_QUERY = "selectExactException";
+ private static final String SELECT_SUMMARIES_QUERY = "selectSummaries";
+
+ private final SqlSessionTemplate sqlPinotSessionTemplate;
+
+ public PinotExceptionTraceDao(@Qualifier("exceptionTracePinotSessionTemplate") SqlSessionTemplate sqlPinotSessionTemplate) {
+ this.sqlPinotSessionTemplate = Objects.requireNonNull(sqlPinotSessionTemplate, "sqlPinotSessionTemplate");
+ }
+
+ @Override
+ public List getExceptions(ExceptionTraceQueryParameter exceptionTraceQueryParameter) {
+ return this.sqlPinotSessionTemplate.selectList(NAMESPACE + SELECT_QUERY, exceptionTraceQueryParameter);
+ }
+
+ @Override
+ public List getSimpleExceptions(ExceptionTraceQueryParameter exceptionTraceQueryParameter) {
+ return this.sqlPinotSessionTemplate.selectList(NAMESPACE + SELECT_SIMPLE_QUERY, exceptionTraceQueryParameter);
+ }
+
+ @Override
+ public SpanEventException getException(ExceptionTraceQueryParameter exceptionTraceQueryParameter) {
+ return this.sqlPinotSessionTemplate.selectOne(NAMESPACE + SELECT_EXACT_QUERY, exceptionTraceQueryParameter);
+ }
+
+ @Override
+ public List getSummaries(ExceptionTraceQueryParameter exceptionTraceQueryParameter) {
+ return this.sqlPinotSessionTemplate.selectList(NAMESPACE + SELECT_SUMMARIES_QUERY, exceptionTraceQueryParameter);
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/StackTraceTypeHandler.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/StackTraceTypeHandler.java
new file mode 100644
index 0000000000000..3fd59677ce203
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/StackTraceTypeHandler.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.mapper;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.navercorp.pinpoint.common.util.StringUtils;
+import com.navercorp.pinpoint.exceptiontrace.common.model.StackTraceElementWrapper;
+import org.apache.hadoop.hbase.shaded.com.google.gson.Gson;
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public class StackTraceTypeHandler extends BaseTypeHandler> {
+
+ private static final Logger logger = LogManager.getLogger(StackTraceTypeHandler.class);
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+
+ @Override
+ public void setNonNullParameter(
+ PreparedStatement preparedStatement,
+ int i,
+ List stackTraceElementWrappers,
+ JdbcType jdbcType
+ ) throws SQLException {
+ preparedStatement.setString(i, new Gson().toJson(stackTraceElementWrappers));
+ }
+
+ @Override
+ public List getNullableResult(ResultSet resultSet, String s) throws SQLException {
+ return convertToList(resultSet.getString(s));
+ }
+
+ @Override
+ public List getNullableResult(ResultSet resultSet, int i) throws SQLException {
+ return convertToList(resultSet.getString(i));
+ }
+
+ @Override
+ public List getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
+ return convertToList(callableStatement.getString(i));
+ }
+
+ protected List convertToList(String s) {
+ if (StringUtils.isEmpty(s)) {
+ return Collections.emptyList();
+ }
+ try {
+ List strings = objectMapper.readValue(s, new TypeReference<>() {
+ });
+ List stackTraceElementWrapperList = new ArrayList<>();
+ for(String str : strings) {
+ stackTraceElementWrapperList.add(objectMapper.readValue(str, StackTraceElementWrapper.class));
+ }
+ return stackTraceElementWrapperList;
+ } catch (IOException e) {
+ logger.error(e);
+ }
+ return Collections.emptyList();
+ }
+
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceGroup.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceGroup.java
new file mode 100644
index 0000000000000..5abf1b08b7cb1
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceGroup.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import com.navercorp.pinpoint.metric.web.util.TimeWindow;
+import com.navercorp.pinpoint.metric.web.view.TimeSeriesValueView;
+import com.navercorp.pinpoint.metric.web.view.TimeseriesValueGroupView;
+
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceGroup implements TimeseriesValueGroupView {
+
+ private final String exceptionClass;
+ private final List values;
+
+ private ExceptionTraceGroup(String exceptionClass, List values) {
+ this.exceptionClass = exceptionClass;
+ this.values = values;
+ }
+
+ public static ExceptionTraceGroup newGroupFromSummaries(
+ TimeWindow timeWindow,
+ String exceptionClass,
+ List exceptionTraceSummaries
+ ) {
+ return new ExceptionTraceGroup(
+ exceptionClass,
+ ExceptionTraceValue.createValueListGroupedBySimilarity(timeWindow, exceptionTraceSummaries)
+ );
+ }
+
+ @Override
+ public String getGroupName() {
+ return exceptionClass;
+ }
+
+ @Override
+ public List getMetricValues() {
+ return values;
+ }
+
+ public static class ExceptionTraceValue implements TimeSeriesValueView {
+
+ private final String fieldName;
+ private final List values;
+
+ private static List createValueListGroupedBySimilarity(
+ TimeWindow timeWindow,
+ List exceptionTraceSummaries
+ ) {
+ int[] timeseriesvalues = new int[(int) timeWindow.getWindowRangeCount()];
+ Arrays.fill(timeseriesvalues, 0);
+
+ for (ExceptionTraceSummary e : exceptionTraceSummaries) {
+ timeseriesvalues[timeWindow.getWindowIndex(e.getTimestamp())] += e.getCount();
+ }
+
+ return List.of(new ExceptionTraceValue(
+ "",
+ Arrays.stream(timeseriesvalues).boxed().collect(Collectors.toList())
+ ));
+ }
+
+ public ExceptionTraceValue(String fieldName, List values) {
+ this.fieldName = fieldName;
+ this.values = values;
+ }
+
+ @Override
+ public String getFieldName() {
+ return this.fieldName;
+ }
+
+ @Override
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public List getTags() {
+ return null;
+ }
+
+ @Override
+ public List getValues() {
+ return values;
+ }
+ }
+
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceSummary.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceSummary.java
new file mode 100644
index 0000000000000..f63522fbb3fb7
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceSummary.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.model;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceSummary {
+
+ private static final String EMPTY_STRING = null;
+ private final long timestamp;
+ private final long count;
+
+ private final String errorClassName;
+ private final String errorMessage;
+ private final String stackTraceHash;
+
+ public ExceptionTraceSummary(long timestamp, String errorClassName, String errorMessage, String stackTraceHash, long count) {
+ this.timestamp = timestamp;
+ this.errorClassName = errorClassName;
+ this.errorMessage = errorMessage;
+ this.stackTraceHash = stackTraceHash;
+ this.count = count;
+ }
+
+ public ExceptionTraceSummary(long timestamp, long count) {
+ this.timestamp = timestamp;
+ this.count = count;
+ this.errorClassName = EMPTY_STRING;
+ this.errorMessage = EMPTY_STRING;
+ this.stackTraceHash = EMPTY_STRING;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getErrorClassName() {
+ return errorClassName;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public String getStackTraceHash() {
+ return stackTraceHash;
+ }
+
+ public long getCount() {
+ return count;
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/service/ExceptionTraceService.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/service/ExceptionTraceService.java
new file mode 100644
index 0000000000000..6f217a22b9a02
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/service/ExceptionTraceService.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.service;
+
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.util.ExceptionTraceQueryParameter;
+
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public interface ExceptionTraceService {
+
+ List getTransactionExceptions(ExceptionTraceQueryParameter queryParameter);
+
+ List getExceptionsInRange(ExceptionTraceQueryParameter queryParameter);
+
+ List getSimilarExceptions(ExceptionTraceQueryParameter targetQuery, ExceptionTraceQueryParameter.Builder queryBuilder);
+
+ List getSummaryInRange(ExceptionTraceQueryParameter queryParameter);
+
+ List getSummaryOfSimilarExceptions(ExceptionTraceQueryParameter targetQuery, ExceptionTraceQueryParameter.Builder queryBuilder);
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/service/ExceptionTraceServiceImpl.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/service/ExceptionTraceServiceImpl.java
new file mode 100644
index 0000000000000..13ca85fbe9794
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/service/ExceptionTraceServiceImpl.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.service;
+
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import com.navercorp.pinpoint.exceptiontrace.web.dao.ExceptionTraceDao;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.util.ExceptionTraceQueryParameter;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * @author intr3p1d
+ */
+@Service
+public class ExceptionTraceServiceImpl implements ExceptionTraceService {
+
+ private final Logger logger = LogManager.getLogger(this.getClass());
+
+ private ExceptionTraceDao exceptionTraceDao;
+
+ public ExceptionTraceServiceImpl(ExceptionTraceDao exceptionTraceDao) {
+ this.exceptionTraceDao = Objects.requireNonNull(exceptionTraceDao, "exceptionTraceDao");
+ }
+
+ @Override
+ public List getTransactionExceptions(
+ ExceptionTraceQueryParameter queryParameter
+ ) {
+ return getTransactionExceptions(
+ queryParameter,
+ this::getSpanEventExceptions
+ );
+ }
+
+ @Override
+ public List getExceptionsInRange(
+ ExceptionTraceQueryParameter queryParameter
+ ) {
+ return getExceptionsInRange(
+ queryParameter,
+ this::getSimpleSpanEventExceptions
+ );
+ }
+
+ @Override
+ public List getSimilarExceptions(
+ ExceptionTraceQueryParameter targetQuery, ExceptionTraceQueryParameter.Builder queryBuilder
+ ) {
+ return getSimilarExceptions(
+ targetQuery,
+ queryBuilder,
+ this::getSimpleSpanEventExceptions
+ );
+ }
+
+ @Override
+ public List getSummaryInRange(
+ ExceptionTraceQueryParameter queryParameter
+ ) {
+ return getExceptionsInRange(
+ queryParameter,
+ this::getExceptionTraceSummaries
+ );
+ }
+
+ @Override
+ public List getSummaryOfSimilarExceptions(
+ ExceptionTraceQueryParameter targetQuery,
+ ExceptionTraceQueryParameter.Builder queryBuilder
+ ) {
+ return getSimilarExceptions(
+ targetQuery,
+ queryBuilder,
+ this::getExceptionTraceSummaries
+ );
+ }
+
+ private List getTransactionExceptions(
+ ExceptionTraceQueryParameter queryParameter,
+ Function> queryFunction
+ ) {
+ return queryFunction.apply(queryParameter);
+ }
+
+ private List getExceptionsInRange(
+ ExceptionTraceQueryParameter queryParameter,
+ Function> queryFunction
+ ) {
+ return queryFunction.apply(queryParameter);
+ }
+
+ private List getSimilarExceptions(
+ ExceptionTraceQueryParameter targetQuery,
+ ExceptionTraceQueryParameter.Builder queryBuilder,
+ Function> queryFunction
+ ) {
+ final SpanEventException targetException = getTheExactException(
+ targetQuery
+ );
+
+ if (targetException == null) {
+ return Collections.emptyList();
+ }
+ ExceptionTraceQueryParameter.Builder builder = queryBuilder
+ .setSpanEventException(targetException);
+
+ return queryFunction.apply(
+ builder.build()
+ );
+ }
+
+ private List getSpanEventExceptions(ExceptionTraceQueryParameter queryParameter) {
+ List spanEventExceptions = exceptionTraceDao.getExceptions(queryParameter);
+ logger.info(spanEventExceptions.size());
+ return spanEventExceptions;
+ }
+
+ private List getSimpleSpanEventExceptions(ExceptionTraceQueryParameter queryParameter) {
+ List spanEventExceptions = exceptionTraceDao.getSimpleExceptions(queryParameter);
+ logger.info(spanEventExceptions.size());
+ return spanEventExceptions;
+ }
+
+ private SpanEventException getTheExactException(ExceptionTraceQueryParameter queryParameter) {
+ return exceptionTraceDao.getException(queryParameter);
+ }
+
+ private List getExceptionTraceSummaries(ExceptionTraceQueryParameter queryParameter) {
+ List spanEventExceptions = exceptionTraceDao.getSummaries(queryParameter);
+ logger.info(spanEventExceptions.size());
+ return spanEventExceptions;
+ }
+
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/util/ExceptionTraceQueryParameter.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/util/ExceptionTraceQueryParameter.java
new file mode 100644
index 0000000000000..31329fb6de584
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/util/ExceptionTraceQueryParameter.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.util;
+
+import com.navercorp.pinpoint.exceptiontrace.common.model.SpanEventException;
+import com.navercorp.pinpoint.metric.web.util.QueryParameter;
+import com.navercorp.pinpoint.metric.web.util.TimePrecision;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceQueryParameter extends QueryParameter {
+ private final String applicationName;
+ private final String agentId;
+ private final SpanEventException spanEventException;
+
+ private final String transactionId;
+ private final long spanId;
+ private final long exceptionId;
+ private final int exceptionDepth;
+
+ protected ExceptionTraceQueryParameter(Builder builder) {
+ super(builder.getRange(), builder.getTimePrecision(), builder.getLimit());
+ this.applicationName = builder.applicationName;
+ this.agentId = builder.agentId;
+ this.spanEventException = builder.spanEventException;
+ this.transactionId = builder.transactionId;
+ this.spanId = builder.spanId;
+ this.exceptionId = builder.exceptionId;
+ this.exceptionDepth = builder.exceptionDepth;
+ }
+
+ public static class Builder extends QueryParameter.Builder {
+ private static final int LIMIT = 65536;
+ private String applicationName;
+ private String agentId = null;
+
+ private SpanEventException spanEventException = null;
+
+ private String transactionId = null;
+ private long spanId = Long.MIN_VALUE;
+ private long exceptionId = Long.MIN_VALUE;
+ private int exceptionDepth = Integer.MIN_VALUE;
+
+ @Override
+ protected Builder self() {
+ return this;
+ }
+
+ public Builder setApplicationName(String applicationName) {
+ this.applicationName = applicationName;
+ return self();
+ }
+
+ public Builder setExceptionDepth(int exceptionDepth) {
+ this.exceptionDepth = exceptionDepth;
+ return self();
+ }
+
+ public Builder setAgentId(String agentId) {
+ this.agentId = agentId;
+ return self();
+ }
+
+ public Builder setSpanEventException(SpanEventException spanEventException) {
+ this.spanEventException = Objects.requireNonNull(spanEventException, "spanEventException");
+ return self();
+ }
+
+ public Builder setTransactionId(String transactionId) {
+ this.transactionId = transactionId;
+ return self();
+ }
+
+ public Builder setSpanId(long spanId) {
+ this.spanId = spanId;
+ return self();
+ }
+
+ public Builder setExceptionId(long exceptionId) {
+ this.exceptionId = exceptionId;
+ return self();
+ }
+
+ @Override
+ public ExceptionTraceQueryParameter build() {
+ if (timePrecision == null) {
+ this.timePrecision = TimePrecision.newTimePrecision(TimeUnit.MILLISECONDS, 30000);
+ }
+ if (this.range != null) {
+ this.limit = estimateLimit();
+ } else {
+ this.limit = LIMIT;
+ }
+ return new ExceptionTraceQueryParameter(this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ExceptionTraceQueryParameter{" +
+ "applicationName='" + applicationName + '\'' +
+ ", agentId='" + agentId + '\'' +
+ ", spanEventException=" + spanEventException +
+ ", transactionId='" + transactionId + '\'' +
+ ", spanId=" + spanId +
+ ", exceptionId=" + exceptionId +
+ ", exceptionDepth=" + exceptionDepth +
+ '}';
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/view/ExceptionTraceView.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/view/ExceptionTraceView.java
new file mode 100644
index 0000000000000..4db12267171f8
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/view/ExceptionTraceView.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2023 NAVER Corp.
+ *
+ * Licensed 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 com.navercorp.pinpoint.exceptiontrace.web.view;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceGroup;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.metric.web.util.TimeWindow;
+import com.navercorp.pinpoint.metric.web.view.TimeSeriesView;
+import com.navercorp.pinpoint.metric.web.view.TimeseriesValueGroupView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceView implements TimeSeriesView {
+
+ private static final String TITLE = "exceptionTrace";
+
+ private final List timestampList;
+
+ private final List exceptionTrace = new ArrayList<>();
+
+ private ExceptionTraceView(List timestampList, List exceptionTraces) {
+ this.timestampList = timestampList;
+ this.exceptionTrace.addAll(exceptionTraces);
+ }
+
+ public static ExceptionTraceView newViewFromSummaries(
+ String exceptionClass,
+ TimeWindow timeWindow,
+ List exceptionTraceSummaries
+ ) {
+ Objects.requireNonNull(timeWindow, "timeWindow");
+ Objects.requireNonNull(exceptionTraceSummaries, "exceptionTraceSummaries");
+
+ List timestampList = createTimeStampList(timeWindow);
+ List timeseriesValueGroupViews = new ArrayList<>();
+ timeseriesValueGroupViews.add(
+ ExceptionTraceGroup.newGroupFromSummaries(timeWindow, exceptionClass, exceptionTraceSummaries)
+ );
+
+ return new ExceptionTraceView(
+ timestampList, timeseriesValueGroupViews
+ );
+ }
+
+ private static List createTimeStampList(TimeWindow timeWindow) {
+ List timestampList = new ArrayList<>((int) timeWindow.getWindowRangeCount());
+
+ for (Long timestamp : timeWindow) {
+ timestampList.add(timestamp);
+ }
+
+ return timestampList;
+ }
+
+ @Override
+ public String getTitle() {
+ return TITLE;
+ }
+
+ @Override
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public String getUnit() {
+ return null;
+ }
+
+ @Override
+ public List getTimestamp() {
+ return timestampList;
+ }
+
+ @Override
+ public List getMetricValueGroups() {
+ return this.exceptionTrace;
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/resources/mapper/exceptiontrace/ExceptionTraceMapper.xml b/exceptiontrace/exceptiontrace-web/src/main/resources/mapper/exceptiontrace/ExceptionTraceMapper.xml
new file mode 100644
index 0000000000000..d841bc76cabd0
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/resources/mapper/exceptiontrace/ExceptionTraceMapper.xml
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ exceptionTrace
+
+
+ DATETIME_CONVERT
+ ("timestamp", '1:MILLISECONDS:EPOCH', '1:MILLISECONDS:EPOCH',
+ '#{timePrecision.timeSize}:${timePrecision.timeUnit}') as "timestamp",
+ transactionId,
+ spanId,
+ exceptionId,
+ applicationServiceType,
+ applicationName,
+ agentId,
+ errorClassName,
+ errorMessage,
+ exceptionDepth,
+ stackTrace,
+ stackTraceHash
+
+
+
+ DATETIME_CONVERT
+ ("timestamp", '1:MILLISECONDS:EPOCH', '1:MILLISECONDS:EPOCH',
+ '#{timePrecision.timeSize}:${timePrecision.timeUnit}') as "timestamp",
+ transactionId,
+ spanId,
+ exceptionId,
+ applicationServiceType,
+ applicationName,
+ agentId,
+ errorClassName,
+ errorMessage,
+ exceptionDepth,
+ stackTraceHash
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-web/src/main/resources/profiles/local/pinpoint-web-exceptiontrace.properties b/exceptiontrace/exceptiontrace-web/src/main/resources/profiles/local/pinpoint-web-exceptiontrace.properties
new file mode 100644
index 0000000000000..87b78431e4e19
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/resources/profiles/local/pinpoint-web-exceptiontrace.properties
@@ -0,0 +1 @@
+config.show.exceptionTrace=true
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-web/src/main/resources/profiles/release/pinpoint-web-exceptiontrace.properties b/exceptiontrace/exceptiontrace-web/src/main/resources/profiles/release/pinpoint-web-exceptiontrace.properties
new file mode 100644
index 0000000000000..87b78431e4e19
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/resources/profiles/release/pinpoint-web-exceptiontrace.properties
@@ -0,0 +1 @@
+config.show.exceptionTrace=true
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-web/src/test/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/StackTraceTypeHandlerTest.java b/exceptiontrace/exceptiontrace-web/src/test/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/StackTraceTypeHandlerTest.java
new file mode 100644
index 0000000000000..c1c75bb9f1467
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/test/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/StackTraceTypeHandlerTest.java
@@ -0,0 +1,57 @@
+package com.navercorp.pinpoint.exceptiontrace.web.mapper;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.navercorp.pinpoint.exceptiontrace.common.model.StackTraceElementWrapper;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+/**
+ * @author intr3p1d
+ */
+public class StackTraceTypeHandlerTest {
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private final static StackTraceTypeHandler stackTraceTypeHandler = new StackTraceTypeHandler();
+
+ private List newStackTraceElementWrappers(Throwable throwable) {
+ List elements = List.of(throwable.getStackTrace());
+ return elements.stream().map(
+ (StackTraceElement s) -> new StackTraceElementWrapper(
+ s.getClassName(),
+ s.getFileName(),
+ s.getLineNumber(),
+ s.getMethodName()
+ )
+ ).collect(Collectors.toList());
+ }
+
+ private String serializedStackTraceElementWrappers(List wrappers) throws JsonProcessingException {
+ List strings = new ArrayList<>();
+ wrappers.forEach(
+ (StackTraceElementWrapper s) -> {
+ try {
+ strings.add(OBJECT_MAPPER.writeValueAsString(s));
+ } catch (JsonProcessingException ignored) {
+ // do nothing
+ }
+ }
+ );
+ return OBJECT_MAPPER.writeValueAsString(strings);
+ }
+
+ @Test
+ public void testConvertToList() throws JsonProcessingException {
+ Throwable exception = new RuntimeException("sample exception");
+ List expected = newStackTraceElementWrappers(exception);
+ List actual = stackTraceTypeHandler.convertToList(serializedStackTraceElementWrappers(expected));
+ assertThat(actual).isEqualTo(expected);
+ }
+
+}
diff --git a/exceptiontrace/pom.xml b/exceptiontrace/pom.xml
new file mode 100644
index 0000000000000..14f8f75a6c103
--- /dev/null
+++ b/exceptiontrace/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ pinpoint
+ com.navercorp.pinpoint
+ 2.5.1-SNAPSHOT
+
+ 4.0.0
+
+ pinpoint-exceptiontrace-module
+ pom
+
+
+ exceptiontrace-common
+ exceptiontrace-collector
+ exceptiontrace-web
+
+
+
\ No newline at end of file
diff --git a/metric-module/collector-starter/pom.xml b/metric-module/collector-starter/pom.xml
index e39d437bc5e21..5822c30d6a7a3 100644
--- a/metric-module/collector-starter/pom.xml
+++ b/metric-module/collector-starter/pom.xml
@@ -34,6 +34,10 @@
com.navercorp.pinpoint
pinpoint-uristat-collector
+
+ com.navercorp.pinpoint
+ pinpoint-exceptiontrace-collector
+
diff --git a/metric-module/collector-starter/src/main/java/com/navercorp/pinpoint/collector/starter/multi/application/MultiApplication.java b/metric-module/collector-starter/src/main/java/com/navercorp/pinpoint/collector/starter/multi/application/MultiApplication.java
index 6c406397362b8..b6a99f3515d4d 100644
--- a/metric-module/collector-starter/src/main/java/com/navercorp/pinpoint/collector/starter/multi/application/MultiApplication.java
+++ b/metric-module/collector-starter/src/main/java/com/navercorp/pinpoint/collector/starter/multi/application/MultiApplication.java
@@ -6,6 +6,7 @@
import com.navercorp.pinpoint.common.server.env.ExternalEnvironmentListener;
import com.navercorp.pinpoint.common.server.env.ProfileResolveListener;
import com.navercorp.pinpoint.common.server.util.ServerBootLogger;
+import com.navercorp.pinpoint.exceptiontrace.collector.ExceptionTraceCollectorConfig;
import com.navercorp.pinpoint.metric.collector.CollectorType;
import com.navercorp.pinpoint.metric.collector.CollectorTypeParser;
import com.navercorp.pinpoint.metric.collector.MetricCollectorApp;
@@ -44,9 +45,10 @@ public static void main(String[] args) {
if (types.hasType(CollectorType.BASIC)) {
logger.info(String.format("Start %s collector", CollectorType.BASIC));
- SpringApplicationBuilder collectorAppBuilder = createAppBuilder(builder, 15400, BasicCollectorApp.class, UriStatCollectorConfig.class);
+ SpringApplicationBuilder collectorAppBuilder = createAppBuilder(builder, 15400, BasicCollectorApp.class, UriStatCollectorConfig.class, ExceptionTraceCollectorConfig.class);
collectorAppBuilder.listeners(new AdditionalProfileListener("metric"));
collectorAppBuilder.listeners(new AdditionalProfileListener("uri"));
+ collectorAppBuilder.listeners(new AdditionalProfileListener("exception"));
collectorAppBuilder.build().run(args);
}
diff --git a/metric-module/web-starter/pom.xml b/metric-module/web-starter/pom.xml
index 0fde50cb86c33..d5708b12c482f 100644
--- a/metric-module/web-starter/pom.xml
+++ b/metric-module/web-starter/pom.xml
@@ -44,6 +44,10 @@
com.navercorp.pinpoint
pinpoint-uristat-web
+
+ com.navercorp.pinpoint
+ pinpoint-exceptiontrace-web
+
diff --git a/metric-module/web-starter/src/main/java/com/navercorp/pinpoint/web/starter/multi/MetricAndWebApp.java b/metric-module/web-starter/src/main/java/com/navercorp/pinpoint/web/starter/multi/MetricAndWebApp.java
index 5d8022412d6a0..b43302195c4db 100644
--- a/metric-module/web-starter/src/main/java/com/navercorp/pinpoint/web/starter/multi/MetricAndWebApp.java
+++ b/metric-module/web-starter/src/main/java/com/navercorp/pinpoint/web/starter/multi/MetricAndWebApp.java
@@ -17,6 +17,7 @@
package com.navercorp.pinpoint.web.starter.multi;
import com.navercorp.pinpoint.common.server.util.ServerBootLogger;
+import com.navercorp.pinpoint.exceptiontrace.web.ExceptionTraceWebConfig;
import com.navercorp.pinpoint.metric.web.MetricWebApp;
import com.navercorp.pinpoint.uristat.web.UriStatWebConfig;
import com.navercorp.pinpoint.web.AuthorizationConfig;
@@ -47,8 +48,8 @@ public class MetricAndWebApp {
public static void main(String[] args) {
try {
- WebStarter starter = new WebStarter(MetricAndWebApp.class, PinpointBasicLoginConfig.class, AuthorizationConfig.class, MetricWebApp.class, UriStatWebConfig.class);
- starter.addProfiles("uri", "metric");
+ WebStarter starter = new WebStarter(MetricAndWebApp.class, PinpointBasicLoginConfig.class, AuthorizationConfig.class, MetricWebApp.class, UriStatWebConfig.class, ExceptionTraceWebConfig.class);
+ starter.addProfiles("uri", "metric", "exception");
starter.start(args);
} catch (Exception exception) {
logger.error("[WebApp] could not launch app.", exception);
diff --git a/pom.xml b/pom.xml
index 6995bfafd5420..764b6fde5161a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -119,6 +119,7 @@
pinot
metric-module
uristat
+ exceptiontrace
@@ -449,6 +450,21 @@
pinpoint-uristat-collector
${project.version}
+
+ com.navercorp.pinpoint
+ pinpoint-exceptiontrace-common
+ ${project.version}
+
+
+ com.navercorp.pinpoint
+ pinpoint-exceptiontrace-collector
+ ${project.version}
+
+
+ com.navercorp.pinpoint
+ pinpoint-exceptiontrace-web
+ ${project.version}
+
com.navercorp.pinpoint
pinpoint-metric