diff --git a/commons-server/pom.xml b/commons-server/pom.xml
index 730bc2ef350e..589e16e8ed7b 100644
--- a/commons-server/pom.xml
+++ b/commons-server/pom.xml
@@ -160,6 +160,10 @@
+
+ org.mapstruct
+ mapstruct
+
diff --git a/commons-server/src/main/java/com/navercorp/pinpoint/common/server/mapper/MapStructUtils.java b/commons-server/src/main/java/com/navercorp/pinpoint/common/server/mapper/MapStructUtils.java
new file mode 100644
index 000000000000..caa2ecdf003b
--- /dev/null
+++ b/commons-server/src/main/java/com/navercorp/pinpoint/common/server/mapper/MapStructUtils.java
@@ -0,0 +1,87 @@
+/*
+ * 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.common.server.mapper;
+
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.navercorp.pinpoint.common.server.util.json.Jackson;
+import com.navercorp.pinpoint.common.server.util.json.JsonRuntimeException;
+import com.navercorp.pinpoint.common.util.CollectionUtils;
+import com.navercorp.pinpoint.common.util.StringUtils;
+import org.mapstruct.Qualifier;
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+@Component
+public class MapStructUtils {
+ private final ObjectMapper mapper;
+
+ public MapStructUtils(ObjectMapper mapper) {
+ this.mapper = Objects.requireNonNull(mapper, "mapper");
+ }
+
+
+ @Qualifier
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.CLASS)
+ public @interface JsonStrToList {
+
+ }
+
+ @Qualifier
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.CLASS)
+ public @interface listToJsonStr {
+
+ }
+
+
+ @JsonStrToList
+ public List jsonStrToList(String s) {
+ if (StringUtils.isEmpty(s)) {
+ return Collections.emptyList();
+ }
+ try {
+ return mapper.readValue(s, new TypeReference<>() {
+ });
+ } catch (JacksonException e) {
+ throw new JsonRuntimeException("Json read error", e);
+ }
+ }
+
+ @listToJsonStr
+ public String listToJsonStr(List lists) {
+ if (CollectionUtils.isEmpty(lists)) {
+ return "";
+ }
+ try {
+ return mapper.writeValueAsString(lists);
+ } catch (JacksonException e) {
+ throw new JsonRuntimeException("Json Write error", e);
+ }
+ }
+
+}
diff --git a/exceptiontrace/exceptiontrace-collector/pom.xml b/exceptiontrace/exceptiontrace-collector/pom.xml
new file mode 100644
index 000000000000..f2c08733e675
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/pom.xml
@@ -0,0 +1,84 @@
+
+
+
+ pinpoint-exceptiontrace-module
+ com.navercorp.pinpoint
+ 2.6.0-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
+ ${spring.kafka.version}
+
+
+ com.navercorp.pinpoint
+ pinpoint-collector
+
+
+ org.mapstruct
+ mapstruct
+
+
+ org.springframework
+ spring-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-test
+
+
+ org.testng
+ testng
+ 6.13.1
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+
+
+
+
+
+
\ 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 000000000000..e6f9e064c4c6
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/ExceptionTraceCollectorConfig.java
@@ -0,0 +1,42 @@
+/*
+ * 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.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.PropertySource;
+
+/**
+ * @author intr3p1d
+ */
+@Configuration
+@Import({PinotConfiguration.class, ExceptionMetricKafkaConfiguration.class})
+@ComponentScan({
+ "com.navercorp.pinpoint.common.server.mapper",
+ "com.navercorp.pinpoint.exceptiontrace.collector.service",
+ "com.navercorp.pinpoint.exceptiontrace.collector.dao",
+ "com.navercorp.pinpoint.exceptiontrace.collector.mapper",
+})
+@PropertySource({ExceptionTraceCollectorConfig.KAFKA_TOPIC_PROPERTIES})
+@ConditionalOnProperty(name = "pinpoint.modules.collector.exceptiontrace.enabled", havingValue = "true")
+public class ExceptionTraceCollectorConfig {
+ public static final String KAFKA_TOPIC_PROPERTIES = "classpath:profiles/${pinpoint.profiles.active}/kafka-topic-exception.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 000000000000..423bbdb300dd
--- /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 000000000000..039e99ed4c0c
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/config/ExceptionMetricKafkaConfiguration.java
@@ -0,0 +1,40 @@
+/*
+ * 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.entity.ExceptionMetaDataEntity;
+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 kafkaExceptionMetaDataTemplate(
+ @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 000000000000..a1454a30e257
--- /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.ExceptionMetaData;
+
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public interface ExceptionTraceDao {
+ void insert(List exceptionMetaData);
+}
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 000000000000..b38fec720ad6
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/dao/PinotExceptionTraceDao.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.collector.dao;
+
+import com.navercorp.pinpoint.common.server.util.StringPrecondition;
+import com.navercorp.pinpoint.exceptiontrace.collector.entity.ExceptionMetaDataEntity;
+import com.navercorp.pinpoint.exceptiontrace.collector.mapper.ExceptionMetaDataMapper;
+import com.navercorp.pinpoint.exceptiontrace.common.model.ExceptionMetaData;
+import com.navercorp.pinpoint.pinot.kafka.util.KafkaCallbacks;
+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.kafka.support.SendResult;
+import org.springframework.stereotype.Repository;
+import org.springframework.util.concurrent.ListenableFuture;
+import org.springframework.util.concurrent.ListenableFutureCallback;
+
+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 kafkaExceptionMetaDataTemplate;
+
+ private final ExceptionMetaDataMapper mapper;
+
+ private final String topic;
+
+ private final ListenableFutureCallback> resultCallback
+ = KafkaCallbacks.loggingCallback("Kafka(ExceptionMetaDataEntity)", logger);
+
+
+ public PinotExceptionTraceDao(
+ @Qualifier("kafkaExceptionMetaDataTemplate") KafkaTemplate kafkaExceptionMetaDataTemplate,
+ @Value("${kafka.exception.topic}") String topic,
+ ExceptionMetaDataMapper mapper
+ ) {
+ this.kafkaExceptionMetaDataTemplate = Objects.requireNonNull(kafkaExceptionMetaDataTemplate, "kafkaExceptionMetaDataTemplate");
+ this.topic = StringPrecondition.requireHasLength(topic, "topic");
+ this.mapper = Objects.requireNonNull(mapper, "mapper");
+ }
+
+ @Override
+ public void insert(List exceptionMetaData) {
+ Objects.requireNonNull(exceptionMetaData);
+ logger.info("Pinot data insert: {}", exceptionMetaData);
+
+ for (ExceptionMetaData e : exceptionMetaData) {
+ ExceptionMetaDataEntity dataEntity = mapper.toEntity(e);
+ ListenableFuture> response = this.kafkaExceptionMetaDataTemplate.send(
+ topic, dataEntity
+ );
+ response.addCallback(resultCallback);
+ }
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/entity/ExceptionMetaDataEntity.java b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/entity/ExceptionMetaDataEntity.java
new file mode 100644
index 000000000000..77f65f0b11bf
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/entity/ExceptionMetaDataEntity.java
@@ -0,0 +1,175 @@
+/*
+ * 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.entity;
+
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionMetaDataEntity {
+ private long timestamp;
+
+ private String transactionId;
+ private long spanId;
+ private long exceptionId;
+
+ private String applicationServiceType;
+ private String applicationName;
+ private String agentId;
+ private String uriTemplate;
+
+ private String errorClassName;
+ private String errorMessage;
+ private int exceptionDepth;
+
+ private List stackTraceClassName;
+ private List stackTraceFileName;
+ private List stackTraceLineNumber;
+ private List stackTraceMethodName;
+ private String stackTraceHash;
+
+ public ExceptionMetaDataEntity() {
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public String getTransactionId() {
+ return transactionId;
+ }
+
+ public void setTransactionId(String transactionId) {
+ this.transactionId = transactionId;
+ }
+
+ public long getSpanId() {
+ return spanId;
+ }
+
+ public void setSpanId(long spanId) {
+ this.spanId = spanId;
+ }
+
+ public long getExceptionId() {
+ return exceptionId;
+ }
+
+ public void setExceptionId(long exceptionId) {
+ this.exceptionId = exceptionId;
+ }
+
+ public String getApplicationServiceType() {
+ return applicationServiceType;
+ }
+
+ public void setApplicationServiceType(String applicationServiceType) {
+ this.applicationServiceType = applicationServiceType;
+ }
+
+ public String getApplicationName() {
+ return applicationName;
+ }
+
+ public void setApplicationName(String applicationName) {
+ this.applicationName = applicationName;
+ }
+
+ public String getAgentId() {
+ return agentId;
+ }
+
+ public void setAgentId(String agentId) {
+ this.agentId = agentId;
+ }
+
+ public String getUriTemplate() {
+ return uriTemplate;
+ }
+
+ public void setUriTemplate(String uriTemplate) {
+ this.uriTemplate = uriTemplate;
+ }
+
+ public String getErrorClassName() {
+ return errorClassName;
+ }
+
+ public void setErrorClassName(String errorClassName) {
+ this.errorClassName = errorClassName;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ public int getExceptionDepth() {
+ return exceptionDepth;
+ }
+
+ public void setExceptionDepth(int exceptionDepth) {
+ this.exceptionDepth = exceptionDepth;
+ }
+
+ public List getStackTraceClassName() {
+ return stackTraceClassName;
+ }
+
+ public void setStackTraceClassName(List stackTraceClassName) {
+ this.stackTraceClassName = stackTraceClassName;
+ }
+
+ public List getStackTraceFileName() {
+ return stackTraceFileName;
+ }
+
+ public void setStackTraceFileName(List stackTraceFileName) {
+ this.stackTraceFileName = stackTraceFileName;
+ }
+
+ public List getStackTraceLineNumber() {
+ return stackTraceLineNumber;
+ }
+
+ public void setStackTraceLineNumber(List stackTraceLineNumber) {
+ this.stackTraceLineNumber = stackTraceLineNumber;
+ }
+
+ public List getStackTraceMethodName() {
+ return stackTraceMethodName;
+ }
+
+ public void setStackTraceMethodName(List stackTraceMethodName) {
+ this.stackTraceMethodName = stackTraceMethodName;
+ }
+
+ public String getStackTraceHash() {
+ return stackTraceHash;
+ }
+
+ public void setStackTraceHash(String stackTraceHash) {
+ this.stackTraceHash = stackTraceHash;
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/mapper/ExceptionMetaDataMapper.java b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/mapper/ExceptionMetaDataMapper.java
new file mode 100644
index 000000000000..6ddf673353a5
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/mapper/ExceptionMetaDataMapper.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.mapper;
+
+import com.navercorp.pinpoint.exceptiontrace.collector.entity.ExceptionMetaDataEntity;
+import com.navercorp.pinpoint.exceptiontrace.common.model.ExceptionMetaData;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+
+/**
+ * @author intr3p1d
+ */
+@Mapper(componentModel = "spring", uses = {StackTraceMapper.class})
+public interface ExceptionMetaDataMapper {
+
+ @Mappings({
+ @Mapping(source = "stackTrace", target = "stackTraceClassName", qualifiedBy = StackTraceMapper.StackTraceToClassNames.class),
+ @Mapping(source = "stackTrace", target = "stackTraceFileName", qualifiedBy = StackTraceMapper.StackTraceToFileNames.class),
+ @Mapping(source = "stackTrace", target = "stackTraceLineNumber", qualifiedBy = StackTraceMapper.StackTraceToLineNumbers.class),
+ @Mapping(source = "stackTrace", target = "stackTraceMethodName", qualifiedBy = StackTraceMapper.StackTraceToMethodNames.class)
+ })
+ ExceptionMetaDataEntity toEntity(ExceptionMetaData model);
+
+}
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/mapper/StackTraceMapper.java b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/mapper/StackTraceMapper.java
new file mode 100644
index 000000000000..520a1061453c
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/mapper/StackTraceMapper.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.collector.mapper;
+
+import com.navercorp.pinpoint.common.server.mapper.MapStructUtils;
+import com.navercorp.pinpoint.exceptiontrace.common.model.StackTraceElementWrapper;
+import org.mapstruct.Qualifier;
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author intr3p1d
+ */
+@Component
+public class StackTraceMapper {
+
+ private final MapStructUtils mapStructUtils;
+
+ public StackTraceMapper(MapStructUtils mapStructUtils) {
+ this.mapStructUtils = Objects.requireNonNull(mapStructUtils, "mapStructUtils");
+ }
+
+ @Qualifier
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.CLASS)
+ public @interface StackTraceToClassNames {
+ }
+
+ @Qualifier
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.CLASS)
+ public @interface StackTraceToFileNames {
+ }
+
+ @Qualifier
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.CLASS)
+ public @interface StackTraceToLineNumbers {
+ }
+
+ @Qualifier
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.CLASS)
+ public @interface StackTraceToMethodNames {
+ }
+
+ @StackTraceToClassNames
+ public List stackTraceToClassNames(List classNames) {
+ return classNames.stream()
+ .map(StackTraceElementWrapper::getClassName)
+ .collect(Collectors.toList());
+ }
+
+ @StackTraceToFileNames
+ public List stackTraceToFileNames(List fileNames) {
+ return fileNames.stream()
+ .map(StackTraceElementWrapper::getFileName)
+ .collect(Collectors.toList());
+ }
+
+ @StackTraceToLineNumbers
+ public List stackTraceToLineNumber(List lineNumbers) {
+ return lineNumbers.stream()
+ .map(StackTraceElementWrapper::getLineNumber)
+ .collect(Collectors.toList());
+ }
+
+ @StackTraceToMethodNames
+ public List stackTraceToMethodNames(List methodNames) {
+ return methodNames.stream()
+ .map(StackTraceElementWrapper::getMethodName)
+ .collect(Collectors.toList());
+ }
+
+}
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 000000000000..4c77bde9c672
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/java/com/navercorp/pinpoint/exceptiontrace/collector/service/PinotExceptionTraceService.java
@@ -0,0 +1,98 @@
+/*
+ * 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.ExceptionMetaDataService;
+import com.navercorp.pinpoint.common.profiler.util.TransactionId;
+import com.navercorp.pinpoint.common.profiler.util.TransactionIdUtils;
+import com.navercorp.pinpoint.common.server.bo.exception.ExceptionMetaDataBo;
+import com.navercorp.pinpoint.common.server.bo.exception.ExceptionWrapperBo;
+import com.navercorp.pinpoint.common.server.bo.exception.StackTraceElementWrapperBo;
+import com.navercorp.pinpoint.common.trace.ServiceType;
+import com.navercorp.pinpoint.exceptiontrace.collector.dao.ExceptionTraceDao;
+import com.navercorp.pinpoint.exceptiontrace.common.model.ExceptionMetaData;
+import com.navercorp.pinpoint.exceptiontrace.common.model.StackTraceElementWrapper;
+import com.navercorp.pinpoint.loader.service.ServiceTypeRegistryService;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.Valid;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author intr3p1d
+ */
+@Service
+@ConditionalOnProperty(name = "pinpoint.modules.collector.exceptiontrace.enabled", havingValue = "true")
+@Validated
+public class PinotExceptionTraceService implements ExceptionMetaDataService {
+ private final ExceptionTraceDao exceptionTraceDao;
+ private final ServiceTypeRegistryService registry;
+
+ public PinotExceptionTraceService(ExceptionTraceDao exceptionTraceDao, ServiceTypeRegistryService registry) {
+ this.exceptionTraceDao = Objects.requireNonNull(exceptionTraceDao, "exceptionTraceDao");
+ this.registry = Objects.requireNonNull(registry, "serviceTypeRegistryService");
+ }
+
+ @Override
+ public void save(@Valid ExceptionMetaDataBo exceptionMetaDataBo) {
+ List exceptionMetaData = toExceptionMetaData(exceptionMetaDataBo);
+ exceptionTraceDao.insert(exceptionMetaData);
+ }
+
+ private List toExceptionMetaData(
+ ExceptionMetaDataBo exceptionMetaDataBo
+ ) {
+ List exceptionMetaData = new ArrayList<>();
+ final ServiceType serviceType = registry.findServiceType(exceptionMetaDataBo.getServiceType());
+ for (ExceptionWrapperBo e : exceptionMetaDataBo.getExceptionWrapperBos()) {
+ final List wrappers = traceElementWrappers(e.getStackTraceElements());
+ exceptionMetaData.add(
+ ExceptionMetaData.valueOf(
+ e.getStartTime(),
+ transactionIdToString(exceptionMetaDataBo.getTransactionId()),
+ exceptionMetaDataBo.getSpanId(),
+ e.getExceptionId(),
+ serviceType.getName(),
+ exceptionMetaDataBo.getApplicationName(),
+ exceptionMetaDataBo.getAgentId(),
+ exceptionMetaDataBo.getUriTemplate(),
+ e.getExceptionClassName(),
+ e.getExceptionMessage(),
+ e.getExceptionDepth(),
+ wrappers
+ )
+ );
+ }
+ return exceptionMetaData;
+ }
+
+ private static List traceElementWrappers(List wrapperBos) {
+ return wrapperBos.stream().map(
+ (StackTraceElementWrapperBo s) -> new StackTraceElementWrapper(s.getClassName(), s.getFileName(), s.getLineNumber(), s.getMethodName())
+ ).collect(Collectors.toList());
+ }
+
+ 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 000000000000..e99d3db765d7
--- /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-collector/src/main/resources/profiles/local/kafka-topic-exception.properties b/exceptiontrace/exceptiontrace-collector/src/main/resources/profiles/local/kafka-topic-exception.properties
new file mode 100644
index 000000000000..e99d3db765d7
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/resources/profiles/local/kafka-topic-exception.properties
@@ -0,0 +1 @@
+kafka.exception.topic=exception-trace
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-collector/src/main/resources/profiles/release/kafka-topic-exception.properties b/exceptiontrace/exceptiontrace-collector/src/main/resources/profiles/release/kafka-topic-exception.properties
new file mode 100644
index 000000000000..e99d3db765d7
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/main/resources/profiles/release/kafka-topic-exception.properties
@@ -0,0 +1 @@
+kafka.exception.topic=exception-trace
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-collector/src/test/java/com/navercorp/pinpoint/exceptiontrace/collector/mapper/ExceptionMetaDataMapperTest.java b/exceptiontrace/exceptiontrace-collector/src/test/java/com/navercorp/pinpoint/exceptiontrace/collector/mapper/ExceptionMetaDataMapperTest.java
new file mode 100644
index 000000000000..175913d04fe0
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-collector/src/test/java/com/navercorp/pinpoint/exceptiontrace/collector/mapper/ExceptionMetaDataMapperTest.java
@@ -0,0 +1,117 @@
+package com.navercorp.pinpoint.exceptiontrace.collector.mapper;
+
+
+import com.navercorp.pinpoint.common.server.mapper.MapStructUtils;
+import com.navercorp.pinpoint.exceptiontrace.collector.entity.ExceptionMetaDataEntity;
+import com.navercorp.pinpoint.exceptiontrace.common.model.ExceptionMetaData;
+import com.navercorp.pinpoint.exceptiontrace.common.model.StackTraceElementWrapper;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.stream.Collectors;
+
+/**
+ * @author intr3p1d
+ */
+@ContextConfiguration(classes = {
+ ExceptionMetaDataMapperImpl.class,
+ StackTraceMapper.class,
+ MapStructUtils.class,
+ JacksonAutoConfiguration.class
+})
+@ExtendWith(SpringExtension.class)
+class ExceptionMetaDataMapperTest {
+
+ private final Logger logger = LogManager.getLogger(getClass());
+ private final Random random = new Random();
+
+
+ @Autowired
+ ExceptionMetaDataMapper mapper;
+
+ @Autowired
+ MapStructUtils mapStructUtils;
+
+
+ @Test
+ public void testModelToEntity() {
+ Throwable throwable = new RuntimeException();
+
+ ExceptionMetaData expected = newRandomExceptionMetaData(throwable);
+ ExceptionMetaDataEntity actual = mapper.toEntity(expected);
+
+ Assertions.assertEquals(expected.getTimestamp(), actual.getTimestamp());
+ Assertions.assertEquals(expected.getTransactionId(), actual.getTransactionId());
+ Assertions.assertEquals(expected.getSpanId(), actual.getSpanId());
+ Assertions.assertEquals(expected.getExceptionId(), actual.getExceptionId());
+
+ Assertions.assertEquals(expected.getApplicationServiceType(), actual.getApplicationServiceType());
+ Assertions.assertEquals(expected.getApplicationName(), actual.getApplicationName());
+ Assertions.assertEquals(expected.getAgentId(), actual.getAgentId());
+ Assertions.assertEquals(expected.getUriTemplate(), actual.getUriTemplate());
+
+ Assertions.assertEquals(expected.getErrorClassName(), actual.getErrorClassName());
+ Assertions.assertEquals(expected.getErrorMessage(), actual.getErrorMessage());
+ Assertions.assertEquals(expected.getExceptionDepth(), actual.getExceptionDepth());
+
+ Assertions.assertEquals(expected.getStackTraceHash(), actual.getStackTraceHash());
+
+ StackTraceElement[] expectedStackTrace = throwable.getStackTrace();
+ int size = throwable.getStackTrace().length;
+
+ List classNames = actual.getStackTraceClassName();
+ List fileNames = actual.getStackTraceFileName();
+ List lineNumbers = actual.getStackTraceLineNumber();
+ List methodNames = actual.getStackTraceMethodName();
+
+ for (int i = 0; i < throwable.getStackTrace().length; i++) {
+ StackTraceElement stackTraceElement = expectedStackTrace[i];
+ Assertions.assertEquals(stackTraceElement.getClassName(), classNames.get(i));
+ Assertions.assertEquals(stackTraceElement.getFileName(), fileNames.get(i));
+ Assertions.assertEquals(stackTraceElement.getLineNumber(), lineNumbers.get(i));
+ Assertions.assertEquals(stackTraceElement.getMethodName(), methodNames.get(i));
+ }
+ }
+
+
+ private ExceptionMetaData newRandomExceptionMetaData(Throwable throwable) {
+ List wrapperList = wrapperList(throwable);
+
+ return new ExceptionMetaData(
+ random.nextLong(),
+ "transactionId",
+ random.nextLong(),
+ random.nextLong(),
+ "applicationServiceType",
+ "applicationName",
+ "agentId",
+ "uriTemplate",
+ "errorClassName",
+ "errorMessage",
+ random.nextInt(),
+ wrapperList,
+ "stackTraceHash"
+ );
+ }
+
+ private List wrapperList(Throwable throwable) {
+ StackTraceElement[] stackTrace = throwable.getStackTrace();
+
+ return Arrays.stream(stackTrace).map(
+ (StackTraceElement s) -> new StackTraceElementWrapper(
+ s.getClassName(), s.getFileName(), s.getLineNumber(), s.getMethodName()
+ )
+ ).collect(Collectors.toList());
+ }
+
+}
\ 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 000000000000..3fa3a44a9f24
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/pom.xml
@@ -0,0 +1,49 @@
+
+
+
+ pinpoint-exceptiontrace-module
+ com.navercorp.pinpoint
+ 2.6.0-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
+
+
+ com.google.guava
+ guava
+
+
+ org.mapstruct
+ mapstruct
+
+
+
+
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/model/ExceptionMetaData.java b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/model/ExceptionMetaData.java
new file mode 100644
index 000000000000..34bd3571b632
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/model/ExceptionMetaData.java
@@ -0,0 +1,228 @@
+/*
+ * 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.util.StringPrecondition;
+import com.navercorp.pinpoint.exceptiontrace.common.util.HashUtils;
+
+
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionMetaData {
+
+ private long timestamp;
+
+ private String transactionId;
+ private long spanId;
+ private long exceptionId;
+
+ private String applicationServiceType;
+ private String applicationName;
+ private String agentId;
+ private String uriTemplate;
+
+ private String errorClassName;
+ private String errorMessage;
+ private int exceptionDepth;
+
+ private List stackTrace;
+
+ private String stackTraceHash;
+
+ public ExceptionMetaData() {
+ }
+
+ public ExceptionMetaData(
+ long timestamp,
+ String transactionId,
+ long spanId,
+ long exceptionId,
+ String applicationServiceType,
+ String applicationName,
+ String agentId,
+ String uriTemplate,
+ 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.uriTemplate = uriTemplate;
+ this.errorClassName = StringPrecondition.requireHasLength(errorClassName, "errorClassName");
+ this.errorMessage = StringPrecondition.requireHasLength(errorMessage, "errorMessage");
+ this.exceptionDepth = exceptionDepth;
+ this.stackTrace = stackTrace;
+ this.stackTraceHash = stackTraceHash;
+ }
+
+ public static ExceptionMetaData valueOf(
+ long timestamp, String transactionId, long spanId, long exceptionId,
+ String applicationServiceType, String applicationName, String agentId,
+ String uriTemplate,
+ String errorClassName, String errorMessage, int exceptionDepth,
+ List wrappers
+ ) {
+ return new ExceptionMetaData(
+ timestamp,
+ transactionId,
+ spanId,
+ exceptionId,
+ applicationServiceType,
+ applicationName,
+ agentId,
+ uriTemplate,
+ errorClassName,
+ errorMessage,
+ exceptionDepth,
+ wrappers,
+ HashUtils.objectsToHashString(wrappers, StackTraceElementWrapper.funnel())
+ );
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public String getTransactionId() {
+ return transactionId;
+ }
+
+ public void setTransactionId(String transactionId) {
+ this.transactionId = transactionId;
+ }
+
+ public long getSpanId() {
+ return spanId;
+ }
+
+ public void setSpanId(long spanId) {
+ this.spanId = spanId;
+ }
+
+ public long getExceptionId() {
+ return exceptionId;
+ }
+
+ public void setExceptionId(long exceptionId) {
+ this.exceptionId = exceptionId;
+ }
+
+ public String getApplicationServiceType() {
+ return applicationServiceType;
+ }
+
+ public void setApplicationServiceType(String applicationServiceType) {
+ this.applicationServiceType = applicationServiceType;
+ }
+
+ public String getApplicationName() {
+ return applicationName;
+ }
+
+ public void setApplicationName(String applicationName) {
+ this.applicationName = applicationName;
+ }
+
+ public String getAgentId() {
+ return agentId;
+ }
+
+ public void setAgentId(String agentId) {
+ this.agentId = agentId;
+ }
+
+ public String getUriTemplate() {
+ return uriTemplate;
+ }
+
+ public void setUriTemplate(String uriTemplate) {
+ this.uriTemplate = uriTemplate;
+ }
+
+ public String getErrorClassName() {
+ return errorClassName;
+ }
+
+ public void setErrorClassName(String errorClassName) {
+ this.errorClassName = errorClassName;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ public int getExceptionDepth() {
+ return exceptionDepth;
+ }
+
+ public void setExceptionDepth(int exceptionDepth) {
+ this.exceptionDepth = exceptionDepth;
+ }
+
+ public List getStackTrace() {
+ return stackTrace;
+ }
+
+ public void setStackTrace(List stackTrace) {
+ this.stackTrace = stackTrace;
+ }
+
+ public String getStackTraceHash() {
+ return stackTraceHash;
+ }
+
+ public void setStackTraceHash(String stackTraceHash) {
+ this.stackTraceHash = stackTraceHash;
+ }
+
+ @Override
+ public String toString() {
+ return "ExceptionMetaData{" +
+ "timestamp=" + timestamp +
+ ", transactionId='" + transactionId + '\'' +
+ ", spanId=" + spanId +
+ ", exceptionId=" + exceptionId +
+ ", applicationServiceType='" + applicationServiceType + '\'' +
+ ", applicationName='" + applicationName + '\'' +
+ ", agentId='" + agentId + '\'' +
+ ", uriTemplate='" + uriTemplate + '\'' +
+ ", 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 000000000000..f42ffaad68f9
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/model/StackTraceElementWrapper.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.google.common.hash.Funnel;
+
+import java.nio.charset.StandardCharsets;
+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 static Funnel funnel() {
+ return (wrapper, into) -> into
+ .putString(wrapper.className, StandardCharsets.UTF_8)
+ .putString(wrapper.fileName, StandardCharsets.UTF_8)
+ .putInt(wrapper.lineNumber)
+ .putString(wrapper.methodName, StandardCharsets.UTF_8);
+ }
+
+ 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 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/pinot/PinotColumns.java b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/pinot/PinotColumns.java
new file mode 100644
index 000000000000..de7a45786a74
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/pinot/PinotColumns.java
@@ -0,0 +1,47 @@
+/*
+ * 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.pinot;
+
+/**
+ * @author intr3p1d
+ */
+public enum PinotColumns {
+ TRANSACTION_ID("transactionId"),
+ SPAN_ID("spanId"),
+ EXCEPTION_ID("exceptionId"),
+ APPLICATION_SERVICE_TYPE("applicationServiceType"),
+ APPLICATION_NAME("applicationName"),
+ AGENT_ID("agentId"),
+ URI_TEMPLATE("uriTemplate"),
+ ERROR_CLASS_NAME("errorClassName"),
+ ERROR_MESSAGE("errorMessage"),
+ EXCEPTION_DEPTH("exceptionDepth"),
+ STACK_TRACE_CLASS_NAME("stackTraceClassName"),
+ STACK_TRACE_FILE_NAME("stackTraceFileName"),
+ STACK_TRACE_LINE_NUMBER("stackTraceLineNumber"),
+ STACK_TRACE_METHOD_NAME("stackTraceMethodName"),
+ STACK_TRACE_HASH("stackTraceHash");
+
+ private final String name;
+
+ PinotColumns(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
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 000000000000..7ca07a3f4558
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/java/com/navercorp/pinpoint/exceptiontrace/common/util/HashUtils.java
@@ -0,0 +1,53 @@
+/*
+ * 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.google.common.hash.Funnel;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+
+/**
+ * @author intr3p1d
+ */
+public final class HashUtils {
+
+ private HashUtils() {
+ }
+
+ private static final HashFunction HASH = Hashing.murmur3_128();
+
+ public static Hasher newHasher() {
+ return HASH.newHasher();
+ }
+
+ public static String objectsToHashString(Iterable objects, Funnel funnel) {
+ return objectsToHashCode(objects, funnel).toString();
+ }
+
+ public static HashCode objectsToHashCode(Iterable objects, Funnel funnel) {
+ Hasher hc = newHasher();
+ for (T element: objects) {
+ funnel.funnel(element, hc);
+ }
+ return hc.hash();
+ }
+
+
+
+}
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 000000000000..dc15e3b96e40
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-common/src/main/pinot/pinot-exceptionTrace-schema.json
@@ -0,0 +1,77 @@
+{
+ "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": "uriTemplate",
+ "dataType": "STRING"
+ },
+ {
+ "name": "errorClassName",
+ "dataType": "STRING"
+ },
+ {
+ "name": "errorMessage",
+ "dataType": "STRING"
+ },
+ {
+ "name": "exceptionDepth",
+ "dataType": "INT"
+ },
+ {
+ "name": "stackTraceClassName",
+ "dataType": "STRING",
+ "singleValueField": false
+ },
+ {
+ "name": "stackTraceFileName",
+ "dataType": "STRING",
+ "singleValueField": false
+ },
+ {
+ "name": "stackTraceLineNumber",
+ "dataType": "INT",
+ "singleValueField": false
+ },
+ {
+ "name": "stackTraceMethodName",
+ "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 000000000000..77d203ccc828
--- /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 000000000000..edffb42b2f68
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/pom.xml
@@ -0,0 +1,62 @@
+
+
+
+ pinpoint-exceptiontrace-module
+ com.navercorp.pinpoint
+ 2.6.0-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
+
+
+ org.mapstruct
+ mapstruct
+
+
+ org.springframework
+ spring-test
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+
+
+
+
+
+
+
\ 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 000000000000..e0cc56f8b9ed
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/ExceptionTraceWebConfig.java
@@ -0,0 +1,45 @@
+/*
+ * 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.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * @author intr3p1d
+ */
+@Configuration
+@ComponentScan({
+ "com.navercorp.pinpoint.common.server.mapper",
+ "com.navercorp.pinpoint.exceptiontrace.web.config",
+ "com.navercorp.pinpoint.exceptiontrace.web.controller",
+ "com.navercorp.pinpoint.exceptiontrace.web.dao",
+ "com.navercorp.pinpoint.exceptiontrace.web.mapper",
+ "com.navercorp.pinpoint.exceptiontrace.web.service",
+})
+@Import({
+ ExceptionTraceWebPropertySources.class,
+ ExceptionTracePinotDaoConfiguration.class,
+ PinotConfiguration.class
+})
+@ConditionalOnProperty(name = "pinpoint.modules.web.exceptiontrace.enabled", havingValue = "true")
+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 000000000000..40d98b7fc971
--- /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 000000000000..9bbe03d38261
--- /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:exceptiontrace/mapper/*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 000000000000..64d822251ade
--- /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.web.entity.ExceptionMetaDataEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionTraceSummaryEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionTraceValueViewEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.GroupedFieldNameEntity;
+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("ExceptionMetaDataEntity", ExceptionMetaDataEntity.class);
+ typeAliasRegistry.registerAlias("GroupedFieldNameEntity", GroupedFieldNameEntity.class);
+ typeAliasRegistry.registerAlias("ExceptionTraceSummaryEntity", ExceptionTraceSummaryEntity.class);
+ typeAliasRegistry.registerAlias("ExceptionTraceValueViewEntity", ExceptionTraceValueViewEntity.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 000000000000..934b7dd62ccd
--- /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.ExceptionMetaData;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceValueView;
+import com.navercorp.pinpoint.exceptiontrace.web.model.GroupByAttributes;
+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.validation.annotation.Validated;
+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 javax.validation.constraints.NotBlank;
+import javax.validation.constraints.PositiveOrZero;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * @author intr3p1d
+ */
+@RestController
+@RequestMapping(value = "/errors")
+@Validated
+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 getListOfExceptionMetaDataFromTransactionId(
+ @RequestParam("applicationName") @NotBlank String applicationName,
+ @RequestParam("agentId") @NotBlank String agentId,
+ @RequestParam("traceId") @NotBlank 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 getListOfExceptionMetaDataByGivenRange(
+ @RequestParam("applicationName") @NotBlank String applicationName,
+ @RequestParam(value = "agentId", required = false) String agentId,
+ @RequestParam("from") @PositiveOrZero long from,
+ @RequestParam("to") @PositiveOrZero long to
+ ) {
+ ExceptionTraceQueryParameter queryParameter = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setRange(Range.newRange(from, to))
+ .setTimePrecision(DETAILED_TIME_PRECISION)
+ .build();
+ return exceptionTraceService.getSummarizedExceptionsInRange(
+ queryParameter
+ );
+ }
+
+ @GetMapping("/errorList/groupBy")
+ public List getListOfExceptionMetaDataWithDynamicGroupBy(
+ @RequestParam("applicationName") @NotBlank String applicationName,
+ @RequestParam(value = "agentId", required = false) String agentId,
+ @RequestParam("from") @PositiveOrZero long from,
+ @RequestParam("to") @PositiveOrZero long to,
+
+ @RequestParam("groupBy") List groupByList
+ ) {
+ List groupByAttributes = groupByList.stream().map(
+ GroupByAttributes::valueOf
+ ).distinct().sorted().collect(Collectors.toList());
+
+ logger.info(groupByAttributes);
+
+ ExceptionTraceQueryParameter queryParameter = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setRange(Range.newRange(from, to))
+ .setTimePrecision(DETAILED_TIME_PRECISION)
+ .addAllGroupBies(groupByAttributes)
+ .build();
+
+ return exceptionTraceService.getSummaries(
+ queryParameter
+ );
+ }
+
+ @GetMapping("/chart")
+ public ExceptionTraceView getCollectedExceptionMetaDataByGivenRange(
+ @RequestParam("applicationName") @NotBlank String applicationName,
+ @RequestParam(value = "agentId", required = false) String agentId,
+ @RequestParam("from") @PositiveOrZero long from,
+ @RequestParam("to") @PositiveOrZero long to
+ ) {
+
+ TimeWindow timeWindow = new TimeWindow(Range.newRange(from, to), DEFAULT_TIME_WINDOW_SAMPLER);
+ ExceptionTraceQueryParameter queryParameter = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setRange(timeWindow.getWindowRange())
+ .setTimePrecision(TimePrecision.newTimePrecision(TimeUnit.MILLISECONDS, (int) timeWindow.getWindowSlotSize()))
+ .setTimeWindowRangeCount(timeWindow.getWindowRangeCount())
+ .build();
+ List exceptionTraceValueViews = exceptionTraceService.getValueViews(
+ queryParameter
+ );
+ return ExceptionTraceView.newViewFromValueViews("total error occurs", timeWindow, exceptionTraceValueViews);
+ }
+
+ @GetMapping("/chart/groupBy")
+ public ExceptionTraceView getCollectedExceptionMetaDataByGivenRange(
+ @RequestParam("applicationName") @NotBlank String applicationName,
+ @RequestParam(value = "agentId", required = false) String agentId,
+ @RequestParam("from") @PositiveOrZero long from,
+ @RequestParam("to") @PositiveOrZero long to,
+
+ @RequestParam("groupBy") List groupByList
+ ) {
+ List groupByAttributes = groupByList.stream().map(
+ GroupByAttributes::valueOf
+ ).distinct().sorted().collect(Collectors.toList());
+ TimeWindow timeWindow = new TimeWindow(Range.newRange(from, to), DEFAULT_TIME_WINDOW_SAMPLER);
+ ExceptionTraceQueryParameter queryParameter = new ExceptionTraceQueryParameter.Builder()
+ .setApplicationName(applicationName)
+ .setAgentId(agentId)
+ .setRange(timeWindow.getWindowRange())
+ .setTimePrecision(TimePrecision.newTimePrecision(TimeUnit.MILLISECONDS, (int) timeWindow.getWindowSlotSize()))
+ .setTimeWindowRangeCount(timeWindow.getWindowRangeCount())
+ .addAllGroupBies(groupByAttributes)
+ .build();
+ List exceptionTraceValueViews = exceptionTraceService.getValueViews(
+ queryParameter
+ );
+ return ExceptionTraceView.newViewFromValueViews("top5 error occurs", timeWindow, exceptionTraceValueViews);
+ }
+
+}
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 000000000000..85c0dcf20ef8
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/dao/ExceptionTraceDao.java
@@ -0,0 +1,36 @@
+/*
+ * 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.ExceptionMetaData;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceValueView;
+import com.navercorp.pinpoint.exceptiontrace.web.util.ExceptionTraceQueryParameter;
+
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public interface ExceptionTraceDao {
+ List getExceptions(ExceptionTraceQueryParameter exceptionTraceQueryParameter);
+ List getSummarizedExceptions(ExceptionTraceQueryParameter exceptionTraceQueryParameter);
+ ExceptionMetaData getException(ExceptionTraceQueryParameter exceptionTraceQueryParameter);
+ List getSummaries(ExceptionTraceQueryParameter exceptionTraceQueryParameter);
+ List getValueViews(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 000000000000..ab26ce168ad0
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/dao/PinotExceptionTraceDao.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.web.dao;
+
+import com.navercorp.pinpoint.exceptiontrace.common.model.ExceptionMetaData;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionMetaDataEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionTraceSummaryEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionTraceValueViewEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.mapper.ExceptionMetaDataEntityMapper;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceValueView;
+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;
+import java.util.stream.Collectors;
+
+/**
+ * @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_SUMMARIZED_QUERY = "selectSummarizedExceptions";
+ private static final String SELECT_EXACT_QUERY = "selectExactException";
+ private static final String SELECT_SUMMARIES_QUERY = "selectSummaries";
+ private static final String SELECT_VALUEVIEWS_QUERY = "selectValueViews";
+
+ private final SqlSessionTemplate sqlPinotSessionTemplate;
+
+ private final ExceptionMetaDataEntityMapper mapper;
+
+ public PinotExceptionTraceDao(
+ @Qualifier("exceptionTracePinotSessionTemplate") SqlSessionTemplate sqlPinotSessionTemplate,
+ ExceptionMetaDataEntityMapper mapper
+ ) {
+ this.sqlPinotSessionTemplate = Objects.requireNonNull(sqlPinotSessionTemplate, "sqlPinotSessionTemplate");
+ this.mapper = Objects.requireNonNull(mapper, "mapper");
+ }
+
+ @Override
+ public List getExceptions(ExceptionTraceQueryParameter exceptionTraceQueryParameter) {
+ List dataEntities = this.sqlPinotSessionTemplate.selectList(NAMESPACE + SELECT_QUERY, exceptionTraceQueryParameter);
+ return dataEntities.stream()
+ .map(mapper::toModel)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List getSummarizedExceptions(ExceptionTraceQueryParameter exceptionTraceQueryParameter) {
+ List dataEntities = this.sqlPinotSessionTemplate.selectList(NAMESPACE + SELECT_SUMMARIZED_QUERY, exceptionTraceQueryParameter);
+ return dataEntities.stream()
+ .map(mapper::toModel)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public ExceptionMetaData getException(ExceptionTraceQueryParameter exceptionTraceQueryParameter) {
+ ExceptionMetaDataEntity entity = this.sqlPinotSessionTemplate.selectOne(NAMESPACE + SELECT_EXACT_QUERY, exceptionTraceQueryParameter);
+ return mapper.toModel(entity);
+ }
+
+ @Override
+ public List getSummaries(ExceptionTraceQueryParameter exceptionTraceQueryParameter) {
+ List entities = this.sqlPinotSessionTemplate.selectList(NAMESPACE + SELECT_SUMMARIES_QUERY, exceptionTraceQueryParameter);
+ return entities.stream()
+ .map(mapper::entityToExceptionTraceSummary)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List getValueViews(ExceptionTraceQueryParameter exceptionTraceQueryParameter) {
+ List valueViewEntities = this.sqlPinotSessionTemplate.selectList(NAMESPACE + SELECT_VALUEVIEWS_QUERY, exceptionTraceQueryParameter);
+ return valueViewEntities.stream()
+ .map(mapper::entityToExceptionTraceValueView)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/ExceptionMetaDataEntity.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/ExceptionMetaDataEntity.java
new file mode 100644
index 000000000000..ecc3d7191375
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/ExceptionMetaDataEntity.java
@@ -0,0 +1,174 @@
+/*
+ * 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.entity;
+
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionMetaDataEntity {
+ private long timestamp;
+
+ private String transactionId;
+ private long spanId;
+ private long exceptionId;
+
+ private String applicationServiceType;
+ private String applicationName;
+ private String agentId;
+ private String uriTemplate;
+
+ private String errorClassName;
+ private String errorMessage;
+ private int exceptionDepth;
+
+ private String stackTraceClassName;
+ private String stackTraceFileName;
+ private String stackTraceLineNumber;
+ private String stackTraceMethodName;
+ private String stackTraceHash;
+
+ public ExceptionMetaDataEntity() {
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public String getTransactionId() {
+ return transactionId;
+ }
+
+ public void setTransactionId(String transactionId) {
+ this.transactionId = transactionId;
+ }
+
+ public long getSpanId() {
+ return spanId;
+ }
+
+ public void setSpanId(long spanId) {
+ this.spanId = spanId;
+ }
+
+ public long getExceptionId() {
+ return exceptionId;
+ }
+
+ public void setExceptionId(long exceptionId) {
+ this.exceptionId = exceptionId;
+ }
+
+ public String getApplicationServiceType() {
+ return applicationServiceType;
+ }
+
+ public void setApplicationServiceType(String applicationServiceType) {
+ this.applicationServiceType = applicationServiceType;
+ }
+
+ public String getApplicationName() {
+ return applicationName;
+ }
+
+ public void setApplicationName(String applicationName) {
+ this.applicationName = applicationName;
+ }
+
+ public String getAgentId() {
+ return agentId;
+ }
+
+ public void setAgentId(String agentId) {
+ this.agentId = agentId;
+ }
+
+ public String getUriTemplate() {
+ return uriTemplate;
+ }
+
+ public void setUriTemplate(String uriTemplate) {
+ this.uriTemplate = uriTemplate;
+ }
+
+ public String getErrorClassName() {
+ return errorClassName;
+ }
+
+ public void setErrorClassName(String errorClassName) {
+ this.errorClassName = errorClassName;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ public int getExceptionDepth() {
+ return exceptionDepth;
+ }
+
+ public void setExceptionDepth(int exceptionDepth) {
+ this.exceptionDepth = exceptionDepth;
+ }
+
+ public String getStackTraceClassName() {
+ return stackTraceClassName;
+ }
+
+ public void setStackTraceClassName(String stackTraceClassName) {
+ this.stackTraceClassName = stackTraceClassName;
+ }
+
+ public String getStackTraceFileName() {
+ return stackTraceFileName;
+ }
+
+ public void setStackTraceFileName(String stackTraceFileName) {
+ this.stackTraceFileName = stackTraceFileName;
+ }
+
+ public String getStackTraceLineNumber() {
+ return stackTraceLineNumber;
+ }
+
+ public void setStackTraceLineNumber(String stackTraceLineNumber) {
+ this.stackTraceLineNumber = stackTraceLineNumber;
+ }
+
+ public String getStackTraceMethodName() {
+ return stackTraceMethodName;
+ }
+
+ public void setStackTraceMethodName(String stackTraceMethodName) {
+ this.stackTraceMethodName = stackTraceMethodName;
+ }
+
+ public String getStackTraceHash() {
+ return stackTraceHash;
+ }
+
+ public void setStackTraceHash(String stackTraceHash) {
+ this.stackTraceHash = stackTraceHash;
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/ExceptionTraceSummaryEntity.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/ExceptionTraceSummaryEntity.java
new file mode 100644
index 000000000000..2292d29a2f10
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/ExceptionTraceSummaryEntity.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.entity;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceSummaryEntity extends GroupedFieldNameEntity {
+ private String mostRecentErrorClass;
+ private String mostRecentErrorMessage;
+ private long count;
+ private long firstOccurred;
+ private long lastOccurred;
+
+ public ExceptionTraceSummaryEntity() {
+ }
+
+ public String getMostRecentErrorClass() {
+ return mostRecentErrorClass;
+ }
+
+ public void setMostRecentErrorClass(String mostRecentErrorClass) {
+ this.mostRecentErrorClass = mostRecentErrorClass;
+ }
+
+ public String getMostRecentErrorMessage() {
+ return mostRecentErrorMessage;
+ }
+
+ public void setMostRecentErrorMessage(String mostRecentErrorMessage) {
+ this.mostRecentErrorMessage = mostRecentErrorMessage;
+ }
+
+ public long getCount() {
+ return count;
+ }
+
+ public void setCount(long count) {
+ this.count = count;
+ }
+
+ public long getFirstOccurred() {
+ return firstOccurred;
+ }
+
+ public void setFirstOccurred(long firstOccurred) {
+ this.firstOccurred = firstOccurred;
+ }
+
+ public long getLastOccurred() {
+ return lastOccurred;
+ }
+
+ public void setLastOccurred(long lastOccurred) {
+ this.lastOccurred = lastOccurred;
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/ExceptionTraceValueViewEntity.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/ExceptionTraceValueViewEntity.java
new file mode 100644
index 000000000000..2418d2ced7d5
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/ExceptionTraceValueViewEntity.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.entity;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceValueViewEntity extends GroupedFieldNameEntity {
+ private String values;
+
+ public ExceptionTraceValueViewEntity() {
+ }
+
+ public String getValues() {
+ return values;
+ }
+
+ public void setValues(String values) {
+ this.values = values;
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/GroupedFieldNameEntity.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/GroupedFieldNameEntity.java
new file mode 100644
index 000000000000..b2d7f329e2cb
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/entity/GroupedFieldNameEntity.java
@@ -0,0 +1,61 @@
+/*
+ * 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.entity;
+
+/**
+ * @author intr3p1d
+ */
+public class GroupedFieldNameEntity {
+ private String uriTemplate;
+ private String errorClassName;
+ private String errorMessage;
+ private String stackTraceHash;
+
+ public GroupedFieldNameEntity() {
+ }
+
+ public String getUriTemplate() {
+ return uriTemplate;
+ }
+
+ public void setUriTemplate(String uriTemplate) {
+ this.uriTemplate = uriTemplate;
+ }
+
+ public String getErrorClassName() {
+ return errorClassName;
+ }
+
+ public void setErrorClassName(String errorClassName) {
+ this.errorClassName = errorClassName;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ public String getStackTraceHash() {
+ return stackTraceHash;
+ }
+
+ public void setStackTraceHash(String stackTraceHash) {
+ this.stackTraceHash = stackTraceHash;
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/ExceptionMetaDataEntityMapper.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/ExceptionMetaDataEntityMapper.java
new file mode 100644
index 000000000000..a34f2dcbf966
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/ExceptionMetaDataEntityMapper.java
@@ -0,0 +1,57 @@
+/*
+ * 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.navercorp.pinpoint.common.server.mapper.MapStructUtils;
+import com.navercorp.pinpoint.exceptiontrace.common.model.ExceptionMetaData;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionMetaDataEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionTraceSummaryEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionTraceValueViewEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceValueView;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Mappings;
+
+/**
+ * @author intr3p1d
+ */
+@Mapper(componentModel = "spring", uses = {StackTraceMapper.class, MapStructUtils.class})
+public interface ExceptionMetaDataEntityMapper {
+
+ @Mappings(
+ @Mapping(source = ".", target = "stackTrace", qualifiedBy = StackTraceMapper.StringsToStackTrace.class)
+ )
+ ExceptionMetaData toModel(ExceptionMetaDataEntity entity);
+
+ @Mappings({
+ @Mapping(source = "values", target = "values", qualifiedBy = MapStructUtils.JsonStrToList.class),
+ @Mapping(source = "uriTemplate", target = "groupedFieldName.uriTemplate"),
+ @Mapping(source = "errorClassName", target = "groupedFieldName.errorClassName"),
+ @Mapping(source = "errorMessage", target = "groupedFieldName.errorMessage"),
+ @Mapping(source = "stackTraceHash", target = "groupedFieldName.stackTraceHash")
+ })
+ ExceptionTraceValueView entityToExceptionTraceValueView(ExceptionTraceValueViewEntity entity);
+
+ @Mappings({
+ @Mapping(source = "uriTemplate", target = "groupedFieldName.uriTemplate"),
+ @Mapping(source = "errorClassName", target = "groupedFieldName.errorClassName"),
+ @Mapping(source = "errorMessage", target = "groupedFieldName.errorMessage"),
+ @Mapping(source = "stackTraceHash", target = "groupedFieldName.stackTraceHash")
+ })
+ ExceptionTraceSummary entityToExceptionTraceSummary(ExceptionTraceSummaryEntity entity);
+
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/StackTraceMapper.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/StackTraceMapper.java
new file mode 100644
index 000000000000..094dd188d2e1
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/StackTraceMapper.java
@@ -0,0 +1,71 @@
+/*
+ * 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.navercorp.pinpoint.common.server.mapper.MapStructUtils;
+import com.navercorp.pinpoint.exceptiontrace.common.model.StackTraceElementWrapper;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionMetaDataEntity;
+import org.mapstruct.Qualifier;
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author intr3p1d
+ */
+@Component
+public class StackTraceMapper {
+ private final MapStructUtils mapStructUtils;
+
+ public StackTraceMapper(MapStructUtils mapStructUtils) {
+ this.mapStructUtils = Objects.requireNonNull(mapStructUtils, "mapStructUtils");
+ }
+
+ @Qualifier
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.CLASS)
+ public @interface StringsToStackTrace {
+
+ }
+
+ @StringsToStackTrace
+ public List stackTrace(ExceptionMetaDataEntity entity) {
+ List classNameIterable = mapStructUtils.jsonStrToList(entity.getStackTraceClassName());
+ List fileNameIterable = mapStructUtils.jsonStrToList(entity.getStackTraceFileName());
+ List lineNumberIterable = mapStructUtils.jsonStrToList(entity.getStackTraceLineNumber());
+ List methodNameIterable = mapStructUtils.jsonStrToList(entity.getStackTraceMethodName());
+
+ List wrappers = new ArrayList<>();
+ for (int i = 0; i < classNameIterable.size(); i++) {
+ wrappers.add(
+ new StackTraceElementWrapper(
+ classNameIterable.get(i),
+ fileNameIterable.get(i),
+ lineNumberIterable.get(i),
+ methodNameIterable.get(i)
+ )
+ );
+ }
+ return wrappers;
+ }
+
+}
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 000000000000..1085ed8eacc8
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceGroup.java
@@ -0,0 +1,67 @@
+/*
+ * 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.navercorp.pinpoint.metric.web.view.TimeSeriesValueView;
+import com.navercorp.pinpoint.metric.web.view.TimeseriesChartType;
+import com.navercorp.pinpoint.metric.web.view.TimeseriesValueGroupView;
+
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceGroup implements TimeseriesValueGroupView {
+
+ private static final TimeseriesChartType CHART_TYPE = TimeseriesChartType.bar;
+ private static final String UNIT = "COUNT";
+ private final String groupName;
+ private final List values;
+
+ private ExceptionTraceGroup(String groupName, List values) {
+ this.groupName = groupName;
+ this.values = values;
+ }
+
+ public static ExceptionTraceGroup newGroupFromValueViews(
+ String groupName,
+ List exceptionTraceValueViews
+ ) {
+ List list = (List) (List extends TimeSeriesValueView>) exceptionTraceValueViews;
+ return new ExceptionTraceGroup(groupName, list);
+ }
+
+ @Override
+ public String getGroupName() {
+ return groupName;
+ }
+
+ @Override
+ public List getMetricValues() {
+ return values;
+ }
+
+ @Override
+ public TimeseriesChartType getChartType() {
+ return CHART_TYPE;
+ }
+
+ @Override
+ public String getUnit() {
+ return UNIT;
+ }
+}
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 000000000000..9bb23b57fb0d
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceSummary.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.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceSummary {
+
+ private GroupedFieldName groupedFieldName;
+ private String mostRecentErrorClass;
+ private String mostRecentErrorMessage;
+ private long count;
+ private long firstOccurred;
+ private long lastOccurred;
+
+ public ExceptionTraceSummary() {
+ }
+
+ @JsonProperty("fieldName")
+ public GroupedFieldName getGroupedFieldName() {
+ return groupedFieldName;
+ }
+
+ public void setGroupedFieldName(GroupedFieldName groupedFieldName) {
+ this.groupedFieldName = groupedFieldName;
+ }
+
+ public String getMostRecentErrorClass() {
+ return mostRecentErrorClass;
+ }
+
+ public void setMostRecentErrorClass(String mostRecentErrorClass) {
+ this.mostRecentErrorClass = mostRecentErrorClass;
+ }
+
+ public String getMostRecentErrorMessage() {
+ return mostRecentErrorMessage;
+ }
+
+ public void setMostRecentErrorMessage(String mostRecentErrorMessage) {
+ this.mostRecentErrorMessage = mostRecentErrorMessage;
+ }
+
+ public long getCount() {
+ return count;
+ }
+
+ public void setCount(long count) {
+ this.count = count;
+ }
+
+ public long getFirstOccurred() {
+ return firstOccurred;
+ }
+
+ public void setFirstOccurred(long firstOccurred) {
+ this.firstOccurred = firstOccurred;
+ }
+
+ public long getLastOccurred() {
+ return lastOccurred;
+ }
+
+ public void setLastOccurred(long lastOccurred) {
+ this.lastOccurred = lastOccurred;
+ }
+
+ @Override
+ public String toString() {
+ return "ExceptionTraceSummary{" +
+ "groupedFieldName=" + groupedFieldName +
+ ", mostRecentErrorClass='" + mostRecentErrorClass + '\'' +
+ ", mostRecentErrorMessage='" + mostRecentErrorMessage + '\'' +
+ ", count=" + count +
+ ", firstOccurred=" + firstOccurred +
+ ", lastOccurred=" + lastOccurred +
+ '}';
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceValueView.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceValueView.java
new file mode 100644
index 000000000000..982638c17514
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceValueView.java
@@ -0,0 +1,76 @@
+/*
+ * 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.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.navercorp.pinpoint.common.util.StringUtils;
+import com.navercorp.pinpoint.metric.web.view.TimeSeriesValueView;
+
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceValueView implements TimeSeriesValueView {
+
+ private static final String TOTAL_FIELDNAME = "total";
+ private GroupedFieldName groupedFieldName;
+ private List values;
+
+ public ExceptionTraceValueView() {
+ }
+
+ public ExceptionTraceValueView(List values) {
+ this.values = values;
+ }
+
+ @Override
+ public String getFieldName() {
+ return groupedFieldName != null ? StringUtils.defaultString(groupedFieldName.inAString(), TOTAL_FIELDNAME) : TOTAL_FIELDNAME;
+ }
+
+ @JsonIgnore
+ public GroupedFieldName getGroupedFieldName() {
+ return groupedFieldName;
+ }
+
+ public void setGroupedFieldName(GroupedFieldName groupedFieldName) {
+ this.groupedFieldName = groupedFieldName;
+ }
+
+ @Override
+ public List getValues() {
+ return values;
+ }
+
+ public void setValues(List values) {
+ this.values = values;
+ }
+
+ @Override
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public List getTags() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "ExceptionTraceValueView{" +
+ "values=" + values +
+ '}';
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/GroupByAttributes.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/GroupByAttributes.java
new file mode 100644
index 000000000000..ae50cdea7984
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/GroupByAttributes.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.web.model;
+
+import com.navercorp.pinpoint.exceptiontrace.common.pinot.PinotColumns;
+
+/**
+ * @author intr3p1d
+ */
+public enum GroupByAttributes {
+ URI_TEMPLATE(PinotColumns.URI_TEMPLATE),
+ ERROR_CLASS_NAME(PinotColumns.ERROR_CLASS_NAME),
+ ERROR_MESSAGE(PinotColumns.ERROR_MESSAGE),
+ STACK_TRACE(PinotColumns.STACK_TRACE_HASH);
+
+ private final PinotColumns column;
+
+ GroupByAttributes(PinotColumns column) {
+ this.column = column;
+ }
+
+ public String getAttributeName() {
+ return column.getName();
+ }
+}
diff --git a/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/GroupedFieldName.java b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/GroupedFieldName.java
new file mode 100644
index 000000000000..2d6bc0a436a5
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/model/GroupedFieldName.java
@@ -0,0 +1,85 @@
+/*
+ * 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.common.util.StringUtils;
+
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author intr3p1d
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class GroupedFieldName {
+
+ private String uriTemplate;
+ private String errorClassName;
+ private String errorMessage;
+ private String stackTraceHash;
+
+ public GroupedFieldName() {
+ }
+
+ public String inAString() {
+ return Stream.of(uriTemplate, errorClassName, errorMessage, stackTraceHash)
+ .filter(StringUtils::hasLength)
+ .collect(Collectors.joining(", "));
+ }
+
+ public String getUriTemplate() {
+ return uriTemplate;
+ }
+
+ public void setUriTemplate(String uriTemplate) {
+ this.uriTemplate = uriTemplate;
+ }
+
+ public String getErrorClassName() {
+ return errorClassName;
+ }
+
+ public void setErrorClassName(String errorClassName) {
+ this.errorClassName = errorClassName;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ public String getStackTraceHash() {
+ return stackTraceHash;
+ }
+
+ public void setStackTraceHash(String stackTraceHash) {
+ this.stackTraceHash = stackTraceHash;
+ }
+
+ @Override
+ public String toString() {
+ return "GroupedFieldName{" +
+ "uriTemplate='" + uriTemplate + '\'' +
+ ", errorClassName='" + errorClassName + '\'' +
+ ", errorMessage='" + errorMessage + '\'' +
+ ", stackTraceHash='" + stackTraceHash + '\'' +
+ '}';
+ }
+}
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 000000000000..205390cb5d20
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/service/ExceptionTraceService.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.web.service;
+
+import com.navercorp.pinpoint.exceptiontrace.common.model.ExceptionMetaData;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceValueView;
+import com.navercorp.pinpoint.exceptiontrace.web.util.ExceptionTraceQueryParameter;
+
+import java.util.List;
+
+/**
+ * @author intr3p1d
+ */
+public interface ExceptionTraceService {
+
+ List getTransactionExceptions(ExceptionTraceQueryParameter queryParameter);
+
+ List getSummarizedExceptionsInRange(ExceptionTraceQueryParameter queryParameter);
+
+ List getSummaries(ExceptionTraceQueryParameter queryParameter);
+
+ List getValueViews(ExceptionTraceQueryParameter queryParameter);
+}
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 000000000000..0c373961c2eb
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/service/ExceptionTraceServiceImpl.java
@@ -0,0 +1,102 @@
+/*
+ * 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.ExceptionMetaData;
+import com.navercorp.pinpoint.exceptiontrace.web.dao.ExceptionTraceDao;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceValueView;
+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.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 final ExceptionTraceDao exceptionTraceDao;
+
+ public ExceptionTraceServiceImpl(ExceptionTraceDao exceptionTraceDao) {
+ this.exceptionTraceDao = Objects.requireNonNull(exceptionTraceDao, "exceptionTraceDao");
+ }
+
+ @Override
+ public List getTransactionExceptions(
+ ExceptionTraceQueryParameter queryParameter
+ ) {
+ return applyQueryFunction(
+ queryParameter,
+ this::getExeptionMetaDataList
+ );
+ }
+
+ @Override
+ public List getSummarizedExceptionsInRange(ExceptionTraceQueryParameter queryParameter) {
+ return applyQueryFunction(
+ queryParameter,
+ this::getSummarizedExeptionMetaDataList
+ );
+ }
+
+ @Override
+ public List getSummaries(ExceptionTraceQueryParameter queryParameter) {
+ return applyQueryFunction(
+ queryParameter,
+ this::getExceptionTraceSummaries
+ );
+ }
+
+ @Override
+ public List getValueViews(ExceptionTraceQueryParameter queryParameter) {
+ return applyQueryFunction(
+ queryParameter,
+ this::getExceptionTraceValueViews
+ );
+ }
+
+ private List applyQueryFunction(
+ ExceptionTraceQueryParameter queryParameter,
+ Function> queryFunction
+ ) {
+ return queryFunction.apply(queryParameter);
+ }
+
+ private List getExeptionMetaDataList(ExceptionTraceQueryParameter queryParameter) {
+ return exceptionTraceDao.getExceptions(queryParameter);
+ }
+
+ private List getSummarizedExeptionMetaDataList(ExceptionTraceQueryParameter queryParameter) {
+ return exceptionTraceDao.getSummarizedExceptions(queryParameter);
+ }
+
+ private List getExceptionTraceSummaries(ExceptionTraceQueryParameter queryParameter) {
+ return exceptionTraceDao.getSummaries(queryParameter);
+ }
+
+ private List getExceptionTraceValueViews(ExceptionTraceQueryParameter queryParameter) {
+ return exceptionTraceDao.getValueViews(queryParameter);
+ }
+}
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 000000000000..6785fc199a37
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/util/ExceptionTraceQueryParameter.java
@@ -0,0 +1,156 @@
+/*
+ * 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.web.model.GroupByAttributes;
+import com.navercorp.pinpoint.metric.web.util.QueryParameter;
+import com.navercorp.pinpoint.metric.web.util.TimePrecision;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * @author intr3p1d
+ */
+public class ExceptionTraceQueryParameter extends QueryParameter {
+
+ public static final int STACKTRACE_COUNT = 3;
+ private final static String TOTAL_FIELD_NAME = "total";
+
+ private final String applicationName;
+ private final String agentId;
+
+ private final String transactionId;
+ private final long spanId;
+ private final long exceptionId;
+ private final int exceptionDepth;
+
+ private final List groupByAttributes;
+
+ private final long timeWindowRangeCount;
+
+ protected ExceptionTraceQueryParameter(Builder builder) {
+ super(builder.getRange(), builder.getTimePrecision(), builder.getLimit());
+ this.applicationName = builder.applicationName;
+ this.agentId = builder.agentId;
+ this.transactionId = builder.transactionId;
+ this.spanId = builder.spanId;
+ this.exceptionId = builder.exceptionId;
+ this.exceptionDepth = builder.exceptionDepth;
+ this.groupByAttributes = builder.groupByAttributes;
+ this.timeWindowRangeCount = builder.timeWindowRangeCount;
+ }
+
+ public static class Builder extends QueryParameter.Builder {
+ private static final int LIMIT = 65536;
+ private String applicationName;
+ private String agentId = null;
+
+
+ private String transactionId = null;
+ private long spanId = Long.MIN_VALUE;
+ private long exceptionId = Long.MIN_VALUE;
+ private int exceptionDepth = Integer.MIN_VALUE;
+
+ private final List groupByAttributes = new ArrayList<>();
+
+ private long timeWindowRangeCount = 0;
+
+ @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 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();
+ }
+
+ public Builder setTimeWindowRangeCount(long timeWindowRangeCount) {
+ this.timeWindowRangeCount = timeWindowRangeCount;
+ return self();
+ }
+
+ public Builder addAllGroupBies(Collection summaryGroupBIES) {
+ List attributes = summaryGroupBIES.stream().map(GroupByAttributes::getAttributeName).collect(Collectors.toList());
+
+ this.groupByAttributes.addAll(
+ summaryGroupBIES
+ );
+ return self();
+ }
+
+ public long estimateLimit() {
+ if (this.range != null) {
+ return (range.getRange() / Math.max(timePrecision.getInterval(), 30000) + 1);
+ } else {
+ return LIMIT;
+ }
+ }
+
+ @Override
+ public ExceptionTraceQueryParameter build() {
+ if (timePrecision == null) {
+ this.timePrecision = TimePrecision.newTimePrecision(TimeUnit.MILLISECONDS, 30000);
+ }
+ this.limit = this.estimateLimit();
+ return new ExceptionTraceQueryParameter(this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ExceptionTraceQueryParameter{" +
+ "applicationName='" + applicationName + '\'' +
+ ", agentId='" + agentId + '\'' +
+ ", transactionId='" + transactionId + '\'' +
+ ", spanId=" + spanId +
+ ", exceptionId=" + exceptionId +
+ ", exceptionDepth=" + exceptionDepth +
+ ", groupByAttributes=" + groupByAttributes +
+ ", timeWindowRangeCount=" + timeWindowRangeCount +
+ '}';
+ }
+}
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 000000000000..70a749021490
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/java/com/navercorp/pinpoint/exceptiontrace/web/view/ExceptionTraceView.java
@@ -0,0 +1,94 @@
+/*
+ * 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.ExceptionTraceValueView;
+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 newViewFromValueViews(
+ String groupName,
+ TimeWindow timeWindow,
+ List exceptionTraceValueViews
+ ) {
+ Objects.requireNonNull(timeWindow, "timeWindow");
+ Objects.requireNonNull(exceptionTraceValueViews, "exceptionTraceValueViews");
+
+ List timestampList = createTimeStampList(timeWindow);
+ List timeSeriesValueGroupViews = new ArrayList<>();
+ timeSeriesValueGroupViews.add(
+ ExceptionTraceGroup.newGroupFromValueViews(groupName, exceptionTraceValueViews)
+ );
+
+ 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;
+ }
+
+ @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/exceptiontrace/mapper/ExceptionTraceMapper.xml b/exceptiontrace/exceptiontrace-web/src/main/resources/exceptiontrace/mapper/ExceptionTraceMapper.xml
new file mode 100644
index 000000000000..493bb8a9017f
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/main/resources/exceptiontrace/mapper/ExceptionTraceMapper.xml
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ exceptionTrace
+
+
+
+
+
+ ${attr.getAttributeName}
+
+
+
+
+
+
+
+
+ DATETIME_CONVERT
+ ("timestamp", '1:MILLISECONDS:EPOCH', '1:MILLISECONDS:EPOCH',
+ '#{timePrecision.timeSize}:${timePrecision.timeUnit}') as "timestamp",
+ transactionId,
+ spanId,
+ exceptionId,
+ applicationServiceType,
+ applicationName,
+ agentId,
+ uriTemplate,
+ errorClassName,
+ errorMessage,
+ exceptionDepth,
+ stackTraceClassName,
+ stackTraceFileName,
+ stackTraceLineNumber,
+ stackTraceMethodName,
+ stackTraceHash
+
+
+
+ DATETIME_CONVERT
+ ("timestamp", '1:MILLISECONDS:EPOCH', '1:MILLISECONDS:EPOCH',
+ '#{timePrecision.timeSize}:${timePrecision.timeUnit}') as "timestamp",
+ transactionId,
+ spanId,
+ exceptionId,
+ applicationServiceType,
+ applicationName,
+ agentId,
+ uriTemplate,
+ errorClassName,
+ errorMessage,
+ exceptionDepth,
+ arraySliceString(stackTraceClassName, 0,
+ ${STACKTRACE_COUNT}
+ )
+ as
+ "stackTraceClassName",
+ arraySliceString
+ (
+ stackTraceFileName,
+ 0,
+ ${STACKTRACE_COUNT}
+ )
+ as
+ "stackTraceFileName",
+ arraySliceInt
+ (
+ stackTraceLineNumber,
+ 0,
+ ${STACKTRACE_COUNT}
+ )
+ as
+ "stackTraceLineNumber",
+ arraySliceString
+ (
+ stackTraceMethodName,
+ 0,
+ ${STACKTRACE_COUNT}
+ )
+ as
+ "stackTraceMethodName",
+ 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 000000000000..87b78431e4e1
--- /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 000000000000..87b78431e4e1
--- /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/ExceptionMetaDataEntityMapperTest.java b/exceptiontrace/exceptiontrace-web/src/test/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/ExceptionMetaDataEntityMapperTest.java
new file mode 100644
index 000000000000..812cd56340b4
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/test/java/com/navercorp/pinpoint/exceptiontrace/web/mapper/ExceptionMetaDataEntityMapperTest.java
@@ -0,0 +1,190 @@
+package com.navercorp.pinpoint.exceptiontrace.web.mapper;
+
+
+import com.navercorp.pinpoint.common.server.mapper.MapStructUtils;
+import com.navercorp.pinpoint.exceptiontrace.common.model.ExceptionMetaData;
+import com.navercorp.pinpoint.exceptiontrace.common.model.StackTraceElementWrapper;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionMetaDataEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionTraceSummaryEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.entity.ExceptionTraceValueViewEntity;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceSummary;
+import com.navercorp.pinpoint.exceptiontrace.web.model.ExceptionTraceValueView;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import java.util.List;
+import java.util.Random;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @author intr3p1d
+ */
+@ContextConfiguration(classes = {
+ ExceptionMetaDataEntityMapperImpl.class,
+ StackTraceMapper.class,
+ MapStructUtils.class,
+ JacksonAutoConfiguration.class
+})
+@ExtendWith(SpringExtension.class)
+class ExceptionMetaDataEntityMapperTest {
+ private static final Logger logger = LogManager.getLogger(ExceptionMetaDataEntityMapper.class);
+ private final Random random = new Random();
+
+
+ @Autowired
+ private ExceptionMetaDataEntityMapper mapper;
+
+ @Autowired
+ private MapStructUtils mapStructUtils;
+
+
+ @Test
+ public void testEntityToModel() {
+ Throwable throwable = new RuntimeException();
+
+ ExceptionMetaDataEntity expected = newExceptionMetaDataEntity(throwable);
+ ExceptionMetaData actual = mapper.toModel(expected);
+
+ Assertions.assertEquals(expected.getTimestamp(), actual.getTimestamp());
+ Assertions.assertEquals(expected.getTransactionId(), actual.getTransactionId());
+ Assertions.assertEquals(expected.getSpanId(), actual.getSpanId());
+ Assertions.assertEquals(expected.getExceptionId(), actual.getExceptionId());
+
+ Assertions.assertEquals(expected.getApplicationServiceType(), actual.getApplicationServiceType());
+ Assertions.assertEquals(expected.getApplicationName(), actual.getApplicationName());
+ Assertions.assertEquals(expected.getAgentId(), actual.getAgentId());
+ Assertions.assertEquals(expected.getUriTemplate(), actual.getUriTemplate());
+
+ Assertions.assertEquals(expected.getErrorClassName(), actual.getErrorClassName());
+ Assertions.assertEquals(expected.getErrorMessage(), actual.getErrorMessage());
+ Assertions.assertEquals(expected.getExceptionDepth(), actual.getExceptionDepth());
+
+ Assertions.assertEquals(expected.getStackTraceHash(), actual.getStackTraceHash());
+
+
+ int size = throwable.getStackTrace().length;
+
+ String classNames = expected.getStackTraceClassName();
+ String fileNames = expected.getStackTraceFileName();
+ String lineNumbers = expected.getStackTraceLineNumber();
+ String methodNames = expected.getStackTraceMethodName();
+
+ List classNameIter = convertToList(classNames);
+ List fileNameIter = convertToList(fileNames);
+ List lineNumberIter = convertToList(lineNumbers);
+ List methodNameIter = convertToList(methodNames);
+
+ List actualStackTrace = actual.getStackTrace();
+
+ for (int i = 0; i < size; i++) {
+ Assertions.assertEquals(classNameIter.get(i), actualStackTrace.get(i).getClassName());
+ Assertions.assertEquals(fileNameIter.get(i), actualStackTrace.get(i).getFileName());
+ Assertions.assertEquals(lineNumberIter.get(i), actualStackTrace.get(i).getLineNumber());
+ Assertions.assertEquals(methodNameIter.get(i), actualStackTrace.get(i).getMethodName());
+ }
+ }
+
+ private ExceptionMetaDataEntity newExceptionMetaDataEntity(Throwable throwable) {
+ ExceptionMetaDataEntity dataEntity = new ExceptionMetaDataEntity();
+
+ dataEntity.setTimestamp(random.nextLong());
+ dataEntity.setTransactionId("transactionId");
+ dataEntity.setSpanId(random.nextLong());
+ dataEntity.setExceptionId(random.nextLong());
+ dataEntity.setApplicationServiceType("applicationServiceType");
+ dataEntity.setApplicationName("applicationName");
+ dataEntity.setAgentId("agentId");
+ dataEntity.setUriTemplate("uriTemplate");
+ dataEntity.setErrorClassName("errorClassName");
+ dataEntity.setErrorMessage("errorMessage");
+ dataEntity.setExceptionDepth(random.nextInt());
+ dataEntity.setStackTraceHash("stackTraceHash");
+
+ List elements = List.of(throwable.getStackTrace());
+
+ dataEntity.setStackTraceClassName(toFlattenedString(elements, StackTraceElement::getClassName));
+ dataEntity.setStackTraceFileName(toFlattenedString(elements, StackTraceElement::getFileName));
+ dataEntity.setStackTraceLineNumber(toFlattenedString(elements, StackTraceElement::getLineNumber));
+ dataEntity.setStackTraceMethodName(toFlattenedString(elements, StackTraceElement::getMethodName));
+ return dataEntity;
+ }
+
+ private String toFlattenedString(List elements, Function getter) {
+ List collect = elements.stream().map(getter).collect(Collectors.toList());
+ return mapStructUtils.listToJsonStr(collect);
+ }
+
+ public List convertToList(String json) {
+ return mapStructUtils.jsonStrToList(json);
+ }
+
+ @Test
+ public void testEntityToValueView() {
+ ExceptionTraceValueViewEntity expected = newExceptionMetaDataEntity();
+
+ ExceptionTraceValueView actual = mapper.entityToExceptionTraceValueView(expected);
+
+ Assertions.assertEquals(expected.getUriTemplate(), actual.getGroupedFieldName().getUriTemplate());
+ Assertions.assertEquals(expected.getErrorClassName(), actual.getGroupedFieldName().getErrorClassName());
+ Assertions.assertEquals(expected.getErrorMessage(), actual.getGroupedFieldName().getErrorMessage());
+ Assertions.assertEquals(expected.getStackTraceHash(), actual.getGroupedFieldName().getStackTraceHash());
+
+ Assertions.assertNotNull(actual.getValues());
+ Assertions.assertFalse(actual.getValues().isEmpty());
+ }
+
+
+ private ExceptionTraceValueViewEntity newExceptionMetaDataEntity() {
+ ExceptionTraceValueViewEntity dataEntity = new ExceptionTraceValueViewEntity();
+
+ dataEntity.setUriTemplate("uriTemplate");
+ dataEntity.setErrorClassName("errorClassName");
+ dataEntity.setErrorMessage("errorMessage");
+ dataEntity.setStackTraceHash("stackTraceHash");
+
+ dataEntity.setValues("[0,83,2,12]");
+ return dataEntity;
+ }
+
+ @Test
+ public void testEntityToSummary() {
+ ExceptionTraceSummaryEntity expected = newExceptionTraceSymmaryEntity();
+
+ ExceptionTraceSummary actual = mapper.entityToExceptionTraceSummary(expected);
+
+ Assertions.assertEquals(expected.getMostRecentErrorClass(), actual.getMostRecentErrorClass());
+ Assertions.assertEquals(expected.getMostRecentErrorMessage(), actual.getMostRecentErrorMessage());
+ Assertions.assertEquals(expected.getCount(), actual.getCount());
+ Assertions.assertEquals(expected.getFirstOccurred(), actual.getFirstOccurred());
+ Assertions.assertEquals(expected.getLastOccurred(), actual.getLastOccurred());
+
+ Assertions.assertEquals(expected.getUriTemplate(), actual.getGroupedFieldName().getUriTemplate());
+ Assertions.assertEquals(expected.getErrorClassName(), actual.getGroupedFieldName().getErrorClassName());
+ Assertions.assertEquals(expected.getErrorMessage(), actual.getGroupedFieldName().getErrorMessage());
+ Assertions.assertEquals(expected.getStackTraceHash(), actual.getGroupedFieldName().getStackTraceHash());
+ }
+
+ private ExceptionTraceSummaryEntity newExceptionTraceSymmaryEntity() {
+ ExceptionTraceSummaryEntity entity = new ExceptionTraceSummaryEntity();
+
+ entity.setMostRecentErrorClass("MostRecentErrorClass");
+ entity.setMostRecentErrorMessage("MostRecentErrorMessage");
+ entity.setCount(random.nextLong());
+ entity.setFirstOccurred(random.nextLong());
+ entity.setLastOccurred(random.nextLong());
+
+ entity.setUriTemplate("uriTemplate");
+ entity.setErrorClassName("errorClassName");
+ entity.setErrorMessage("errorMessage");
+ entity.setStackTraceHash("stackTraceHash");
+ return entity;
+ }
+}
\ No newline at end of file
diff --git a/exceptiontrace/exceptiontrace-web/src/test/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceGroupTest.java b/exceptiontrace/exceptiontrace-web/src/test/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceGroupTest.java
new file mode 100644
index 000000000000..5c7d0bd35a66
--- /dev/null
+++ b/exceptiontrace/exceptiontrace-web/src/test/java/com/navercorp/pinpoint/exceptiontrace/web/model/ExceptionTraceGroupTest.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.web.model;
+
+import com.navercorp.pinpoint.metric.web.view.TimeSeriesValueView;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+
+class ExceptionTraceGroupTest {
+
+ @Test
+ void newGroupFromValueViews_casting() {
+ List view = List.of(
+ new ExceptionTraceValueView(List.of(1, 2, 3)),
+ new ExceptionTraceValueView(List.of(4, 5, 6))
+ );
+ ExceptionTraceGroup group = ExceptionTraceGroup.newGroupFromValueViews("test", view);
+ @SuppressWarnings("unused")
+ List metricValues = group.getMetricValues();
+ }
+}
\ No newline at end of file
diff --git a/exceptiontrace/pom.xml b/exceptiontrace/pom.xml
new file mode 100644
index 000000000000..349f8196ee3a
--- /dev/null
+++ b/exceptiontrace/pom.xml
@@ -0,0 +1,21 @@
+
+
+
+ pinpoint
+ com.navercorp.pinpoint
+ 2.6.0-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 b19176b761e1..bc187eafd7b3 100644
--- a/metric-module/collector-starter/pom.xml
+++ b/metric-module/collector-starter/pom.xml
@@ -48,6 +48,10 @@
com.navercorp.pinpoint
pinpoint-log-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 808481dee741..777fe42a99c1 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
@@ -8,6 +8,7 @@
import com.navercorp.pinpoint.common.server.util.ServerBootLogger;
import com.navercorp.pinpoint.inspector.collector.InspectorCollectorApp;
import com.navercorp.pinpoint.log.collector.LogCollectorModule;
+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;
@@ -58,7 +59,8 @@ public static void main(String[] args) {
logger.info(String.format("Start %s collector", CollectorType.BASIC));
SpringApplicationBuilder collectorAppBuilder = createAppBuilder(builder, 15400,
BasicCollectorApp.class,
- UriStatCollectorConfig.class
+ UriStatCollectorConfig.class,
+ ExceptionTraceCollectorConfig.class
);
collectorAppBuilder.listeners(new AdditionalProfileListener("metric"));
collectorAppBuilder.listeners(new AdditionalProfileListener("uri"));
@@ -70,6 +72,7 @@ public static void main(String[] args) {
SpringApplicationBuilder collectorAppBuilder = createAppBuilder(builder, 15400,
BasicCollectorApp.class,
UriStatCollectorConfig.class,
+ ExceptionTraceCollectorConfig.class,
InspectorCollectorApp.class
);
collectorAppBuilder.build().run(args);
diff --git a/metric-module/collector-starter/src/main/resources/application.yml b/metric-module/collector-starter/src/main/resources/application.yml
index 27f5fbffa822..ae1c49140d63 100644
--- a/metric-module/collector-starter/src/main/resources/application.yml
+++ b/metric-module/collector-starter/src/main/resources/application.yml
@@ -3,4 +3,10 @@ spring:
allow-bean-definition-overriding: false
# web-application-type: none
profiles:
- active: local
\ No newline at end of file
+ active: local
+
+pinpoint:
+ modules:
+ collector:
+ exceptiontrace:
+ enabled: true
diff --git a/metric-module/web-starter/pom.xml b/metric-module/web-starter/pom.xml
index 551e2e94d660..51b236b9c422 100644
--- a/metric-module/web-starter/pom.xml
+++ b/metric-module/web-starter/pom.xml
@@ -52,6 +52,10 @@
com.navercorp.pinpoint
pinpoint-log-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 7b12ce254c7b..2871659e8801 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
@@ -21,6 +21,7 @@
import com.navercorp.pinpoint.inspector.web.InspectorWebApp;
import com.navercorp.pinpoint.log.web.LogWebModule;
import com.navercorp.pinpoint.login.basic.PinpointBasicLoginConfig;
+import com.navercorp.pinpoint.exceptiontrace.web.ExceptionTraceWebConfig;
import com.navercorp.pinpoint.metric.web.MetricWebApp;
import com.navercorp.pinpoint.redis.RedisPropertySources;
import com.navercorp.pinpoint.uristat.web.UriStatWebConfig;
@@ -66,7 +67,8 @@ public static void main(String[] args) {
MetricWebApp.class,
UriStatWebConfig.class,
InspectorWebApp.class,
- LogWebModule.class
+ LogWebModule.class,
+ ExceptionTraceWebConfig.class
);
starter.addProfiles("uri", "metric");
starter.start(args);
diff --git a/metric-module/web-starter/src/main/resources/application.yml b/metric-module/web-starter/src/main/resources/application.yml
index fc2a54609544..0deec9c04282 100644
--- a/metric-module/web-starter/src/main/resources/application.yml
+++ b/metric-module/web-starter/src/main/resources/application.yml
@@ -12,4 +12,10 @@ server:
include-binding-errors: always
include-stacktrace: always
whitelabel:
- enabled: true
\ No newline at end of file
+ enabled: true
+
+pinpoint:
+ modules:
+ web:
+ exceptiontrace:
+ enabled: true
diff --git a/pom.xml b/pom.xml
index ae090e6d4757..9389046d14c5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -130,6 +130,7 @@
user
batch-alarmsender
channel
+ exceptiontrace
@@ -161,6 +162,7 @@
2.15.2
1.33
+ 1.5.5.Final
4.5.13
4.4.14
@@ -525,6 +527,21 @@
pinpoint-realtime-common
${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
@@ -885,6 +902,12 @@
snakeyaml
${snakeyaml.version}
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
org.apache.hbase