diff --git a/package.json b/package.json
index 4147223..ff872cf 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0",
"@iconify/react": "^4.1.1",
+ "@pyroscope/flamegraph": "^0.35.6",
"@tanstack/react-query": "^5.35.1",
"@types/plotly.js": "^2.33.3",
"@types/plotly.js-dist-min": "^2.3.4",
diff --git a/src/components/Trace/FlameGraph.tsx b/src/components/Trace/FlameGraph.tsx
new file mode 100644
index 0000000..55549e8
--- /dev/null
+++ b/src/components/Trace/FlameGraph.tsx
@@ -0,0 +1,76 @@
+import { useMemo } from 'react';
+import '@pyroscope/flamegraph/dist/index.css';
+import { FlamegraphRenderer } from '@pyroscope/flamegraph';
+import { SpansType, SpanType } from '@/utils/types/traceType.ts';
+
+export const FlameGraph = ({ spans } : SpansType) => {
+ const flamegraphData = useMemo(() => convertTraceToFlamegraph(spans.spans), [spans]);
+
+ if (!flamegraphData) {
+ return
No valid flamegraph data available
;
+ }
+
+ return (
+
+ );
+};
+
+// 매개변수를 SpanType[] 타입으로 유지합니다.
+function convertTraceToFlamegraph(spans: SpanType[]) {
+ if (spans.length === 0) return null;
+
+ const nameMap: { [key: string]: number } = {};
+ const levels: number[][] = [];
+
+ if(!spans[0]) return;
+
+ const traceStartTime = spans[0].start_time_unix_nano;
+ const traceEndTime = spans[0].end_time_unix_nano;
+ const traceDuration = traceEndTime - traceStartTime;
+
+ function addToNameMap(name: string): number {
+ if (nameMap[name] === undefined) {
+ nameMap[name] = Object.keys(nameMap).length;
+ }
+ return nameMap[name];
+ }
+
+ spans.forEach((span, index) => {
+ const startTime = span.start_time_unix_nano;
+ const endTime = span.end_time_unix_nano;
+ const duration = endTime - startTime;
+ const startOffset = startTime - traceStartTime;
+ const nameIndex = addToNameMap(span.name);
+
+ if (!levels[index]) levels[index] = [];
+ levels[index].push(startOffset, duration, 0, nameIndex);
+ });
+
+ const rootSpan = spans[0];
+ return {
+ version: 1,
+ flamebearer: {
+ names: Object.keys(nameMap),
+ levels,
+ numTicks: traceDuration,
+ maxSelf: Math.max(...levels.flat()),
+ },
+ metadata: {
+ appName: 'your-app-name',
+ name: `${rootSpan.name} Flamegraph`,
+ startTime: traceStartTime,
+ endTime: traceEndTime,
+ query: 'your-app-query',
+ maxNodes: 1024,
+ format: 'single' as const,
+ sampleRate: 1000000000,
+ spyName: 'opentelemetry' as const,
+ units: 'trace_samples' as const, // 'milliseconds' 대신 'samples' 사용
+ },
+ };
+}
diff --git a/src/components/Trace/TraceInformation.tsx b/src/components/Trace/TraceInformation.tsx
index 5baeda6..c22c3d5 100644
--- a/src/components/Trace/TraceInformation.tsx
+++ b/src/components/Trace/TraceInformation.tsx
@@ -4,6 +4,7 @@ import styled from '@emotion/styled';
import { useResizable } from '@/hooks/useResizable';
import { getDetailTrace } from '@/utils/apis/trace';
import { SpanType } from '@/utils/types/traceType';
+import { FlameGraph } from '@/components/Trace/FlameGraph.tsx';
type PropsType = {
selectedTrace: string | null;
@@ -11,13 +12,15 @@ type PropsType = {
};
export const TraceInformation = ({ selectedTrace, setSelectedTrace }: PropsType) => {
- const { width, boxRef, handleMouseDown } = useResizable(400, 300, window.innerWidth - 280);
+ const { width, boxRef, handleMouseDown } = useResizable(800, 300, window.innerWidth - 280);
const [data, setData] = useState();
+ console.log(data);
+
useEffect(() => {
if (selectedTrace) {
getDetailTrace(selectedTrace).then((res) => {
- setData(res.data.spans);
+ setData(res.data);
});
}
}, [selectedTrace]);
@@ -32,6 +35,9 @@ export const TraceInformation = ({ selectedTrace, setSelectedTrace }: PropsType)
Trace View
setSelectedTrace(null)}>닫기
+
+ {selectedTrace && }
+
)}
@@ -39,49 +45,49 @@ export const TraceInformation = ({ selectedTrace, setSelectedTrace }: PropsType)
};
const Wrapper = styled.div<{ selectedTrace: string | null; width: number }>`
- position: fixed;
- top: 80px;
- right: 0;
- width: ${({ width }) => `${width}px`};
- height: calc(100vh - 80px);
- background-color: ${theme.color.gray1};
- z-index: 1;
- display: ${({ selectedTrace }) => (selectedTrace ? 'flex' : 'none')};
- flex-direction: row;
+ position: fixed;
+ top: 80px;
+ right: 0;
+ width: ${({ width }) => `${width}px`};
+ height: calc(100vh - 80px);
+ background-color: ${theme.color.gray1};
+ z-index: 1;
+ display: ${({ selectedTrace }) => (selectedTrace ? 'flex' : 'none')};
+ flex-direction: row;
`;
const Content = styled.div`
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow-y: auto;
- border-left: 1px solid ${theme.color.gray4};
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ border-left: 1px solid ${theme.color.gray4};
`;
const Resizer = styled.div`
- width: 4px;
- height: 100%;
- background-color: rgba(0, 0, 0, 0);
- cursor: ew-resize;
- transition: 0.2s linear;
- :hover {
- background-color: ${theme.color.gray3};
- }
+ width: 4px;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0);
+ cursor: ew-resize;
+ transition: 0.2s linear;
+ :hover {
+ background-color: ${theme.color.gray3};
+ }
`;
const TitleContainer = styled.div`
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0 20px;
- > b {
- color: red;
- cursor: pointer;
- }
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 20px;
+ > b {
+ color: red;
+ cursor: pointer;
+ }
`;
const ColorBox = styled.div`
- width: 100%;
- height: 14px;
- background-color: ${theme.color.main};
+ width: 100%;
+ height: 14px;
+ background-color: ${theme.color.main};
`;
diff --git a/src/utils/types/traceType.ts b/src/utils/types/traceType.ts
index 532b80b..6a560b6 100644
--- a/src/utils/types/traceType.ts
+++ b/src/utils/types/traceType.ts
@@ -17,3 +17,7 @@ export type SpanType = {
[key: string]: string;
};
};
+
+export type SpansType = {
+ spans: SpanType[]
+}
\ No newline at end of file