diff --git a/.github/workflows/skywalking.yaml b/.github/workflows/skywalking.yaml index 0850b6750dc8..57bd56f15f02 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 caa250e63cdc..d1219e1a17d9 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 000000000000..8faf53b8104b --- /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 bd04334a1c8f..98d048e84ee1 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 ProfileThreadSnapshotRecord storage2Entity(final Convert2Entity converter 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 void entity2Storage(final ProfileThreadSnapshotRecord storageData, final 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 000000000000..fe2a9e53b65e --- /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 key2Id = new HashMap<>(); // "parentId|name" -> id + List elements = new ArrayList<>(); + + // Strict per-segment filtering + final List 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 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 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> childrenMap = new HashMap<>(); + for (ProfileStackElement e : elements) { + childrenMap.computeIfAbsent(e.getParentId(), k -> new ArrayList<>()).add(e); + } + + List ordered = new ArrayList<>(); + ArrayDeque 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 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 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 records, List queries) { + ProfileAnalyzation result = new ProfileAnalyzation(); + + // Build query map for O(1) lookup + Map 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 65d67cd454c4..6a80e6f10dc3 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.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 ProfileAnalyzer(ModuleManager moduleManager, int snapshotAnalyzeBatchSize public ProfileAnalyzation analyze(final List 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 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 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.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 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 trees = analyzeByStack(stacks); + if (trees != null && !trees.isEmpty()) { + analyzation.getTrees().addAll(trees); + // Java analysis found data, return early + return analyzation; + } } - // query snapshots - List 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 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 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.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 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 @@ protected SequenceSearch getAllSequenceRange(String segmentId, long start, long 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 int getMaxSequence() { } } + } 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 2475a1969477..260b2352329e 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 static FrameTree dumpTree(String filePath) throws IOException { 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 000000000000..7fcba93be24a --- /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 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 getStack() { + return stack; + } + + public void setStack(List 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 parseSegments(ProfileProto.Profile profile) { + List stringTable = profile.getStringTableList(); + + // Group samples by segmentId + Map> 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 result = new ArrayList<>(segmentSamples.size()); + for (Map.Entry> entry : segmentSamples.entrySet()) { + String segmentId = entry.getKey(); + List 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 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 labels, List 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 labels, List 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 labels, List 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 labels, List 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 extractCombinedStackFromSamples(List samples, ProfileProto.Profile profile) { + Set uniqueStack = new LinkedHashSet<>(); + + for (ProfileProto.Sample sample : samples) { + List stack = extractStackFromSample(sample, profile); + uniqueStack.addAll(stack); + } + + return new ArrayList<>(uniqueStack); + } + + /** + * Extract call stack from a single sample + */ + public static List extractStackFromSample(ProfileProto.Sample sample, ProfileProto.Profile profile) { + List 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 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 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 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 43c7d47f64de..ce893cba2889 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 lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.skywalking.oap.server.library.pprof.parser.PprofParser; @Data @NoArgsConstructor @@ -58,15 +59,7 @@ private FrameTree parseTree(RawFrameTree rawTree) { } 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 f5f75875b556..5eb3a966e8dd 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 @@ skywalking-sharing-server-plugin ${project.version} + + + org.apache.skywalking + library-pprof-parser + ${project.version} + \ 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 1de09417fff9..e30556b03ef2 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 @@ 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.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 void onNext(ThreadSnapshot snapshot) { 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 void onCompleted() { }; } + @Override + public StreamObserver goProfileReport(StreamObserver responseObserver) { + return new StreamObserver() { + 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 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 responseObserver) { // query task from cache, set log time bucket need it @@ -160,4 +255,55 @@ private void recordProfileTaskLog(ProfileTask task, String instanceId, ProfileTa 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 c4dfd2491d23..24256e0dae1a 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.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 @@ private static Serializable buildTag(Object value, final 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 @@ private static Serializable buildField(Object value, f 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 914154ea4078..02fd400181ee 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.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 ad5f7c860d85..57079de7ce09 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 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 String getColumnDefinition(ModelColumn column) { 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 fd76087b4bcd..c0a775b3c25d 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 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 void start() { @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 833edab181d3..febb593d8c58 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 1e2b1db6e369..2197662dbd58 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 000000000000..d1df1d9c806c --- /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 000000000000..326893f51759 --- /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 000000000000..8f89323654cb --- /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 000000000000..61a843740748 --- /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 000000000000..61a843740748 --- /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 000000000000..1e8d0167ce59 --- /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 000000000000..14103b5b294a --- /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 }}