This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git
The following commit(s) were added to refs/heads/master by this push:
new 5afbc290d9 feat(add go profile data collect and analyze) (#13524)
5afbc290d9 is described below
commit 5afbc290d90976cbba97e9bc1333a4e306e2ceca
Author: hao <[email protected]>
AuthorDate: Fri Oct 31 18:59:35 2025 +0800
feat(add go profile data collect and analyze) (#13524)
---
.github/workflows/skywalking.yaml | 5 +-
docs/en/changes/changes.md | 4 +-
.../core/profiling/trace/ProfileLanguageType.java | 45 +++
.../trace/ProfileThreadSnapshotRecord.java | 7 +
.../profiling/trace/analyze/GoProfileAnalyzer.java | 243 +++++++++++++++
.../profiling/trace/analyze/ProfileAnalyzer.java | 112 ++++++-
.../server/library/pprof/parser/PprofParser.java | 27 ++
.../library/pprof/parser/PprofSegmentParser.java | 326 +++++++++++++++++++++
.../library/pprof/type/FrameTreeBuilder.java | 11 +-
.../skywalking-profile-receiver-plugin/pom.xml | 6 +
.../handler/ProfileTaskServiceHandler.java | 146 +++++++++
.../storage/plugin/banyandb/BanyanDBConverter.java | 9 +
.../elasticsearch/base/ColumnTypeEsMapping.java | 3 +-
.../plugin/jdbc/common/JDBCTableInstaller.java | 3 +-
.../jdbc/postgresql/PostgreSQLTableInstaller.java | 3 +-
test/e2e-v2/cases/go/service/e2e.go | 20 ++
test/e2e-v2/cases/go/service/go.mod | 1 -
.../cases/profiling/trace/go/docker-compose.yml | 65 ++++
test/e2e-v2/cases/profiling/trace/go/e2e.yaml | 92 ++++++
.../profiling/trace/go/expected/profile-create.yml | 17 ++
.../trace/go/expected/profile-list-finished.yml | 34 +++
.../trace/go/expected/profile-list-notified.yml | 34 +++
.../trace/go/expected/profile-segment-analyze.yml | 32 ++
.../trace/go/expected/profile-segment-list.yml | 52 ++++
24 files changed, 1266 insertions(+), 31 deletions(-)
diff --git a/.github/workflows/skywalking.yaml
b/.github/workflows/skywalking.yaml
index 0850b6750d..57bd56f15f 100644
--- a/.github/workflows/skywalking.yaml
+++ b/.github/workflows/skywalking.yaml
@@ -492,6 +492,9 @@ jobs:
config: test/e2e-v2/cases/profiling/trace/opensearch/e2e.yaml
env: OPENSEARCH_VERSION=2.4.0
+ - name: Go Trace Profiling
+ config: test/e2e-v2/cases/profiling/trace/go/e2e.yaml
+
- name: eBPF Profiling On CPU BanyanDB
config: test/e2e-v2/cases/profiling/ebpf/oncpu/banyandb/e2e.yaml
docker:
@@ -1118,4 +1121,4 @@ jobs:
[[ ${e2eJavaVersionResults} == 'success' ]] || [[ ${execute} !=
'true' && ${e2eJavaVersionResults} == 'skipped' ]] || exit -7;
[[ ${timeConsumingITResults} == 'success' ]] || [[ ${execute} !=
'true' && ${timeConsumingITResults} == 'skipped' ]] || exit -8;
- exit 0;
+ exit 0;
\ No newline at end of file
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index caa250e63c..d1219e1a17 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -113,7 +113,9 @@
* Update Grafana dashboards for OAP observability.
* BanyanDB: fix query `getInstance` by instance ID.
* Support the go agent(0.7.0 release) bundled pprof profiling feature.
-
+* Library-pprof-parser: feat: add PprofSegmentParser.
+* Storage: feat: add languageType column to ProfileThreadSnapshotRecord.
+* Feat: add go profile analyzer
#### UI
diff --git
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/ProfileLanguageType.java
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/ProfileLanguageType.java
new file mode 100644
index 0000000000..8faf53b810
--- /dev/null
+++
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/ProfileLanguageType.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.core.profiling.trace;
+
+/**
+ * Language type for profile records. Stored as int in storage for
compatibility.
+ */
+public enum ProfileLanguageType {
+ JAVA(0),
+ GO(1);
+
+ private final int value;
+
+ ProfileLanguageType(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public static ProfileLanguageType fromValue(int value) {
+ for (ProfileLanguageType language : values()) {
+ if (language.value == value) {
+ return language;
+ }
+ }
+ return JAVA; // default to Java
+ }
+}
\ No newline at end of file
diff --git
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/ProfileThreadSnapshotRecord.java
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/ProfileThreadSnapshotRecord.java
index bd04334a1c..98d048e84e 100644
---
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/ProfileThreadSnapshotRecord.java
+++
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/ProfileThreadSnapshotRecord.java
@@ -52,6 +52,7 @@ public class ProfileThreadSnapshotRecord extends Record {
public static final String DUMP_TIME = "dump_time";
public static final String SEQUENCE = "sequence";
public static final String STACK_BINARY = "stack_binary";
+ public static final String LANGUAGE_TYPE = "language_type";
@Column(name = TASK_ID)
@SQLDatabase.CompositeIndex(withColumns = {SEGMENT_ID})
@@ -69,6 +70,8 @@ public class ProfileThreadSnapshotRecord extends Record {
private int sequence;
@Column(name = STACK_BINARY)
private byte[] stackBinary;
+ @Column(name = LANGUAGE_TYPE) // NoIndexing
+ private ProfileLanguageType language = ProfileLanguageType.JAVA;
@Override
public StorageID id() {
@@ -88,6 +91,8 @@ public class ProfileThreadSnapshotRecord extends Record {
snapshot.setSequence(((Number)
converter.get(SEQUENCE)).intValue());
snapshot.setTimeBucket(((Number)
converter.get(TIME_BUCKET)).intValue());
snapshot.setStackBinary(converter.getBytes(STACK_BINARY));
+ final Number languageTypeNum = (Number)
converter.get(LANGUAGE_TYPE);
+ snapshot.setLanguage(ProfileLanguageType.fromValue(languageTypeNum
!= null ? languageTypeNum.intValue() : 0));
return snapshot;
}
@@ -99,6 +104,8 @@ public class ProfileThreadSnapshotRecord extends Record {
converter.accept(SEQUENCE, storageData.getSequence());
converter.accept(TIME_BUCKET, storageData.getTimeBucket());
converter.accept(STACK_BINARY, storageData.getStackBinary());
+ ProfileLanguageType language = storageData.getLanguage();
+ converter.accept(LANGUAGE_TYPE, language != null ?
language.getValue() : ProfileLanguageType.JAVA.getValue());
}
}
}
diff --git
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/analyze/GoProfileAnalyzer.java
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/analyze/GoProfileAnalyzer.java
new file mode 100644
index 0000000000..fe2a9e53b6
--- /dev/null
+++
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/analyze/GoProfileAnalyzer.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.server.core.profiling.trace.analyze;
+
+import com.google.perftools.profiles.ProfileProto;
+import java.util.Collections;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.ArrayDeque;
+import
org.apache.skywalking.oap.server.core.profiling.trace.ProfileThreadSnapshotRecord;
+import
org.apache.skywalking.oap.server.core.query.input.SegmentProfileAnalyzeQuery;
+import org.apache.skywalking.oap.server.core.query.type.ProfileAnalyzation;
+import org.apache.skywalking.oap.server.core.query.type.ProfileStackElement;
+import org.apache.skywalking.oap.server.core.query.type.ProfileStackTree;
+import
org.apache.skywalking.oap.server.library.pprof.parser.PprofSegmentParser;
+import org.apache.skywalking.oap.server.library.pprof.parser.PprofParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Analyzer for Go pprof samples. Builds a stack tree with total/self
durations using sampling period.
+ * This works independently from ThreadSnapshot, for Go profiles only.
+ */
+public class GoProfileAnalyzer {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(GoProfileAnalyzer.class);
+
+ /**
+ * Analyze a pprof profile for a specific segment and time window.
+ */
+ public ProfileAnalyzation analyze(final String segmentId,
+ final ProfileProto.Profile profile) {
+ final long periodMs = PprofSegmentParser.resolvePeriodMillis(profile);
+
+ // Build ProfileStackElement directly (reuse FrameTreeBuilder's
mergeSample logic)
+ Map<String, Integer> key2Id = new HashMap<>(); // "parentId|name" -> id
+ List<ProfileStackElement> elements = new ArrayList<>();
+
+ // Strict per-segment filtering
+ final List<String> stringTable = profile.getStringTableList();
+
+ for (ProfileProto.Sample sample : profile.getSampleList()) {
+ final String seg =
PprofSegmentParser.extractSegmentIdFromLabels(sample.getLabelList(),
stringTable);
+ if (seg == null || !seg.equals(segmentId)) {
+ continue;
+ }
+ long sampleCount = sample.getValueCount() > 0 ? sample.getValue(0)
: 1L;
+ long weightMs = sampleCount * periodMs;
+
+ // Build function stack then ensure root->leaf order for
aggregation
+ List<String> stack =
PprofSegmentParser.extractStackFromSample(sample, profile);
+ Collections.reverse(stack);
+
+ // Aggregate along path (similar to FrameTreeBuilder.mergeSample)
+ int parentId = -1; // root
+ for (String fn : stack) {
+ String key = parentId + "|" + fn;
+ Integer nodeId = key2Id.get(key);
+
+ if (nodeId == null) {
+ ProfileStackElement element = new ProfileStackElement();
+ element.setId(elements.size());
+ element.setParentId(parentId);
+ element.setCodeSignature(fn);
+ element.setDuration(0);
+ element.setDurationChildExcluded(0);
+ element.setCount(0);
+ elements.add(element);
+ nodeId = element.getId();
+ key2Id.put(key, nodeId);
+ }
+
+ ProfileStackElement element = elements.get(nodeId);
+ element.setDuration(element.getDuration() + (int) weightMs);
+ element.setCount(element.getCount() + (int) sampleCount);
+
+ parentId = nodeId;
+ }
+ }
+
+ int rootCount = 0;
+ for (ProfileStackElement e : elements) {
+ if (e.getParentId() == -1) {
+ rootCount++;
+ }
+ }
+ if (rootCount > 1) {
+ int virtualRootId = elements.size();
+ ProfileStackElement virtualRoot = new ProfileStackElement();
+ virtualRoot.setId(virtualRootId);
+ virtualRoot.setParentId(-1);
+ virtualRoot.setCodeSignature("root");
+ virtualRoot.setDuration(0);
+ virtualRoot.setDurationChildExcluded(0);
+ virtualRoot.setCount(0);
+ elements.add(virtualRoot);
+
+ for (ProfileStackElement e : elements) {
+ if (e.getId() == virtualRootId) {
+ continue;
+ }
+ if (e.getParentId() == -1) {
+ e.setParentId(virtualRootId);
+ virtualRoot.setDuration(virtualRoot.getDuration() +
e.getDuration());
+ virtualRoot.setCount(virtualRoot.getCount() +
e.getCount());
+ }
+ }
+ }
+
+ Map<Integer, Integer> childDurSum = new HashMap<>();
+ for (ProfileStackElement child : elements) {
+ int pid = child.getParentId();
+ if (pid != -1) {
+ childDurSum.put(pid, childDurSum.getOrDefault(pid, 0) +
child.getDuration());
+ }
+ }
+ for (ProfileStackElement elem : elements) {
+ int childrenSum = childDurSum.getOrDefault(elem.getId(), 0);
+ elem.setDurationChildExcluded(Math.max(0, elem.getDuration() -
childrenSum));
+ }
+
+ Integer rootId = null;
+ for (ProfileStackElement e : elements) {
+ if (e.getParentId() == -1) {
+ rootId = e.getId();
+ break;
+ }
+ }
+ if (rootId != null) {
+ Map<Integer, List<ProfileStackElement>> childrenMap = new
HashMap<>();
+ for (ProfileStackElement e : elements) {
+ childrenMap.computeIfAbsent(e.getParentId(), k -> new
ArrayList<>()).add(e);
+ }
+
+ List<ProfileStackElement> ordered = new ArrayList<>();
+ ArrayDeque<ProfileStackElement> queue = new ArrayDeque<>();
+ // start from root
+ for (ProfileStackElement e : elements) {
+ if (e.getId() == rootId) {
+ queue.add(e);
+ break;
+ }
+ }
+ while (!queue.isEmpty()) {
+ ProfileStackElement cur = queue.removeFirst();
+ ordered.add(cur);
+ List<ProfileStackElement> children =
childrenMap.get(cur.getId());
+ if (children != null) {
+ // sort children by duration desc to make primary path
first
+ children.sort((a, b) -> Integer.compare(b.getDuration(),
a.getDuration()));
+ queue.addAll(children);
+ }
+ }
+
+ Map<Integer, Integer> idRemap = new HashMap<>();
+ for (int i = 0; i < ordered.size(); i++) {
+ idRemap.put(ordered.get(i).getId(), i);
+ }
+ for (ProfileStackElement e : ordered) {
+ int newId = idRemap.get(e.getId());
+ int parentId = e.getParentId();
+ e.setId(newId);
+ if (parentId == -1) {
+ e.setParentId(-1);
+ } else {
+ e.setParentId(idRemap.getOrDefault(parentId, -1));
+ }
+ }
+ elements = ordered;
+ }
+
+ ProfileStackTree tree = new ProfileStackTree();
+ tree.setElements(elements);
+
+ ProfileAnalyzation result = new ProfileAnalyzation();
+ result.getTrees().add(tree);
+ return result;
+ }
+
+ /**
+ * Analyze multiple Go profile records and return combined results
+ */
+ public ProfileAnalyzation analyzeRecords(List<ProfileThreadSnapshotRecord>
records, List<SegmentProfileAnalyzeQuery> queries) {
+ ProfileAnalyzation result = new ProfileAnalyzation();
+
+ // Build query map for O(1) lookup
+ Map<String, SegmentProfileAnalyzeQuery> queryMap = queries.stream()
+
.collect(Collectors.toMap(SegmentProfileAnalyzeQuery::getSegmentId, q -> q));
+
+ for (ProfileThreadSnapshotRecord record : records) {
+ try {
+ // Find the corresponding query for this segment
+ SegmentProfileAnalyzeQuery query =
queryMap.get(record.getSegmentId());
+
+ if (query == null) {
+ LOGGER.warn("No query found for Go profile segment: {}",
record.getSegmentId());
+ continue;
+ }
+
+ // Parse pprof data from stackBinary
+ ProfileProto.Profile profile =
PprofParser.parseProfile(record.getStackBinary());
+
+ // Analyze this record
+ ProfileAnalyzation recordAnalyzation = analyze(
+ record.getSegmentId(),
+ profile
+ );
+
+ if (recordAnalyzation != null &&
!recordAnalyzation.getTrees().isEmpty()) {
+ result.getTrees().addAll(recordAnalyzation.getTrees());
+
+ if (LOGGER.isInfoEnabled()) {
+ LOGGER.info("Go profile analysis completed:
segmentId={}, window=[{}-{}], trees={}",
+ record.getSegmentId(),
query.getTimeRange().getStart(), query.getTimeRange().getEnd(),
+ recordAnalyzation.getTrees().size());
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.error("Failed to analyze Go profile record:
segmentId={}, sequence={}, dumpTime={}",
+ record.getSegmentId(), record.getSequence(),
record.getDumpTime(), e);
+ }
+ }
+
+ return result;
+ }
+}
diff --git
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/analyze/ProfileAnalyzer.java
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/analyze/ProfileAnalyzer.java
index 65d67cd454..6a80e6f10d 100644
---
a/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/analyze/ProfileAnalyzer.java
+++
b/oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/profiling/trace/analyze/ProfileAnalyzer.java
@@ -28,6 +28,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import
org.apache.skywalking.oap.server.core.profiling.trace.ProfileThreadSnapshotRecord;
+import
org.apache.skywalking.oap.server.core.profiling.trace.ProfileLanguageType;
import
org.apache.skywalking.oap.server.core.query.input.SegmentProfileAnalyzeQuery;
import org.apache.skywalking.oap.server.core.query.type.ProfileAnalyzation;
import org.apache.skywalking.oap.server.core.query.type.ProfileStackTree;
@@ -67,30 +68,99 @@ public class ProfileAnalyzer {
public ProfileAnalyzation analyze(final List<SegmentProfileAnalyzeQuery>
queries) throws IOException {
ProfileAnalyzation analyzation = new ProfileAnalyzation();
- // query sequence range list
+ // Step 1: Try Java profile analysis first (original logic with time
window)
SequenceSearch sequenceSearch = getAllSequenceRange(queries);
- if (sequenceSearch == null) {
- analyzation.setTip("Data not found");
- return analyzation;
+ List<ProfileThreadSnapshotRecord> javaRecords = new ArrayList<>();
+
+ if (sequenceSearch != null) {
+ if (sequenceSearch.getTotalSequenceCount() >
analyzeSnapshotMaxSize) {
+ analyzation.setTip("Out of snapshot analyze limit, " +
sequenceSearch.getTotalSequenceCount() + " snapshots found, but analysis first
" + analyzeSnapshotMaxSize + " snapshots only.");
+ }
+
+ // query snapshots within time window
+ List<ProfileThreadSnapshotRecord> records =
sequenceSearch.getRanges().parallelStream().map(r -> {
+ try {
+ return
getProfileThreadSnapshotQueryDAO().queryRecords(r.getSegmentId(),
r.getMinSequence(), r.getMaxSequence());
+ } catch (IOException e) {
+ LOGGER.warn(e.getMessage(), e);
+ return
Collections.<ProfileThreadSnapshotRecord>emptyList();
+ }
+ }).flatMap(Collection::stream)
+ .collect(Collectors.toList());
+
+ if (LOGGER.isDebugEnabled()) {
+ final int totalRanges = sequenceSearch.getRanges().size();
+ LOGGER.debug("Profile analyze fetched records,
segmentId(s)={}, ranges={}, recordsCount={}",
+
sequenceSearch.getRanges().stream().map(SequenceRange::getSegmentId).distinct().collect(Collectors.toList()),
+ totalRanges, records.size());
+ }
+
+ // Filter Java records
+ javaRecords = records.stream()
+ .filter(rec -> rec.getLanguage() == ProfileLanguageType.JAVA)
+ .collect(Collectors.toList());
+ } else {
+ if (LOGGER.isInfoEnabled()) {
+ LOGGER.info("Profile analyze: no Java records found in time
window, will try Go fallback");
+ }
}
- if (sequenceSearch.getTotalSequenceCount() > analyzeSnapshotMaxSize) {
- analyzation.setTip("Out of snapshot analyze limit, " +
sequenceSearch.getTotalSequenceCount() + " snapshots found, but analysis first
" + analyzeSnapshotMaxSize + " snapshots only.");
+
+ // Analyze Java profiles if found
+ if (!javaRecords.isEmpty()) {
+ LOGGER.info("Analyzing {} Java profile records",
javaRecords.size());
+ List<ProfileStack> stacks = javaRecords.stream()
+ .map(rec -> {
+ try {
+ return ProfileStack.deserialize(rec);
+ } catch (Exception ex) {
+ LOGGER.warn("Deserialize stack failed, segmentId={},
sequence={}, dumpTime={}",
+ rec.getSegmentId(), rec.getSequence(),
rec.getDumpTime(), ex);
+ return null;
+ }
+ })
+ .filter(java.util.Objects::nonNull)
+ .distinct()
+ .collect(Collectors.toList());
+
+ final List<ProfileStackTree> trees = analyzeByStack(stacks);
+ if (trees != null && !trees.isEmpty()) {
+ analyzation.getTrees().addAll(trees);
+ // Java analysis found data, return early
+ return analyzation;
+ }
}
- // query snapshots
- List<ProfileStack> stacks =
sequenceSearch.getRanges().parallelStream().map(r -> {
+ // Step 2: Fallback to Go profile analysis if Java found no data
+ // For Go profiles, ignore time window and fetch all records per
segment
+ List<ProfileThreadSnapshotRecord> goRecords = new ArrayList<>();
+ for (SegmentProfileAnalyzeQuery q : queries) {
+ final String segId = q.getSegmentId();
try {
- return
getProfileThreadSnapshotQueryDAO().queryRecords(r.getSegmentId(),
r.getMinSequence(), r.getMaxSequence());
+ int minSeq =
getProfileThreadSnapshotQueryDAO().queryMinSequence(segId, 0L, Long.MAX_VALUE);
+ int maxSeqExclusive =
getProfileThreadSnapshotQueryDAO().queryMaxSequence(segId, 0L, Long.MAX_VALUE)
+ 1;
+ if (maxSeqExclusive > minSeq) {
+ List<ProfileThreadSnapshotRecord> full =
getProfileThreadSnapshotQueryDAO().queryRecords(segId, minSeq, maxSeqExclusive);
+ for (ProfileThreadSnapshotRecord r : full) {
+ if (r.getLanguage() == ProfileLanguageType.GO) {
+ goRecords.add(r);
+ }
+ }
+ }
} catch (IOException e) {
- LOGGER.warn(e.getMessage(), e);
- return Collections.<ProfileThreadSnapshotRecord>emptyList();
+ LOGGER.warn("Go fallback: full-range fetch failed for
segmentId={}", segId, e);
}
-
}).flatMap(Collection::stream).map(ProfileStack::deserialize).distinct().collect(Collectors.toList());
+ }
- // analyze
- final List<ProfileStackTree> trees = analyzeByStack(stacks);
- if (trees != null) {
- analyzation.getTrees().addAll(trees);
+ if (!goRecords.isEmpty()) {
+ LOGGER.info("Java analysis found no data, fallback to Go:
analyzing {} Go profile records", goRecords.size());
+ GoProfileAnalyzer goAnalyzer = new GoProfileAnalyzer();
+ ProfileAnalyzation goAnalyzation =
goAnalyzer.analyzeRecords(goRecords, queries);
+ if (goAnalyzation != null && !goAnalyzation.getTrees().isEmpty()) {
+ analyzation.getTrees().addAll(goAnalyzation.getTrees());
+ }
+ } else if (sequenceSearch == null && javaRecords.isEmpty()) {
+ // Both Java (time window) and Go (full range) found nothing
+ analyzation.setTip("Data not found");
}
return analyzation;
@@ -115,8 +185,17 @@ public class ProfileAnalyzer {
int minSequence =
getProfileThreadSnapshotQueryDAO().queryMinSequence(segmentId, start, end);
int maxSequence =
getProfileThreadSnapshotQueryDAO().queryMaxSequence(segmentId, start, end) + 1;
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Profile analyze sequence window: segmentId={},
start={}, end={}, minSeq={}, maxSeq(exclusive)={}",
+ segmentId, start, end, minSequence, maxSequence);
+ }
+
// data not found
if (maxSequence <= 0) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Profile analyze not found any sequence in
window: segmentId={}, start={}, end={}",
+ segmentId, start, end);
+ }
return null;
}
@@ -207,4 +286,5 @@ public class ProfileAnalyzer {
}
}
+
}
diff --git
a/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/parser/PprofParser.java
b/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/parser/PprofParser.java
index 2475a19694..260b235232 100644
---
a/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/parser/PprofParser.java
+++
b/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/parser/PprofParser.java
@@ -54,4 +54,31 @@ public class PprofParser {
FrameTree tree = new FrameTreeBuilder(profile).build();
return tree;
}
+
+ /**
+ * Resolve function signature for a given location id. The signature
format matches FrameTreeBuilder
+ * (functionName:line;... when inlined, joined by ';').
+ */
+ public static String resolveSignature(long locationId,
ProfileProto.Profile profile) {
+ if (locationId == 0) {
+ return "root";
+ }
+ ProfileProto.Location location = profile.getLocation((int) locationId
- 1);
+ return location.getLineList().stream().map(line -> {
+ ProfileProto.Function function = profile.getFunction((int)
line.getFunctionId() - 1);
+ String functionName = profile.getStringTable((int)
function.getName());
+ return functionName + ":" + line.getLine();
+ }).collect(java.util.stream.Collectors.joining(";"));
+ }
+
+ public static ProfileProto.Profile parseProfile(byte[] payload) throws
IOException {
+ if (payload == null) {
+ throw new IOException("pprof payload is null");
+ }
+ java.io.InputStream input = new java.io.ByteArrayInputStream(payload);
+ if (payload.length >= 2 && (payload[0] == (byte) 0x1F) && (payload[1]
== (byte) 0x8B)) {
+ input = new GZIPInputStream(input);
+ }
+ return ProfileProto.Profile.parseFrom(input);
+ }
}
diff --git
a/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/parser/PprofSegmentParser.java
b/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/parser/PprofSegmentParser.java
new file mode 100644
index 0000000000..7fcba93be2
--- /dev/null
+++
b/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/parser/PprofSegmentParser.java
@@ -0,0 +1,326 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.oap.server.library.pprof.parser;
+
+import com.google.perftools.profiles.ProfileProto;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Parse pprof profile data and extract segment information
+ */
+public class PprofSegmentParser {
+
+ /**
+ * Segment information extracted from pprof labels
+ */
+ public static class SegmentInfo {
+ private String segmentId;
+ private String traceId;
+ private String spanId;
+ private String serviceInstanceId;
+ private List<String> stack;
+ private long count;
+
+ public String getSegmentId() {
+ return segmentId;
+ }
+
+ public void setSegmentId(String segmentId) {
+ this.segmentId = segmentId;
+ }
+
+ public String getTraceId() {
+ return traceId;
+ }
+
+ public void setTraceId(String traceId) {
+ this.traceId = traceId;
+ }
+
+ public String getSpanId() {
+ return spanId;
+ }
+
+ public void setSpanId(String spanId) {
+ this.spanId = spanId;
+ }
+
+ public String getServiceInstanceId() {
+ return serviceInstanceId;
+ }
+
+ public void setServiceInstanceId(String serviceInstanceId) {
+ this.serviceInstanceId = serviceInstanceId;
+ }
+
+ public List<String> getStack() {
+ return stack;
+ }
+
+ public void setStack(List<String> stack) {
+ this.stack = stack;
+ }
+
+ public long getCount() {
+ return count;
+ }
+
+ public void setCount(long count) {
+ this.count = count;
+ }
+ }
+
+ /**
+ * Parse pprof profile and extract all segment information
+ */
+ public static List<SegmentInfo> parseSegments(ProfileProto.Profile
profile) {
+ List<String> stringTable = profile.getStringTableList();
+
+ // Group samples by segmentId
+ Map<String, List<ProfileProto.Sample>> segmentSamples = new
HashMap<>();
+
+ for (ProfileProto.Sample sample : profile.getSampleList()) {
+ String segmentId =
extractSegmentIdFromLabels(sample.getLabelList(), stringTable);
+ if (segmentId != null) {
+ segmentSamples.computeIfAbsent(segmentId, k -> new
ArrayList<>()).add(sample);
+ }
+ }
+
+ // Create SegmentInfo for each segment
+ List<SegmentInfo> result = new ArrayList<>(segmentSamples.size());
+ for (Map.Entry<String, List<ProfileProto.Sample>> entry :
segmentSamples.entrySet()) {
+ String segmentId = entry.getKey();
+ List<ProfileProto.Sample> samples = entry.getValue();
+
+ SegmentInfo segmentInfo = new SegmentInfo();
+ segmentInfo.setSegmentId(segmentId);
+
+ // Extract basic information from first sample
+ ProfileProto.Sample firstSample = samples.get(0);
+
segmentInfo.setTraceId(extractTraceIdFromLabels(firstSample.getLabelList(),
stringTable));
+
segmentInfo.setSpanId(extractSpanIdFromLabels(firstSample.getLabelList(),
stringTable));
+
segmentInfo.setServiceInstanceId(extractServiceInstanceIdFromLabels(firstSample.getLabelList(),
stringTable));
+
+ // Merge call stacks from all samples
+ List<String> combinedStack =
extractCombinedStackFromSamples(samples, profile);
+ segmentInfo.setStack(combinedStack);
+
+ // Calculate total sample count
+ long totalCount = samples.stream()
+ .mapToLong(sample -> sample.getValueCount() > 0 ?
sample.getValue(0) : 1)
+ .sum();
+ segmentInfo.setCount(totalCount);
+
+ result.add(segmentInfo);
+ }
+
+ return result;
+ }
+
+ /**
+ * Extract segmentId from labels
+ */
+ public static String extractSegmentIdFromLabels(List<ProfileProto.Label>
labels, List<String> stringTable) {
+ for (ProfileProto.Label label : labels) {
+ String key = getStringFromTable(label.getKey(), stringTable);
+ if (key != null && (key.equals("segment_id") ||
key.equals("trace_segment_id") ||
+ key.equals("segmentId") ||
key.equals("traceSegmentId") ||
+ key.equals("traceSegmentID"))) {
+ return getStringFromTable(label.getStr(), stringTable);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Extract traceId from labels
+ */
+ private static String extractTraceIdFromLabels(List<ProfileProto.Label>
labels, List<String> stringTable) {
+ for (ProfileProto.Label label : labels) {
+ String key = getStringFromTable(label.getKey(), stringTable);
+ if (key != null && (key.equals("trace_id") ||
key.equals("traceId") || key.equals("traceID"))) {
+ return getStringFromTable(label.getStr(), stringTable);
+ }
+ }
+ return "go_trace_" + UUID.randomUUID().toString().replace("-", "");
+ }
+
+ /**
+ * Extract spanId from labels
+ */
+ private static String extractSpanIdFromLabels(List<ProfileProto.Label>
labels, List<String> stringTable) {
+ for (ProfileProto.Label label : labels) {
+ String key = getStringFromTable(label.getKey(), stringTable);
+ if (key != null && (key.equals("span_id") || key.equals("spanId")
|| key.equals("spanID"))) {
+ return getStringFromTable(label.getStr(), stringTable);
+ }
+ }
+ return null; // spanId is optional
+ }
+
+ /**
+ * Extract serviceInstanceId from labels
+ */
+ private static String
extractServiceInstanceIdFromLabels(List<ProfileProto.Label> labels,
List<String> stringTable) {
+ for (ProfileProto.Label label : labels) {
+ String key = getStringFromTable(label.getKey(), stringTable);
+ if (key != null && (key.equals("service_instance_id") ||
key.equals("serviceInstanceId") ||
+ key.equals("instance_id") ||
key.equals("instanceId"))) {
+ return getStringFromTable(label.getStr(), stringTable);
+ }
+ }
+ return "go_instance_1";
+ }
+
+ /**
+ * Extract merged call stack from samples
+ */
+ private static List<String>
extractCombinedStackFromSamples(List<ProfileProto.Sample> samples,
ProfileProto.Profile profile) {
+ Set<String> uniqueStack = new LinkedHashSet<>();
+
+ for (ProfileProto.Sample sample : samples) {
+ List<String> stack = extractStackFromSample(sample, profile);
+ uniqueStack.addAll(stack);
+ }
+
+ return new ArrayList<>(uniqueStack);
+ }
+
+ /**
+ * Extract call stack from a single sample
+ */
+ public static List<String> extractStackFromSample(ProfileProto.Sample
sample, ProfileProto.Profile profile) {
+ List<String> stack = new ArrayList<>();
+
+ // Traverse location_id from leaf to root
+ for (int i = sample.getLocationIdCount() - 1; i >= 0; i--) {
+ long locationId = sample.getLocationId(i);
+
+ // Delegate signature resolution to PprofParser to avoid
duplication
+ String signature = PprofParser.resolveSignature(locationId,
profile);
+ if (signature != null && !signature.isEmpty()) {
+ stack.add(signature);
+ }
+ }
+
+ return stack;
+ }
+
+ /**
+ * Get string from string table
+ */
+ public static String getStringFromTable(long index, List<String>
stringTable) {
+ if (index >= 0 && index < stringTable.size()) {
+ return stringTable.get((int) index);
+ }
+ return null;
+ }
+
+ /**
+ * Extract label value from sample labels
+ */
+ public static String extractLabel(ProfileProto.Sample sample, List<String>
stringTable, String... keys) {
+ for (ProfileProto.Label l : sample.getLabelList()) {
+ String k = getStringFromTable(l.getKey(), stringTable);
+ if (k == null) {
+ continue;
+ }
+ for (String expect : keys) {
+ if (k.equals(expect)) {
+ return getStringFromTable(l.getStr(), stringTable);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Extract timestamp from sample labels
+ */
+ public static long extractTimestamp(ProfileProto.Sample sample,
List<String> stringTable, boolean isStart) {
+ String target = isStart ? "startTime" : "endTime";
+ for (ProfileProto.Label l : sample.getLabelList()) {
+ String k = getStringFromTable(l.getKey(), stringTable);
+ if (k == null) {
+ continue;
+ }
+ if (!target.equalsIgnoreCase(k)) {
+ continue;
+ }
+ long v = l.getNum();
+ if (v <= 0) {
+ try {
+ String sv = getStringFromTable(l.getStr(), stringTable);
+ if (sv != null) {
+ v = Long.parseLong(sv.trim());
+ }
+ } catch (Exception ignored) {
+ // ignore
+ }
+ }
+ if (v > 0 && v < 1_000_000_000_000L) {
+ // looks like seconds -> millis
+ return v * 1000L;
+ }
+ return v;
+ }
+ return 0L;
+ }
+
+ /**
+ * Resolve sampling period in milliseconds from pprof profile
+ */
+ public static long resolvePeriodMillis(ProfileProto.Profile profile) {
+ try {
+ long period = profile.getPeriod();
+ String unit = null;
+ if (profile.hasPeriodType()) {
+ unit = getStringFromTable(profile.getPeriodType().getUnit(),
profile.getStringTableList());
+ }
+ if (period > 0) {
+ if (unit == null || unit.isEmpty() ||
"nanoseconds".equals(unit) || "nanosecond".equals(unit) || "ns".equals(unit)) {
+ return Math.max(1L, period / 1_000_000L);
+ }
+ if ("microseconds".equals(unit) || "us".equals(unit)) {
+ return Math.max(1L, period / 1_000L);
+ }
+ if ("milliseconds".equals(unit) || "ms".equals(unit)) {
+ return Math.max(1L, period);
+ }
+ if ("seconds".equals(unit) || "s".equals(unit)) {
+ return Math.max(1L, period * 1000L);
+ }
+ if ("hz".equals(unit) || "HZ".equals(unit)) {
+ // samples per second
+ return Math.max(1L, 1000L / Math.max(1L, period));
+ }
+ }
+ } catch (Throwable t) {
+ // keep silent in normal path; this is non-fatal and we fallback
to default
+ }
+ return 10L; // default fallback
+ }
+}
+
diff --git
a/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/type/FrameTreeBuilder.java
b/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/type/FrameTreeBuilder.java
index 43c7d47f64..ce893cba28 100644
---
a/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/type/FrameTreeBuilder.java
+++
b/oap-server/server-library/library-pprof-parser/src/main/java/org/apache/skywalking/oap/server/library/pprof/type/FrameTreeBuilder.java
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import org.apache.skywalking.oap.server.library.pprof.parser.PprofParser;
@Data
@NoArgsConstructor
@@ -58,15 +59,7 @@ public class FrameTreeBuilder {
}
private String getSignature(long locationId) {
- if (locationId == 0) {
- return "root";
- }
- ProfileProto.Location location = profile.getLocation((int) locationId
- 1);
- return location.getLineList().stream().map((line) -> {
- ProfileProto.Function function = profile.getFunction((int)
line.getFunctionId() - 1);
- String functionName = profile.getStringTable((int)
function.getName());
- return functionName + ":" + line.getLine();
- }).collect(Collectors.joining(";"));
+ return PprofParser.resolveSignature(locationId, profile);
}
public FrameTree build() {
diff --git
a/oap-server/server-receiver-plugin/skywalking-profile-receiver-plugin/pom.xml
b/oap-server/server-receiver-plugin/skywalking-profile-receiver-plugin/pom.xml
index f5f75875b5..5eb3a966e8 100644
---
a/oap-server/server-receiver-plugin/skywalking-profile-receiver-plugin/pom.xml
+++
b/oap-server/server-receiver-plugin/skywalking-profile-receiver-plugin/pom.xml
@@ -33,5 +33,11 @@
<artifactId>skywalking-sharing-server-plugin</artifactId>
<version>${project.version}</version>
</dependency>
+ <!-- Use centralized pprof parser -->
+ <dependency>
+ <groupId>org.apache.skywalking</groupId>
+ <artifactId>library-pprof-parser</artifactId>
+ <version>${project.version}</version>
+ </dependency>
</dependencies>
</project>
\ No newline at end of file
diff --git
a/oap-server/server-receiver-plugin/skywalking-profile-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/profile/provider/handler/ProfileTaskServiceHandler.java
b/oap-server/server-receiver-plugin/skywalking-profile-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/profile/provider/handler/ProfileTaskServiceHandler.java
index 1de09417ff..e30556b03e 100644
---
a/oap-server/server-receiver-plugin/skywalking-profile-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/profile/provider/handler/ProfileTaskServiceHandler.java
+++
b/oap-server/server-receiver-plugin/skywalking-profile-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/profile/provider/handler/ProfileTaskServiceHandler.java
@@ -20,13 +20,17 @@ package
org.apache.skywalking.oap.server.receiver.profile.provider.handler;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.skywalking.apm.network.common.v3.Commands;
+import org.apache.skywalking.apm.network.language.profile.v3.GoProfileData;
import
org.apache.skywalking.apm.network.language.profile.v3.ProfileTaskCommandQuery;
import
org.apache.skywalking.apm.network.language.profile.v3.ProfileTaskFinishReport;
import org.apache.skywalking.apm.network.language.profile.v3.ProfileTaskGrpc;
import org.apache.skywalking.apm.network.language.profile.v3.ThreadSnapshot;
+import com.google.perftools.profiles.ProfileProto;
import org.apache.skywalking.oap.server.core.CoreModule;
import org.apache.skywalking.oap.server.core.analysis.IDManager;
import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
@@ -34,12 +38,16 @@ import
org.apache.skywalking.oap.server.core.analysis.worker.RecordStreamProcess
import org.apache.skywalking.oap.server.core.cache.ProfileTaskCache;
import org.apache.skywalking.oap.server.core.command.CommandService;
import
org.apache.skywalking.oap.server.core.profiling.trace.ProfileTaskLogRecord;
+import
org.apache.skywalking.oap.server.core.profiling.trace.ProfileLanguageType;
import
org.apache.skywalking.oap.server.core.profiling.trace.ProfileThreadSnapshotRecord;
import org.apache.skywalking.oap.server.core.query.type.ProfileTask;
import
org.apache.skywalking.oap.server.core.query.type.ProfileTaskLogOperationType;
import org.apache.skywalking.oap.server.library.module.ModuleManager;
import org.apache.skywalking.oap.server.library.server.grpc.GRPCHandler;
import org.apache.skywalking.oap.server.library.util.CollectionUtils;
+import
org.apache.skywalking.oap.server.library.pprof.parser.PprofSegmentParser;
+import org.apache.skywalking.oap.server.library.pprof.parser.PprofParser;
+import
org.apache.skywalking.oap.server.library.pprof.parser.PprofSegmentParser.SegmentInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -105,6 +113,7 @@ public class ProfileTaskServiceHandler extends
ProfileTaskGrpc.ProfileTaskImplBa
record.setSequence(snapshot.getSequence());
record.setStackBinary(snapshot.getStack().toByteArray());
record.setTimeBucket(TimeBucket.getRecordTimeBucket(snapshot.getTime()));
+ record.setLanguage(ProfileLanguageType.JAVA); // default
language for thread snapshots
// async storage
RecordStreamProcessor.getInstance().in(record);
@@ -130,6 +139,92 @@ public class ProfileTaskServiceHandler extends
ProfileTaskGrpc.ProfileTaskImplBa
};
}
+ @Override
+ public StreamObserver<GoProfileData>
goProfileReport(StreamObserver<Commands> responseObserver) {
+ return new StreamObserver<GoProfileData>() {
+ private ByteArrayOutputStream profileDataBuffer = new
ByteArrayOutputStream();
+ private String currentTaskId = null;
+
+ @Override
+ public void onNext(GoProfileData profileData) {
+ LOGGER.debug("receive go profile data: taskId='{}',
payloadSize={}, isLast={}",
+ profileData.getTaskId(),
+ profileData.getPayload().size(),
+ profileData.getIsLast());
+
+ // Check if taskId is empty - this indicates a problem with Go
agent
+ if (profileData.getTaskId() == null ||
profileData.getTaskId().isEmpty()) {
+ LOGGER.error("Go agent sent empty taskId! This indicates a
problem with Go agent's profile task management. " +
+ "Please check Go agent's profile task creation
and task.TaskID assignment.");
+ return;
+ }
+
+ // Reset state if this is a new task
+ if (currentTaskId == null ||
!currentTaskId.equals(profileData.getTaskId())) {
+ currentTaskId = profileData.getTaskId();
+ profileDataBuffer.reset();
+ LOGGER.debug("Starting new task: {}", currentTaskId);
+ }
+
+ // Collect profile data
+ try {
+
profileDataBuffer.write(profileData.getPayload().toByteArray());
+ } catch (IOException e) {
+ LOGGER.error("Failed to write Go profile data", e);
+ return;
+ }
+
+ // If this is the last data chunk, parse and store
+ if (profileData.getIsLast()) {
+ try {
+ // Parse Go profile data via library-pprof-parser
(auto-detect gzip)
+ ProfileProto.Profile profile =
PprofParser.parseProfile(profileDataBuffer.toByteArray());
+ List<SegmentInfo> segments =
PprofSegmentParser.parseSegments(profile);
+
+ // Log parsed segments briefly for troubleshooting
+ if (CollectionUtils.isEmpty(segments)) {
+ LOGGER.debug("Parsed Go profile has no segments.
taskId={}, hint=check labels segment_id/trace_id", currentTaskId);
+ }
+
+ // Store ProfileThreadSnapshotRecord for each segment
+ for (SegmentInfo segmentInfo : segments) {
+ storeGoProfileSegment(segmentInfo, currentTaskId,
profile);
+ }
+
+ // Analyzer preview removed to reduce log noise after
verification
+
+ LOGGER.info("Processed Go profile data: taskId={},
segments={}", currentTaskId, segments.size());
+
+ } catch (Exception e) {
+ LOGGER.error("Failed to parse Go profile data for
task: " + currentTaskId, e);
+ } finally {
+ // Reset state
+ profileDataBuffer.reset();
+ currentTaskId = null;
+ }
+ }
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ Status status = Status.fromThrowable(throwable);
+ if (Status.CANCELLED.getCode() == status.getCode()) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(throwable.getMessage(), throwable);
+ }
+ return;
+ }
+ LOGGER.error(throwable.getMessage(), throwable);
+ }
+
+ @Override
+ public void onCompleted() {
+ responseObserver.onNext(Commands.newBuilder().build());
+ responseObserver.onCompleted();
+ }
+ };
+ }
+
@Override
public void reportTaskFinish(ProfileTaskFinishReport request,
StreamObserver<Commands> responseObserver) {
// query task from cache, set log time bucket need it
@@ -160,4 +255,55 @@ public class ProfileTaskServiceHandler extends
ProfileTaskGrpc.ProfileTaskImplBa
RecordStreamProcessor.getInstance().in(logRecord);
}
+
+
+
+
+ /**
+ * Store Go profile segment - create a filtered pprof containing only
samples for this segment
+ */
+ private void storeGoProfileSegment(SegmentInfo segmentInfo, String taskId,
ProfileProto.Profile originalProfile) {
+ try {
+ // Create a filtered pprof profile containing only samples for
this segment
+ ProfileProto.Profile.Builder filteredProfileBuilder =
originalProfile.toBuilder();
+ filteredProfileBuilder.clearSample();
+
+ // Add only samples that belong to this segment
+ for (ProfileProto.Sample sample : originalProfile.getSampleList())
{
+ String sampleSegmentId =
PprofSegmentParser.extractSegmentIdFromLabels(sample.getLabelList(),
originalProfile.getStringTableList());
+ if (segmentInfo.getSegmentId().equals(sampleSegmentId)) {
+ filteredProfileBuilder.addSample(sample);
+ }
+ }
+
+ ProfileProto.Profile filteredProfile =
filteredProfileBuilder.build();
+ byte[] filteredPprofData = filteredProfile.toByteArray();
+
+ // Create ProfileThreadSnapshotRecord for this segment
+ ProfileThreadSnapshotRecord record = new
ProfileThreadSnapshotRecord();
+ record.setTaskId(taskId);
+ record.setSegmentId(segmentInfo.getSegmentId()); // Use real
segmentId
+ long dumpTimeMs = originalProfile.getTimeNanos() > 0 ?
originalProfile.getTimeNanos() / 1_000_000L : System.currentTimeMillis();
+ record.setDumpTime(dumpTimeMs);
+ record.setSequence(0); // Each segment has only one record
+ record.setLanguage(ProfileLanguageType.GO); // mark as Go profile
data
+
+ // Store filtered pprof data containing only this segment's samples
+ record.setStackBinary(filteredPprofData);
+ record.setTimeBucket(TimeBucket.getRecordTimeBucket(dumpTimeMs));
+
+ LOGGER.info("About to store Go profile snapshot: taskId={},
segmentId={}, dumpTime={}, timeBucket={}, sequence={}, language={},
filteredDataSize={}, samples={}",
+ record.getTaskId(), record.getSegmentId(),
record.getDumpTime(), record.getTimeBucket(),
+ record.getSequence(), record.getLanguage(),
filteredPprofData.length, filteredProfile.getSampleCount());
+
+ // Store to database
+ RecordStreamProcessor.getInstance().in(record);
+ LOGGER.info("Stored Go profile snapshot: taskId={}, segmentId={},
dumpTime={}, sequence={}, language={}",
+ record.getTaskId(), record.getSegmentId(),
record.getDumpTime(), record.getSequence(), record.getLanguage());
+
+ } catch (Exception e) {
+ LOGGER.error("Failed to store Go profile segment: segmentId={},
taskId={}", segmentInfo.getSegmentId(), taskId, e);
+ }
+ }
+
}
diff --git
a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBConverter.java
b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBConverter.java
index c4dfd2491d..24256e0dae 100644
---
a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBConverter.java
+++
b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBConverter.java
@@ -31,6 +31,7 @@ import org.apache.skywalking.banyandb.v1.client.TraceWrite;
import
org.apache.skywalking.banyandb.v1.client.grpc.exception.BanyanDBException;
import org.apache.skywalking.banyandb.v1.client.metadata.Serializable;
import org.apache.skywalking.oap.server.core.analysis.Layer;
+import
org.apache.skywalking.oap.server.core.profiling.trace.ProfileLanguageType;
import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
import org.apache.skywalking.oap.server.core.analysis.metrics.Metrics;
import org.apache.skywalking.oap.server.core.analysis.record.Record;
@@ -283,6 +284,9 @@ public class BanyanDBConverter {
return TagAndValue.stringTagValue(((StorageDataComplexObject<?>)
value).toStorageData());
} else if (Layer.class.equals(clazz)) {
return TagAndValue.longTagValue(((Integer) value).longValue());
+ } else if (ProfileLanguageType.class.equals(clazz)) {
+ // Mirror Layer handling: value is provided as Integer (enum
ordinal/value)
+ return TagAndValue.longTagValue(((Integer) value).longValue());
} else if (JsonObject.class.equals(clazz)) {
return TagAndValue.stringTagValue((String) value);
} else if (byte[].class.equals(clazz)) {
@@ -302,6 +306,11 @@ public class BanyanDBConverter {
return TagAndValue.binaryFieldValue(ByteUtil.double2Bytes((double)
value));
} else if (StorageDataComplexObject.class.isAssignableFrom(clazz)) {
return TagAndValue.stringFieldValue(((StorageDataComplexObject<?>)
value).toStorageData());
+ } else if (Layer.class.equals(clazz)) {
+ return TagAndValue.longFieldValue(((Integer) value).longValue());
+ } else if (ProfileLanguageType.class.equals(clazz)) {
+ // Mirror Layer handling: value is provided as Integer (enum
ordinal/value)
+ return TagAndValue.longFieldValue(((Integer) value).longValue());
}
throw new IllegalStateException(clazz.getSimpleName() + " is not
supported");
}
diff --git
a/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/base/ColumnTypeEsMapping.java
b/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/base/ColumnTypeEsMapping.java
index 914154ea40..02fd400181 100644
---
a/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/base/ColumnTypeEsMapping.java
+++
b/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/base/ColumnTypeEsMapping.java
@@ -24,12 +24,13 @@ import java.lang.reflect.Type;
import java.util.List;
import org.apache.skywalking.oap.server.core.analysis.Layer;
import
org.apache.skywalking.oap.server.core.storage.model.ElasticSearchExtension;
+import
org.apache.skywalking.oap.server.core.profiling.trace.ProfileLanguageType;
import
org.apache.skywalking.oap.server.core.storage.type.StorageDataComplexObject;
public class ColumnTypeEsMapping {
public String transform(Class<?> type, Type genericType, int length,
boolean storageOnly, final ElasticSearchExtension elasticSearchExtension) {
- if (Integer.class.equals(type) || int.class.equals(type) ||
Layer.class.equals(type)) {
+ if (Integer.class.equals(type) || int.class.equals(type) ||
Layer.class.equals(type) || ProfileLanguageType.class.equals(type)) {
return "integer";
} else if (Long.class.equals(type) || long.class.equals(type)) {
return "long";
diff --git
a/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/JDBCTableInstaller.java
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/JDBCTableInstaller.java
index ad5f7c860d..57079de7ce 100644
---
a/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/JDBCTableInstaller.java
+++
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/common/JDBCTableInstaller.java
@@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.oap.server.core.analysis.DownSampling;
import org.apache.skywalking.oap.server.core.analysis.Layer;
import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
+import
org.apache.skywalking.oap.server.core.profiling.trace.ProfileLanguageType;
import org.apache.skywalking.oap.server.core.storage.StorageData;
import org.apache.skywalking.oap.server.core.storage.model.ColumnName;
import org.apache.skywalking.oap.server.core.storage.model.Model;
@@ -111,7 +112,7 @@ public class JDBCTableInstaller extends ModelInstaller {
protected String getColumnDefinition(ModelColumn column, Class<?> type,
Type genericType) {
final String storageName = column.getColumnName().getStorageName();
- if (Integer.class.equals(type) || int.class.equals(type) ||
Layer.class.equals(type)) {
+ if (Integer.class.equals(type) || int.class.equals(type) ||
Layer.class.equals(type) || ProfileLanguageType.class.equals(type)) {
return storageName + " INT";
} else if (Long.class.equals(type) || long.class.equals(type)) {
return storageName + " BIGINT";
diff --git
a/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/postgresql/PostgreSQLTableInstaller.java
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/postgresql/PostgreSQLTableInstaller.java
index fd76087b4b..c0a775b3c2 100644
---
a/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/postgresql/PostgreSQLTableInstaller.java
+++
b/oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/jdbc/postgresql/PostgreSQLTableInstaller.java
@@ -29,6 +29,7 @@ import
org.apache.skywalking.oap.server.storage.plugin.jdbc.common.JDBCTableInst
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
+import
org.apache.skywalking.oap.server.core.profiling.trace.ProfileLanguageType;
public class PostgreSQLTableInstaller extends JDBCTableInstaller {
public PostgreSQLTableInstaller(Client client, ModuleManager
moduleManager) {
@@ -48,7 +49,7 @@ public class PostgreSQLTableInstaller extends
JDBCTableInstaller {
@Override
protected String getColumnDefinition(ModelColumn column, Class<?> type,
Type genericType) {
final String storageName = column.getColumnName().getStorageName();
- if (Integer.class.equals(type) || int.class.equals(type) ||
Layer.class.equals(type)) {
+ if (Integer.class.equals(type) || int.class.equals(type) ||
Layer.class.equals(type) || ProfileLanguageType.class.equals(type)) {
return storageName + " INT";
} else if (Long.class.equals(type) || long.class.equals(type)) {
return storageName + " BIGINT";
diff --git a/test/e2e-v2/cases/go/service/e2e.go
b/test/e2e-v2/cases/go/service/e2e.go
index 833edab181..febb593d8c 100644
--- a/test/e2e-v2/cases/go/service/e2e.go
+++ b/test/e2e-v2/cases/go/service/e2e.go
@@ -63,5 +63,25 @@ func main() {
context.String(200, "Nobody cares me.")
})
+ engine.Handle("GET", "/profile", func(context *gin.Context) {
+ log.Printf("=== /profile endpoint called ===")
+ log.Printf("Starting profiling work...")
+ doWork()
+ log.Printf("Profiling work completed")
+ context.String(200, "Profiling completed")
+ })
+
_ = engine.Run(":8080")
}
+
+func doWork() {
+ log.Printf("doWork() started")
+ start := time.Now()
+ for time.Since(start) < 10*time.Second {
+ _ = 1
+ for i := 0; i < 1e6; i++ {
+ _ = i * i
+ }
+ }
+ log.Printf("doWork() completed after %v", time.Since(start))
+}
\ No newline at end of file
diff --git a/test/e2e-v2/cases/go/service/go.mod
b/test/e2e-v2/cases/go/service/go.mod
index 1e2b1db6e3..2197662dbd 100644
--- a/test/e2e-v2/cases/go/service/go.mod
+++ b/test/e2e-v2/cases/go/service/go.mod
@@ -61,5 +61,4 @@ require (
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- skywalking.apache.org/repo/goapi v0.0.0-20230314034821-0c5a44bb767a //
indirect
)
diff --git a/test/e2e-v2/cases/profiling/trace/go/docker-compose.yml
b/test/e2e-v2/cases/profiling/trace/go/docker-compose.yml
new file mode 100644
index 0000000000..d1df1d9c80
--- /dev/null
+++ b/test/e2e-v2/cases/profiling/trace/go/docker-compose.yml
@@ -0,0 +1,65 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+version: '2.1'
+
+services:
+ oap:
+ extends:
+ file: ../../../../script/docker-compose/base-compose.yml
+ service: oap
+ ports:
+ - 12800
+ networks:
+ - e2e
+
+ banyandb:
+ extends:
+ file: ../../../../script/docker-compose/base-compose.yml
+ service: banyandb
+ ports:
+ - 17912
+ networks:
+ - e2e
+
+ go-service:
+ build:
+ context: ../../../go/service
+ dockerfile: Dockerfile
+ args:
+ - SW_AGENT_GO_COMMIT=${SW_AGENT_GO_COMMIT}
+ networks:
+ - e2e
+ expose:
+ - 8080
+ ports:
+ - 8080
+ environment:
+ SW_AGENT_NAME: go-service
+ SW_AGENT_REPORTER_GRPC_BACKEND_SERVICE: oap:11800
+ SW_AGENT_REPORTER_GRPC_PROFILE_FETCH_INTERVAL: 1
+ SW_AGENT_COLLECTOR_GET_PROFILE_TASK_INTERVAL: 1
+ SW_AGENT_COLLECTOR_GET_AGENT_DYNAMIC_CONFIG_INTERVAL: 1
+ depends_on:
+ oap:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "sh", "-c", "nc -z 127.0.0.1 8080"]
+ interval: 5s
+ timeout: 60s
+ retries: 120
+
+networks:
+ e2e:
diff --git a/test/e2e-v2/cases/profiling/trace/go/e2e.yaml
b/test/e2e-v2/cases/profiling/trace/go/e2e.yaml
new file mode 100644
index 0000000000..326893f517
--- /dev/null
+++ b/test/e2e-v2/cases/profiling/trace/go/e2e.yaml
@@ -0,0 +1,92 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+setup:
+ env: compose
+ file: docker-compose.yml
+ timeout: 20m
+ init-system-environment: ../../../../script/env
+ steps:
+ - name: set PATH
+ command: export PATH=/tmp/skywalking-infra-e2e/bin:$PATH
+ - name: install yq
+ command: bash test/e2e-v2/script/prepare/setup-e2e-shell/install.sh yq
+ - name: install swctl
+ command: bash test/e2e-v2/script/prepare/setup-e2e-shell/install.sh swctl
+
+trigger:
+ action: http
+ interval: 3s
+ times: 60
+ url: http://${go-service_host}:${go-service_8080}/profile
+ method: GET
+
+verify:
+ retry:
+ count: 20
+ interval: 3s
+ cases:
+ # create profiling task for Go service
+ - query: |
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql \
+ profiling trace create --service-name=go-service \
+ --endpoint-name=GET:/profile \
+ --start-time=-1 \
+ --duration=1 --min-duration-threshold=1000 \
+ --dump-period=500 --max-sampling-count=3
+ expected: expected/profile-create.yml
+
+ # profiling list notified: sleep to wait agent notices and query profiling
list
+ - query: sleep 3 && swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace list
-service-name=go-service --endpoint-name=GET:/profile
+ expected: expected/profile-list-notified.yml
+
+ # profiling list finished
+ - query: |
+ sleep 3;
+ go_host=$(printenv go-service_host || printenv go_service_host)
+ go_port=$(printenv go-service_8080 || printenv go_service_8080)
+ curl -s -XGET http://$go_host:$go_port/profile > /dev/null;
+ sleep 30;
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace list
-service-name=go-service --endpoint-name=GET:/profile
+ expected: expected/profile-list-finished.yml
+
+ # profiled segment list
+ - query: |
+ sleep 30;
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace segment-list
--task-id=$( \
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace list
-service-name=go-service --endpoint-name=GET:/profile | yq e '.[0].id' - \
+ )
+ expected: expected/profile-segment-list.yml
+
+ # query profiled segment analyze
+ - query: |
+ segmentid=$( \
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace segment-list
--task-id=$( \
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace list
-service-name=go-service --endpoint-name=GET:/profile | yq e '.[0].id' - \
+ ) | yq e '.[0].spans.[] | select(.spanid == 0) | .segmentid' - \
+ );
+ start=$(
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace segment-list
--task-id=$( \
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace list
-service-name=go-service --endpoint-name=GET:/profile | yq e '.[0].id' - \
+ ) | yq e '.[0].spans.[] | select(.spanid == 0) | .starttime' - \
+ );
+ end=$(
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace segment-list
--task-id=$( \
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace list
-service-name=go-service --endpoint-name=GET:/profile | yq e '.[0].id' - \
+ ) | yq e '.[0].spans.[] | select(.spanid == 0) | .endtime' - \
+ );
+ swctl --display yaml
--base-url=http://${oap_host}:${oap_12800}/graphql profiling trace analysis
--segment-ids=$segmentid --time-ranges=$(echo $start"-"$end)
+ expected: expected/profile-segment-analyze.yml
\ No newline at end of file
diff --git a/test/e2e-v2/cases/profiling/trace/go/expected/profile-create.yml
b/test/e2e-v2/cases/profiling/trace/go/expected/profile-create.yml
new file mode 100644
index 0000000000..8f89323654
--- /dev/null
+++ b/test/e2e-v2/cases/profiling/trace/go/expected/profile-create.yml
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+id: {{ notEmpty .id }}
+errorreason: null
diff --git
a/test/e2e-v2/cases/profiling/trace/go/expected/profile-list-finished.yml
b/test/e2e-v2/cases/profiling/trace/go/expected/profile-list-finished.yml
new file mode 100644
index 0000000000..61a8437407
--- /dev/null
+++ b/test/e2e-v2/cases/profiling/trace/go/expected/profile-list-finished.yml
@@ -0,0 +1,34 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ {{- contains . }}
+- id: {{ notEmpty .id }}
+ serviceid: {{ b64enc "go-service" }}.1
+ servicename: ""
+ endpointname: GET:/profile
+ starttime: {{ gt .starttime 0 }}
+ duration: 1
+ mindurationthreshold: 1000
+ dumpperiod: 500
+ maxsamplingcount: 3
+ logs:
+ {{- contains .logs }}
+ - id: {{ notEmpty .id }}
+ instanceid: {{ notEmpty .instanceid }}
+ operationtype: {{ .operationtype }}
+ instancename: ""
+ operationtime: {{ gt .operationtime 0 }}
+ {{- end }}
+ {{- end }}
diff --git
a/test/e2e-v2/cases/profiling/trace/go/expected/profile-list-notified.yml
b/test/e2e-v2/cases/profiling/trace/go/expected/profile-list-notified.yml
new file mode 100644
index 0000000000..61a8437407
--- /dev/null
+++ b/test/e2e-v2/cases/profiling/trace/go/expected/profile-list-notified.yml
@@ -0,0 +1,34 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ {{- contains . }}
+- id: {{ notEmpty .id }}
+ serviceid: {{ b64enc "go-service" }}.1
+ servicename: ""
+ endpointname: GET:/profile
+ starttime: {{ gt .starttime 0 }}
+ duration: 1
+ mindurationthreshold: 1000
+ dumpperiod: 500
+ maxsamplingcount: 3
+ logs:
+ {{- contains .logs }}
+ - id: {{ notEmpty .id }}
+ instanceid: {{ notEmpty .instanceid }}
+ operationtype: {{ .operationtype }}
+ instancename: ""
+ operationtime: {{ gt .operationtime 0 }}
+ {{- end }}
+ {{- end }}
diff --git
a/test/e2e-v2/cases/profiling/trace/go/expected/profile-segment-analyze.yml
b/test/e2e-v2/cases/profiling/trace/go/expected/profile-segment-analyze.yml
new file mode 100644
index 0000000000..1e8d0167ce
--- /dev/null
+++ b/test/e2e-v2/cases/profiling/trace/go/expected/profile-segment-analyze.yml
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+tip: null
+trees:
+ {{- if .trees }}
+ {{- contains .trees }}
+ - elements:
+ {{- contains .elements }}
+ - id: "{{ notEmpty .id }}"
+ parentid: "{{ notEmpty .parentid }}"
+ codesignature: "{{ notEmpty .codesignature }}"
+ duration: {{ gt .duration 0 }}
+ durationchildexcluded: 0
+ count: {{ gt .count 0 }}
+ {{- end }}
+ {{- end }}
+ {{- else }}
+ []
+ {{- end }}
diff --git
a/test/e2e-v2/cases/profiling/trace/go/expected/profile-segment-list.yml
b/test/e2e-v2/cases/profiling/trace/go/expected/profile-segment-list.yml
new file mode 100644
index 0000000000..14103b5b29
--- /dev/null
+++ b/test/e2e-v2/cases/profiling/trace/go/expected/profile-segment-list.yml
@@ -0,0 +1,52 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+{{- contains . }}
+- duration: {{ gt .duration 0 }}
+ endpointnames:
+ - GET:/profile
+ instanceid: {{ notEmpty .instanceid }}
+ instancename: {{ notEmpty .instancename }}
+ spans:
+ {{- contains .spans }}
+ - component: Gin
+ endpointname: GET:/profile
+ endtime: {{ gt .endtime 0 }}
+ iserror: false
+ layer: Http
+ logs: []
+ parentspanid: -1
+ peer: ""
+ profiled: true
+ refs: []
+ segmentid: {{ notEmpty .segmentid }}
+ servicecode: go-service
+ serviceinstancename: {{ notEmpty .serviceinstancename }}
+ spanid: 0
+ starttime: {{ gt .starttime 0 }}
+ tags:
+ {{- contains .tags }}
+ - key: http.method
+ value: GET
+ - key: url
+ value: {{ notEmpty .value }}
+ - key: status_code
+ value: "200"
+ {{- end }}
+ type: Entry
+ {{- end }}
+ start: {{ notEmpty .start }}
+ traceid: {{ notEmpty .traceid }}
+{{- end }}