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