From 8500ae6c20ef52073005153225aa7c5baf99c66c Mon Sep 17 00:00:00 2001 From: dutexion Date: Mon, 12 Aug 2024 21:53:31 +0900 Subject: [PATCH] feat :: trace information --- src/components/Trace/TraceInformation.tsx | 214 ++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/components/Trace/TraceInformation.tsx diff --git a/src/components/Trace/TraceInformation.tsx b/src/components/Trace/TraceInformation.tsx new file mode 100644 index 0000000..19bf417 --- /dev/null +++ b/src/components/Trace/TraceInformation.tsx @@ -0,0 +1,214 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { theme } from '@/style/theme'; +import styled from '@emotion/styled'; +import axios from 'axios'; + +interface Span { + spanId: string; + parentSpanId?: string; + name: string; + startTimeUnixNano: string; + endTimeUnixNano: string; + attributes: Array<{ + key: string; + value: { + stringValue?: string; + intValue?: number; + }; + }>; + children?: Span[]; + depth?: number; +} + +interface Trace { + batches: Array<{ + scopeSpans: Array<{ + spans: Span[]; + }>; + }>; +} + +type PropsType = { + selectedTrace: string | null; + setSelectedTrace: React.Dispatch>; +}; + +export const TraceInformation = ({ selectedTrace, setSelectedTrace }: PropsType) => { + const [trace, setTrace] = useState(null); + const [selectedSpan, setSelectedSpan] = useState(null); + const elementRef = useRef(null); + const [elementWidth, setElementWidth] = useState(elementRef?.current?.offsetWidth ?? 0); + + useEffect(() => { + if (!elementRef.current) return; + + const measureWidth = () => { + if (elementRef.current) { + setElementWidth(elementRef.current.offsetWidth); + } + }; + + measureWidth(); + window.addEventListener('resize', measureWidth); + + return () => { + window.removeEventListener('resize', measureWidth); + }; + }, [selectedTrace]); + + useEffect(() => { + const fetchTrace = async () => { + try { + const response = await axios.get(`https://grafana-tempo.xquare.app/api/traces/${selectedTrace}`); + setTrace(response.data); + } catch (error) { + console.error('Error fetching trace data:', error); + } + }; + + if (selectedTrace) { + fetchTrace(); + } + }, [selectedTrace]); + + if (!trace) { + return null; + } + + const spans = trace.batches.flatMap((batch) => batch.scopeSpans.flatMap((scope) => scope.spans)); + + const organizeSpans = (spans: Span[]): Span[] => { + const spanMap = new Map(spans.map((span) => [span.spanId, { ...span, children: [] as Span[] }])); + const rootSpans: Span[] = []; + + spanMap.forEach((span) => { + if (span.parentSpanId && spanMap.has(span.parentSpanId)) { + spanMap.get(span.parentSpanId)!.children!.push(span); + } else { + rootSpans.push(span); + } + }); + + const flattenSpans = (span: Span, depth: number = 0): Span[] => { + return [{ ...span, depth }, ...span.children!.flatMap((child) => flattenSpans(child, depth + 1))]; + }; + + return rootSpans.flatMap((span) => flattenSpans(span)); + }; + + const organizedSpans = organizeSpans(spans); + + const traceStart = Math.min(...spans.map((s) => Number(s.startTimeUnixNano))); + const traceEnd = Math.max(...spans.map((s) => Number(s.endTimeUnixNano))); + const traceDuration = traceEnd - traceStart; + + const svgWidth = elementWidth; + const svgHeight = organizedSpans.length * 42; + const barHeight = 40; + + const handleSpanClick = (span: Span) => { + setSelectedSpan(span); + }; + + return ( + + + +

Trace View

+ { + setSelectedTrace(null); + }} + > + 닫기 + +
+ + {organizedSpans.map((span, index) => { + const startOffset = ((Number(span.startTimeUnixNano) - traceStart) / traceDuration) * svgWidth; + const duration = ((Number(span.endTimeUnixNano) - Number(span.startTimeUnixNano)) / traceDuration) * svgWidth; + const yPosition = index * 42; + + return ( + handleSpanClick(span)} style={{ cursor: 'pointer' }}> + + + {span.name} ({(Number(span.endTimeUnixNano) - Number(span.startTimeUnixNano)) / 1e6}ms) + + + ); + })} + + {selectedSpan && } +
+ ); +}; + +const SpanDetails: React.FC<{ span: Span }> = ({ span }) => { + return ( +
+

Span Details

+

+ Name: {span.name} +

+

+ ID: {span.spanId} +

+

+ Parent ID: {span.parentSpanId || 'None'} +

+

+ Duration: {(Number(span.endTimeUnixNano) - Number(span.startTimeUnixNano)) / 1e6}ms +

+

Attributes:

+
    + {span.attributes.map((attr, index) => ( +
  • + {attr.key}: {attr.value.stringValue || attr.value.intValue} +
  • + ))} +
+
+ ); +}; + +const Wrapper = styled.div<{ selectedTrace: string | null }>` + position: fixed; + bottom: 0; + right: ${({ selectedTrace }) => (Boolean(selectedTrace) ? '0' : '-1500px')}; + border-left: 1px solid ${theme.color.gray4}; + width: calc(100vw - 460px); + height: calc(100vh - 80px); + overflow-y: auto; + background-color: ${theme.color.gray1}; + transition: right 0.7s ease-in-out; + z-index: 1; +`; + +const TitleContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding-right: 50px; + > b { + color: red; + cursor: pointer; + } +`; + +const ColorBox = styled.div` + width: 100%; + height: 14px; + background-color: ${theme.color.main}; +`;