Compare commits

...

66 Commits

Author SHA1 Message Date
vikrantgupta25
cc49c3958c feat(trace-detail): merge main 2025-01-22 16:57:18 +05:30
vikrantgupta25
128623ebe0 feat(trace0-detail): handle old and new trace detail routing 2025-01-22 16:44:38 +05:30
vikrantgupta25
c22196c931 feat(trace0-detail): handle old and new trace detail routing 2025-01-22 16:40:34 +05:30
vikrantgupta25
6114a60f6d feat(trace-detail): added support for go to related logs 2025-01-22 16:12:49 +05:30
vikrantgupta25
b29803e6a4 feat(trace-detail): polish the UI for trace detail 2025-01-22 15:53:01 +05:30
vikrantgupta25
a642f60793 feat(trace-detail): polish the UI for trace detail 2025-01-22 15:52:40 +05:30
Vikrant Gupta
497ef1ed4b Merge branch 'main' into query-service-waterfall 2025-01-22 01:23:59 +05:30
vikrantgupta25
e4e6353d13 fix(trace-detail): json payload size fix 2025-01-22 01:01:18 +05:30
vikrantgupta25
c89a9cadd7 fix(trace-detail): improve some cache and UI 2025-01-21 20:14:49 +05:30
Vikrant Gupta
dc5e59c70a Merge branch 'main' into query-service-waterfall 2025-01-21 14:01:40 +05:30
vikrantgupta25
1af27a32f2 feat(trace-detail): handle missing span banner UI 2025-01-21 13:59:32 +05:30
vikrantgupta25
abfc6c29e9 feat(trace-detail): handle missing span banner UI 2025-01-21 13:47:48 +05:30
vikrantgupta25
8b7082263d feat(trace-detail): handle missing span functionality 2025-01-21 13:34:04 +05:30
vikrantgupta25
670387f751 fix(trace-detail): remove bad code due to bad rebase 2025-01-21 00:53:12 +05:30
vikrantgupta25
da676b729d feat(trace-detail): merge branch 'main' into query-service-waterfall 2025-01-21 00:48:47 +05:30
vikrantgupta25
d9dd81f94b feat(trace-detail): added flux interval for caching 2025-01-16 16:50:05 +05:30
vikrantgupta25
dee22a772f feat(trace-detail): some minor UI fixes 2025-01-16 15:50:02 +05:30
vikrantgupta25
fe27921e12 feat(trace-waterfall): use OTEL SpanRefs instead of parent span id 2025-01-16 15:39:22 +05:30
vikrantgupta25
b64edc2bec feat(trace-detail): handle the infinte loading and selectedSpanID for flamegraph 2025-01-16 14:47:31 +05:30
vikrantgupta25
ef21834fb0 feat(trace-detail): fixed the UI for progress indicators 2025-01-15 19:54:26 +05:30
Vikrant Gupta
709cbb0ab5 Merge branch 'main' into query-service-waterfall 2025-01-15 17:28:16 +05:30
vikrantgupta25
2669b6e6ad feat(trace-detail): add events table 2025-01-15 17:26:21 +05:30
Vikrant Gupta
04082d8bfc Merge branch 'main' into query-service-waterfall 2025-01-15 10:28:47 +05:30
vikrantgupta25
038cf3b144 feat(trace-detail): added attribute table for span details 2025-01-15 00:40:22 +05:30
vikrantgupta25
4276918528 feat(trace-detail): added attribute table for span details 2025-01-14 23:29:17 +05:30
vikrantgupta25
7427fd36bb feat(trace-detail): added the generic detail drawer and span info 2025-01-14 19:32:43 +05:30
vikrantgupta25
84ca704e10 feat(trace-detail): frontend changes for exec times 2025-01-14 17:59:07 +05:30
vikrantgupta25
9d623bc819 feat(trace-detail): remove extra comma 2025-01-14 16:14:17 +05:30
vikrantgupta25
aaf41f4b3b feat(flamegraph): color the flamegraph based on the service name 2025-01-14 16:09:09 +05:30
vikrantgupta25
5684e11047 feat(trace-detail): query service flamegraph API cleanup 2025-01-14 16:01:13 +05:30
vikrantgupta25
d758cc28d0 feat(trace-detail): query service waterfall API cleanup 2025-01-14 13:13:47 +05:30
Vikrant Gupta
8bc182317c Merge branch 'main' into query-service-waterfall 2025-01-14 10:43:51 +05:30
Vikrant Gupta
837170d284 feat(flamegraph): new trace details flamegraph (#6789)
* feat(flamegraph): added flamegraph list api

* feat(flamegraph): added frontend base setup for flamegraph

* feat(flamegraph): improve the API response and virtuoso addition

* feat(flamegraph): build the flamegraph UI

* feat(flamegraph): added metadata UI changes

* feat(flamegraph): set the entire UI scaffolding

* feat(flamegraph): minor UI enhancements

* feat(flamegraph): minor UI enhancements

* feat(flamegraph): refactor required for metadata handling

* feat(flamegraph): add the missing metadata in the trace details response

* feat(flamegraph): added route tabs in the UI

* feat(flamegraph): added route tabs in the UI

* feat(flamegraph): added route tabs in the UI
2025-01-14 10:43:09 +05:30
vikrantgupta25
025b02d41a feat(waterfall): implement sticky headers and col resize 2025-01-09 20:05:17 +05:30
vikrantgupta25
180996a4e6 feat(waterfall): some css fixes 2025-01-08 23:00:41 +05:30
Vikrant Gupta
212d68661d Merge branch 'main' into query-service-waterfall 2025-01-08 22:25:34 +05:30
vikrantgupta25
2025365e2a feat(waterfall): some more processing to handle waterfall collapse nodes 2025-01-08 22:07:43 +05:30
vikrantgupta25
0cb710e627 feat(waterfall): handle infinite scroll 2025-01-08 19:04:36 +05:30
vikrantgupta25
b78dcc21eb feat(waterfall): merge branch 'main' into query-service-waterfall 2025-01-08 18:31:49 +05:30
vikrantgupta25
af4b8a8278 feat(waterfall): handle the interested span and uncollapsed nodes better 2025-01-08 18:28:29 +05:30
vikrantgupta25
e5ab9840cf feat(waterfall): virtualise the trace waterfall and add scrollToIndeX function 2025-01-08 18:16:45 +05:30
vikrantgupta25
0fb9f53fb0 feat(waterfall): make the react-table virtualised 2025-01-08 17:03:52 +05:30
vikrantgupta25
e1750a142b feat(waterfall): fix build issues 2025-01-08 13:40:45 +05:30
Vikrant Gupta
11ea662b01 Merge branch 'main' into query-service-waterfall 2025-01-08 12:58:08 +05:30
Vikrant Gupta
61b3b45196 Merge branch 'main' into query-service-waterfall 2025-01-07 20:03:31 +05:30
vikrantgupta25
81a0ef6be0 feat(waterfall): intersection observer changes 2025-01-07 20:03:08 +05:30
vikrantgupta25
e3ff469e67 feat(waterfall): timeline UI changes for header and cells 2025-01-07 17:40:02 +05:30
Vikrant Gupta
25404e8c10 Merge branch 'main' into query-service-waterfall 2025-01-07 14:09:30 +05:30
vikrantgupta25
457f959ed7 feat(waterfall): some minor UI enhancements 2025-01-06 18:31:55 +05:30
vikrantgupta25
74eb63772f feat(waterfall): start building the waterfall UI 2025-01-06 16:30:01 +05:30
Vikrant Gupta
cea901077c Merge branch 'main' into query-service-waterfall 2025-01-06 11:20:51 +05:30
vikrantgupta25
a0eefda0f5 feat(trace-details): integrate the new cache for trace details 2025-01-04 12:53:10 +05:30
Vikrant Gupta
efa9b5a9dd Merge branch 'main' into query-service-waterfall 2025-01-04 12:14:02 +05:30
vikrantgupta25
04a0c75dbc feat: handle the uncollapsed node and span search better 2024-12-28 21:08:53 +05:30
Vikrant Gupta
5370b80122 Merge branch 'new-trace-detail' into query-service-waterfall 2024-12-27 16:42:48 +05:30
Vikrant Gupta
44c1f54352 Merge branch 'main' into new-trace-detail 2024-12-27 16:42:14 +05:30
vikrantgupta25
008727251a feat: base branch for trace details 2024-12-27 16:41:47 +05:30
vikrantgupta25
a451044938 feat: temporary changes for inmemory cache 2024-12-27 14:23:56 +05:30
vikrantgupta25
f9c8850642 feat: use range changed to handle loads better 2024-12-26 23:54:59 +05:30
vikrantgupta25
e9f5e77175 feat: added code comments and TODO 2024-12-26 19:59:48 +05:30
vikrantgupta25
d7e6b412a2 feat: add caching for tree at query service 2024-12-26 19:46:56 +05:30
vikrantgupta25
8d16ec37fe feat: handle the scroll to top and bottom 2024-12-26 17:13:20 +05:30
vikrantgupta25
ef971484dc feat: setup the success state for the spans waterfall model 2024-12-26 16:25:11 +05:30
vikrantgupta25
5da2b39716 feat: structure the code in sync with api lifecycle 2024-12-26 14:15:31 +05:30
vikrantgupta25
6f33842e4f feat: frontend base setup and api integration for trace v2 2024-12-24 23:37:39 +05:30
vikrantgupta25
6e1371b6a0 feat: enable waterfall tree construction at query service 2024-12-24 17:30:30 +05:30
65 changed files with 4457 additions and 66 deletions

View File

@@ -7,6 +7,7 @@ import (
"github.com/jmoiron/sqlx"
cacheV2 "go.signoz.io/signoz/pkg/cache"
basechr "go.signoz.io/signoz/pkg/query-service/app/clickhouseReader"
"go.signoz.io/signoz/pkg/query-service/interfaces"
)
@@ -17,6 +18,7 @@ type ClickhouseReader struct {
*basechr.ClickHouseReader
}
// dummy
func NewDataConnector(
localDB *sqlx.DB,
promConfigPath string,
@@ -27,8 +29,10 @@ func NewDataConnector(
cluster string,
useLogsNewSchema bool,
useTraceNewSchema bool,
fluxInterval time.Duration,
cacheV2 cacheV2.Cache,
) *ClickhouseReader {
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema)
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema, fluxInterval, cacheV2)
return &ClickhouseReader{
conn: ch.GetConn(),
appdb: localDB,

View File

@@ -140,10 +140,24 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
var c cache.Cache
if serverOptions.CacheConfigPath != "" {
cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath)
if err != nil {
return nil, err
}
c = cache.NewCache(cacheOpts)
}
// set license manager as feature flag provider in dao
modelDao.SetFlagProvider(lm)
readerReady := make(chan bool)
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
if err != nil {
return nil, err
}
var reader interfaces.DataConnector
storage := os.Getenv("STORAGE")
if storage == "clickhouse" {
@@ -158,6 +172,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
fluxInterval,
serverOptions.SigNoz.Cache,
)
go qb.Start(readerReady)
reader = qb
@@ -172,14 +188,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
}
var c cache.Cache
if serverOptions.CacheConfigPath != "" {
cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath)
if err != nil {
return nil, err
}
c = cache.NewCache(cacheOpts)
}
<-readerReady
rm, err := makeRulesManager(serverOptions.PromConfigPath,
@@ -248,12 +256,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
telemetry.GetInstance().SetReader(reader)
telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
if err != nil {
return nil, err
}
apiOpts := api.APIHandlerOptions{
DataConnector: reader,
SkipConfig: skipConfig,

View File

@@ -120,7 +120,7 @@ func main() {
flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection.)")
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)")
flag.StringVar(&fluxInterval, "flux-interval", "5m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)")
flag.StringVar(&fluxInterval, "flux-interval", "0m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)")
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")

View File

@@ -44,6 +44,7 @@
"@sentry/webpack-plugin": "2.22.6",
"@signozhq/design-tokens": "1.1.4",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/shape": "3.5.0",

View File

@@ -0,0 +1,22 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.02102 14.418C4.02102 14.418 3.82659 14.6658 3.32106 14.6658C2.81553 14.6658 2.62109 14.418 2.62109 14.418V5.74512H4.02102V14.418Z" fill="#9E9E9E"/>
<path d="M4.02102 14.4179C4.02102 14.4179 3.82659 14.6657 3.32106 14.6657C2.81553 14.6657 2.62109 14.4179 2.62109 14.4179V11.077C2.62109 11.077 2.76331 10.8059 3.28328 10.8059C3.80325 10.8059 4.02102 11.077 4.02102 11.077V14.4179Z" fill="#BDBDBD"/>
<path d="M13.3765 14.418C13.3765 14.418 13.1821 14.6658 12.6765 14.6658C12.171 14.6658 11.9766 14.418 11.9766 14.418V5.74512H13.3765V14.418Z" fill="#9E9E9E"/>
<path d="M13.3765 14.4179C13.3765 14.4179 13.1821 14.6657 12.6765 14.6657C12.171 14.6657 11.9766 14.4179 11.9766 14.4179V11.077C11.9766 11.077 12.1188 10.8059 12.6387 10.8059C13.1587 10.8059 13.3765 11.077 13.3765 11.077V14.4179Z" fill="#BDBDBD"/>
<path d="M14.3623 9.98379H1.63633C1.46856 9.98379 1.33301 9.84825 1.33301 9.68048V5.79624C1.33301 5.62847 1.46856 5.49292 1.63633 5.49292H14.3623C14.5301 5.49292 14.6656 5.62847 14.6656 5.79624V9.68048C14.6656 9.84825 14.5301 9.98379 14.3623 9.98379Z" fill="#9E9E9E"/>
<path d="M14.3623 5.71509H1.63633C1.46856 5.71509 1.33301 5.85064 1.33301 6.01841V9.90264C1.33301 10.0704 1.46856 10.206 1.63633 10.206H14.3623C14.5301 10.206 14.6656 10.0704 14.6656 9.90264V6.01841C14.6656 5.85064 14.5301 5.71509 14.3623 5.71509Z" fill="#FFD600"/>
<path d="M2.82515 5.71509L1.33301 7.20723V9.40712L5.02504 5.71509H2.82515Z" fill="#212121"/>
<path d="M7.01027 5.71509L2.52051 10.206H4.72039L9.21016 5.71509H7.01027Z" fill="#212121"/>
<path d="M11.1969 5.71509L6.70605 10.206H8.90594L13.3957 5.71509H11.1969Z" fill="#212121"/>
<path d="M14.6658 6.43188L10.8916 10.2061H13.0915L14.6658 8.63177V6.43188Z" fill="#212121"/>
<path d="M13.5322 5.9095H11.8223C11.6623 5.9095 11.5445 5.80506 11.5823 5.6984L11.7012 4.95288H13.6511L13.77 5.6984C13.81 5.80617 13.6922 5.9095 13.5322 5.9095Z" fill="#E2A610"/>
<path d="M12.6768 4.93514C13.4567 4.93514 14.0889 4.3029 14.0889 3.52299C14.0889 2.74308 13.4567 2.11084 12.6768 2.11084C11.8969 2.11084 11.2646 2.74308 11.2646 3.52299C11.2646 4.3029 11.8969 4.93514 12.6768 4.93514Z" fill="#FFCA28"/>
<path d="M12.6761 4.56629C13.2523 4.56629 13.7194 4.0992 13.7194 3.52301C13.7194 2.94683 13.2523 2.47974 12.6761 2.47974C12.0999 2.47974 11.6328 2.94683 11.6328 3.52301C11.6328 4.0992 12.0999 4.56629 12.6761 4.56629Z" fill="#FF5722"/>
<path d="M13.652 4.96417H11.7021C11.7021 4.96417 11.7088 4.65308 12.2666 4.65308C12.8243 4.65308 13.0932 4.65308 13.0932 4.65308C13.6832 4.65308 13.652 4.96417 13.652 4.96417Z" fill="#FFCA28"/>
<path d="M12.7998 3.02315L13.0665 2.67872C13.0787 2.66317 13.1031 2.67539 13.0987 2.69428L12.9954 3.11759C12.9709 3.21647 13.0465 3.31202 13.1487 3.3098L13.5842 3.30313C13.6042 3.30313 13.6098 3.3298 13.592 3.33869L13.1965 3.52201C13.1042 3.56534 13.0776 3.68311 13.142 3.762L13.4187 4.09865C13.4309 4.1142 13.4142 4.13531 13.3965 4.12642L13.0065 3.93199C12.9154 3.88644 12.8065 3.93866 12.7843 4.03865L12.6932 4.46529C12.6887 4.48529 12.6609 4.48529 12.6576 4.46529L12.5665 4.03865C12.5454 3.93866 12.4354 3.88644 12.3443 3.93199L11.9543 4.12642C11.9365 4.13531 11.9188 4.11309 11.9321 4.09865L12.2087 3.762C12.2732 3.68311 12.2465 3.56534 12.1543 3.52201L11.7588 3.33869C11.741 3.3298 11.7465 3.30313 11.7665 3.30313L12.2021 3.3098C12.3043 3.31091 12.3798 3.21647 12.3554 3.11759L12.2521 2.69428C12.2476 2.67539 12.2721 2.66317 12.2843 2.67872L12.5509 3.02315C12.6165 3.10314 12.7376 3.10314 12.7998 3.02315Z" fill="#FFD5CA"/>
<path d="M4.17575 5.9095H2.46584C2.30584 5.9095 2.18807 5.80506 2.22585 5.6984L2.34473 4.95288H4.29463L4.41351 5.6984C4.45351 5.80617 4.33574 5.9095 4.17575 5.9095Z" fill="#E2A610"/>
<path d="M3.3223 4.93514C4.10221 4.93514 4.73445 4.3029 4.73445 3.52299C4.73445 2.74308 4.10221 2.11084 3.3223 2.11084C2.5424 2.11084 1.91016 2.74308 1.91016 3.52299C1.91016 4.3029 2.5424 4.93514 3.3223 4.93514Z" fill="#FFCA28"/>
<path d="M3.3216 4.56629C3.89779 4.56629 4.36488 4.0992 4.36488 3.52301C4.36488 2.94683 3.89779 2.47974 3.3216 2.47974C2.74541 2.47974 2.27832 2.94683 2.27832 3.52301C2.27832 4.0992 2.74541 4.56629 3.3216 4.56629Z" fill="#FF5722"/>
<path d="M4.29658 4.96417H2.34668C2.34668 4.96417 2.35335 4.65308 2.91109 4.65308C3.46884 4.65308 3.73772 4.65308 3.73772 4.65308C4.32769 4.65308 4.29658 4.96417 4.29658 4.96417Z" fill="#FFCA28"/>
<path d="M3.44337 3.02315L3.71002 2.67872C3.72224 2.66317 3.74669 2.67539 3.74224 2.69428L3.63892 3.11759C3.61447 3.21647 3.69002 3.31202 3.79224 3.3098L4.22777 3.30313C4.24777 3.30313 4.25333 3.3298 4.23555 3.33869L3.84002 3.52201C3.7478 3.56534 3.72113 3.68311 3.78557 3.762L4.06223 4.09865C4.07445 4.1142 4.05778 4.13531 4.04001 4.12642L3.65003 3.93199C3.55892 3.88644 3.45004 3.93866 3.42782 4.03865L3.33671 4.46529C3.33227 4.48529 3.30449 4.48529 3.30116 4.46529L3.21005 4.03865C3.18894 3.93866 3.07894 3.88644 2.98784 3.93199L2.59786 4.12642C2.58008 4.13531 2.56231 4.11309 2.57564 4.09865L2.85229 3.762C2.91673 3.68311 2.89007 3.56534 2.79785 3.52201L2.40231 3.33869C2.38454 3.3298 2.39009 3.30313 2.41009 3.30313L2.84562 3.3098C2.94784 3.31091 3.02339 3.21647 2.99895 3.11759L2.89562 2.69428C2.89118 2.67539 2.91562 2.66317 2.92784 2.67872L3.19449 3.02315C3.26005 3.10314 3.38115 3.10314 3.44337 3.02315Z" fill="#FFD5CA"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1 @@
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M17.947 6.906h-6.62V6.24h6.62v.666zM17.947 10.293h-6.62v-.667h6.62v.667z" fill="#616161"/><path d="M16.174 29.403h-2.553V3.442s.238-.553 1.278-.553 1.278.553 1.278.553v25.96h-.003z" fill="#9E9E9E"/><path d="M15.519 2.971v20.974H13.62v1.91c.322.001.613.04.876.097a1.3 1.3 0 011.024 1.269v2.182h.655V3.442c-.002 0-.14-.318-.657-.471z" fill="#757575"/><path d="M22.362 2.738a5.382 5.382 0 00-5.381 5.401 5.365 5.365 0 005.381 5.382 5.378 5.378 0 005.382-5.382c0-2.975-2.406-5.401-5.382-5.401z" fill="#2196F3"/><path d="M24.713 4.749c-.338-.085-1.011-.174-2.349-.174-1.338 0-2.01.087-2.349.174-.2.05-.869.328-.869 1.077v4.618c0 .253.205.46.46.46h.17v.51c0 .15.12.27.268.27h.545c.149 0 .269-.12.269-.27v-.51h3.008v.51c0 .15.12.27.27.27h.544c.148 0 .268-.12.268-.27v-.51h.174a.46.46 0 00.46-.46V5.826c0-.717-.67-1.029-.87-1.077zm-3.802.366c0-.113.09-.204.204-.204h2.494c.113 0 .204.09.204.204v.362a.204.204 0 01-.204.205h-2.494a.204.204 0 01-.204-.205v-.362zm.042 4.864a.139.139 0 01-.138.138h-.549a.469.469 0 01-.468-.469v-.246c0-.076.062-.138.137-.138h.55c.257 0 .468.209.468.469v.246zm3.973-.33a.469.469 0 01-.469.468h-.549a.138.138 0 01-.137-.138v-.246c0-.258.209-.47.468-.47h.55c.075 0 .137.063.137.139v.246zm.125-2.007c0 .297-.645.817-2.69.817-2.046 0-2.688-.482-2.688-.817V6.277c0-.089.089-.31.311-.31h4.791c.222 0 .276.224.276.31v1.365z" fill="#fff"/><path d="M18.86 24.652h-7.926a.506.506 0 01-.506-.507V14.99c0-.28.226-.506.506-.506h7.924c.28 0 .507.226.507.506v9.155c0 .28-.227.507-.504.507z" fill="#F5F5F5"/><path opacity=".8" d="M18.005 23.139h-6.216l-.018-7.235h6.218l.015 7.235z" fill="#82AEC0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M14.66 23.234v-7.33h.444v7.33h-.445z" fill="#F5F5F5"/><path d="M14.658 15.904h-2.886v.604h2.886v-.604z" fill="#616161"/><path fill-rule="evenodd" clip-rule="evenodd" d="M18.03 21.879h-6.263v-.223h6.264v.223zM18.03 20.412h-6.263v-.222h6.264v.222zM17.989 18.946H11.77v-.223h6.218v.223zM17.99 17.482h-6.223v-.223h6.224v.223z" fill="#F5F5F5"/><path d="M17.988 18.534h-2.886v.605h2.886v-.605zM14.672 19.999h-2.878v.604h2.878V20z" fill="#616161"/><path fill-rule="evenodd" clip-rule="evenodd" d="M10.935 14.817a.173.173 0 00-.174.173v9.155c0 .096.078.174.174.174h7.926a.173.173 0 00.171-.174V14.99a.173.173 0 00-.173-.173h-7.924zm-.84.173a.84.84 0 01.84-.84h7.924a.84.84 0 01.84.84v9.155a.84.84 0 01-.838.84h-7.926a.84.84 0 01-.84-.84V14.99z" fill="#9E9E9E"/><path d="M10.968 24.323c-.344.031-.344-.195-.344-.258V15.1c0-.25.204-.455.456-.455h7.693c.208 0 .304.127.262.384 0 0-.027-.164-.227-.164h-7.726a.237.237 0 00-.236.235v8.969c0 .209.122.255.122.255z" fill="#757575"/><path d="M12.268 4.933H4.695v6.577h7.573V4.933z" fill="#FFCA28"/><path d="M11.845 4.933c.233 0 .422.189.422.422v5.733a.422.422 0 01-.422.422H5.119a.422.422 0 01-.423-.422V5.355c0-.233.19-.422.423-.422h6.726zm0-.444H5.119a.868.868 0 00-.867.866v5.733c0 .478.389.867.867.867h6.726a.868.868 0 00.866-.867V5.355a.866.866 0 00-.866-.866z" fill="#9E9E9E"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.27 7.308H4.696v-.444h7.575v.444zM12.27 9.58H4.696v-.445h7.575v.444z" fill="#FFFDE7"/><path d="M7.066 5.664H5.162v.502h1.904v-.502zM6.598 10.27H5.162v.503h1.436v-.502zM11.648 10.27h-1.435v.503h1.435v-.502zM11.648 5.664h-1.435v.502h1.435v-.502zM10.301 7.968h-.826v.502h.826v-.502zM11.647 7.968h-.827v.502h.827v-.502zM7.802 7.968h-2.64v.502h2.64v-.502z" fill="#757575"/></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -43,7 +43,10 @@ export const TraceFilter = Loadable(
);
export const TraceDetail = Loadable(
() => import(/* webpackChunkName: "TraceDetail Page" */ 'pages/TraceDetail'),
() =>
import(
/* webpackChunkName: "TraceDetail Page" */ 'pages/TraceDetailV2/index'
),
);
export const UsageExplorerPage = Loadable(

View File

@@ -0,0 +1,33 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { omit } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
GetTraceFlamegraphPayloadProps,
GetTraceFlamegraphSuccessResponse,
} from 'types/api/trace/getTraceFlamegraph';
const getTraceFlamegraph = async (
props: GetTraceFlamegraphPayloadProps,
): Promise<
SuccessResponse<GetTraceFlamegraphSuccessResponse> | ErrorResponse
> => {
try {
const response = await axios.post<GetTraceFlamegraphSuccessResponse>(
`/traces/flamegraph/${props.traceId}`,
omit(props, 'traceId'),
);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getTraceFlamegraph;

View File

@@ -0,0 +1,41 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { omit } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
GetTraceV2PayloadProps,
GetTraceV2SuccessResponse,
} from 'types/api/trace/getTraceV2';
const getTraceV2 = async (
props: GetTraceV2PayloadProps,
): Promise<SuccessResponse<GetTraceV2SuccessResponse> | ErrorResponse> => {
try {
let uncollapsedSpans = [...props.uncollapsedSpans];
if (!props.isSelectedSpanIDUnCollapsed) {
uncollapsedSpans = uncollapsedSpans.filter(
(node) => node !== props.selectedSpanId,
);
}
const postData: GetTraceV2PayloadProps = {
...props,
uncollapsedSpans,
};
const response = await axios.post<GetTraceV2SuccessResponse>(
`/traces/${props.traceId}`,
omit(postData, 'traceId'),
);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getTraceV2;

View File

@@ -0,0 +1,93 @@
.details-drawer {
.ant-drawer-wrapper-body {
border-left: 1px solid var(--bg-slate-500);
}
.ant-drawer-header {
background: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-slate-500);
.ant-drawer-header-title {
display: flex;
align-items: center;
.ant-drawer-close {
margin-inline-end: 0px;
padding: 0px;
padding-right: 16px;
border-right: 1px solid var(--bg-slate-500);
}
.ant-drawer-title {
padding-left: 16px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
.ant-drawer-body {
padding: 16px;
background: var(--bg-ink-400);
&::-webkit-scrollbar {
width: 0.1rem;
}
}
.details-drawer-tabs {
margin-top: 32px;
.ant-tabs-tab {
display: flex;
align-items: center;
justify-content: center;
width: 114px;
height: 32px;
flex-shrink: 0;
padding: 7px 20px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
color: #fff;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: -0.06px;
.ant-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0px;
}
.ant-btn:hover {
background: unset;
}
}
.ant-tabs-tab-active {
background: var(--bg-slate-400);
}
.ant-tabs-tab + .ant-tabs-tab {
margin-left: 0px;
}
.ant-tabs-nav::before {
border-bottom: 0px;
}
.ant-tabs-ink-bar {
background: none;
}
}
}

View File

@@ -0,0 +1,56 @@
import './DetailsDrawer.styles.scss';
import { Drawer, Tabs, TabsProps } from 'antd';
import cx from 'classnames';
import { Dispatch, SetStateAction } from 'react';
interface IDetailsDrawerProps {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
title: string;
descriptiveContent: JSX.Element;
defaultActiveKey: string;
items: TabsProps['items'];
detailsDrawerClassName?: string;
tabBarExtraContent?: JSX.Element;
}
function DetailsDrawer(props: IDetailsDrawerProps): JSX.Element {
const {
open,
setOpen,
title,
descriptiveContent,
defaultActiveKey,
detailsDrawerClassName,
items,
tabBarExtraContent,
} = props;
return (
<Drawer
width="60%"
open={open}
afterOpenChange={setOpen}
title={title}
onClose={(): void => setOpen(false)}
className="details-drawer"
>
<div>{descriptiveContent}</div>
<Tabs
items={items}
addIcon
defaultActiveKey={defaultActiveKey}
animated
className={cx('details-drawer-tabs', detailsDrawerClassName)}
tabBarExtraContent={tabBarExtraContent}
/>
</Drawer>
);
}
DetailsDrawer.defaultProps = {
detailsDrawerClassName: '',
tabBarExtraContent: null,
};
export default DetailsDrawer;

View File

@@ -7,28 +7,47 @@ import {
Table,
useReactTable,
} from '@tanstack/react-table';
import React, { useMemo } from 'react';
import { useVirtualizer, Virtualizer } from '@tanstack/react-virtual';
import cx from 'classnames';
import React, { MutableRefObject, useEffect, useMemo } from 'react';
// here we are manually rendering the table body so that we can memoize the same for performant re-renders
function TableBody<T>({ table }: { table: Table<T> }): JSX.Element {
function TableBody<T>({
table,
virtualizer,
}: {
table: Table<T>;
virtualizer: Virtualizer<HTMLDivElement, Element>;
}): JSX.Element {
const { rows } = table.getRowModel();
return (
<div className="div-tbody">
{table.getRowModel().rows.map((row) => (
<div key={row.id} className="div-tr">
{row.getVisibleCells().map((cell) => (
<div
key={cell.id}
className="div-td"
// we are manually setting the column width here based on the calculated column vars
style={{
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
}}
>
{cell.renderValue<any>()}
</div>
))}
</div>
))}
{virtualizer.getVirtualItems().map((virtualRow, index) => {
const row = rows[virtualRow.index];
return (
<div
key={virtualRow.index}
className="div-tr"
style={{
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start - index * virtualRow.size}px)`,
}}
>
{row.getVisibleCells().map((cell) => (
<div
key={cell.id}
className="div-td"
// we are manually setting the column width here based on the calculated column vars
style={{
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
))}
</div>
);
})}
</div>
);
}
@@ -40,17 +59,24 @@ const MemoizedTableBody = React.memo(
) as typeof TableBody;
interface ITableConfig {
defaultColumnMinSize: number;
defaultColumnMaxSize: number;
defaultColumnMinSize?: number;
defaultColumnMaxSize?: number;
handleVirtualizerInstanceChanged?: (
instance: Virtualizer<HTMLDivElement, Element>,
) => void;
}
interface ITableV3Props<T> {
columns: ColumnDef<T, any>[];
data: T[];
config: ITableConfig;
customClassName?: string;
virtualiserRef?: MutableRefObject<
Virtualizer<HTMLDivElement, Element> | undefined
>;
}
export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
const { data, columns, config } = props;
const { data, columns, config, customClassName = '', virtualiserRef } = props;
const table = useReactTable({
data,
@@ -61,11 +87,26 @@ export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
},
columnResizeMode: 'onChange',
getCoreRowModel: getCoreRowModel(),
debugTable: true,
debugHeaders: true,
debugColumns: true,
// turn on debug flags to get debug logs from these instances
debugAll: false,
});
const tableRef = React.useRef<HTMLDivElement>(null);
const { rows } = table.getRowModel();
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => tableRef.current,
estimateSize: () => 54,
overscan: 20,
onChange: config.handleVirtualizerInstanceChanged,
});
useEffect(() => {
if (virtualiserRef) {
virtualiserRef.current = virtualizer;
}
}, [virtualiserRef, virtualizer]);
/**
* Instead of calling `column.getSize()` on every render for every header
* and especially every data cell (very expensive),
@@ -85,13 +126,14 @@ export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
return (
<div className="p-2">
<div className={cx('p-2', customClassName)} ref={tableRef}>
{/* Here in the <table> equivalent element (surrounds all table head and data cells), we will define our CSS variables for column sizes */}
<div
className="div-table"
style={{
...columnSizeVars, // Define column sizes on the <table> element
width: table.getTotalSize(),
height: `${virtualizer.getTotalSize()}px`,
}}
>
<div className="div-thead">
@@ -113,6 +155,9 @@ export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
onDoubleClick: (): void => header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
style: {
display: !header.column.getCanResize() ? 'none' : '',
},
className: `resizer ${
header.column.getIsResizing() ? 'isResizing' : ''
}`,
@@ -125,11 +170,16 @@ export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
</div>
{/* When resizing any column we will render this special memoized version of our table body */}
{table.getState().columnSizingInfo.isResizingColumn ? (
<MemoizedTableBody table={table} />
<MemoizedTableBody table={table} virtualizer={virtualizer} />
) : (
<TableBody table={table} />
<TableBody table={table} virtualizer={virtualizer} />
)}
</div>
</div>
);
}
TableV3.defaultProps = {
customClassName: '',
virtualiserRef: null,
};

View File

@@ -21,6 +21,7 @@ export const REACT_QUERY_KEY = {
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
GET_TRACE_V2: 'GET_TRACE_V2',
GET_POD_LIST: 'GET_POD_LIST',
GET_NODE_LIST: 'GET_NODE_LIST',
GET_DEPLOYMENT_LIST: 'GET_DEPLOYMENT_LIST',

View File

@@ -427,7 +427,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
? 0
: '0 1rem',
...(isTraceDetailsView() ? { marginRight: 0 } : {}),
...(isTraceDetailsView() ? { margin: 0 } : {}),
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}

View File

@@ -0,0 +1,112 @@
.flamegraph {
display: flex;
height: 20vh;
border-bottom: 1px solid var(--bg-slate-400);
.flamegraph-chart {
width: 80%;
padding: 15px;
.loading-skeleton {
justify-content: center;
align-items: center;
}
}
.flamegraph-stats {
width: 20%;
display: flex;
flex-direction: column;
border-left: 1px solid var(--bg-slate-400);
overflow-y: auto;
overflow-x: hidden;
padding: 16px 12px;
.exec-time-service {
display: flex;
height: 30px;
flex-shrink: 0;
justify-content: center;
align-items: center;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-400);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: -0.06px;
}
.stats {
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 0rem;
}
.value-row {
display: flex;
justify-content: space-between;
.service-name {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
.service-text {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
.square-box {
height: 8px;
width: 8px;
}
}
.progress-service {
display: flex;
align-items: center;
width: 85px;
gap: 8px;
justify-content: flex-start;
.service-progress-indicator {
width: fit-content;
margin-inline-end: 0px !important;
margin-bottom: 0px !important;
.ant-progress-inner {
width: 30px;
}
}
.percent-value {
color: var(--bg-vanilla-100);
text-align: right;
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.48px;
}
}
}
}
}
}

View File

@@ -0,0 +1,149 @@
import './PaginatedTraceFlamegraph.styles.scss';
import { Progress, Skeleton, Typography } from 'antd';
import { AxiosError } from 'axios';
import Spinner from 'components/Spinner';
import { themeColors } from 'constants/theme';
import useGetTraceFlamegraph from 'hooks/trace/useGetTraceFlamegraph';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { TraceDetailFlamegraphURLProps } from 'types/api/trace/getTraceFlamegraph';
import { TraceFlamegraphStates } from './constants';
import Error from './TraceFlamegraphStates/Error/Error';
import NoData from './TraceFlamegraphStates/NoData/NoData';
import Success from './TraceFlamegraphStates/Success/Success';
interface ITraceFlamegraphProps {
serviceExecTime: Record<string, number>;
startTime: number;
endTime: number;
}
function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element {
const { serviceExecTime, startTime, endTime } = props;
const { id: traceId } = useParams<TraceDetailFlamegraphURLProps>();
const urlQuery = useUrlQuery();
const [firstSpanAtFetchLevel, setFirstSpanAtFetchLevel] = useState<string>(
urlQuery.get('spanId') || '',
);
const { data, isFetching, error } = useGetTraceFlamegraph({
traceId,
selectedSpanId: firstSpanAtFetchLevel,
});
const isDarkMode = useIsDarkMode();
// get the current state of trace flamegraph based on the API lifecycle
const traceFlamegraphState = useMemo(() => {
if (isFetching) {
if (
data &&
data.payload &&
data.payload.spans &&
data.payload.spans.length > 0
) {
return TraceFlamegraphStates.FETCHING_WITH_OLD_DATA_PRESENT;
}
return TraceFlamegraphStates.LOADING;
}
if (error) {
return TraceFlamegraphStates.ERROR;
}
if (
data &&
data.payload &&
data.payload.spans &&
data.payload.spans.length === 0
) {
return TraceFlamegraphStates.NO_DATA;
}
return TraceFlamegraphStates.SUCCESS;
}, [error, isFetching, data]);
// capture the spans from the response, since we do not need to do any manipulation on the same we will keep this as a simple constant [ memoized ]
const spans = useMemo(() => data?.payload?.spans || [], [
data?.payload?.spans,
]);
// get the content based on the current state of the trace waterfall
const getContent = useMemo(() => {
switch (traceFlamegraphState) {
case TraceFlamegraphStates.LOADING:
return (
<div className="loading-skeleton">
<Skeleton active paragraph={{ rows: 3 }} />
</div>
);
case TraceFlamegraphStates.ERROR:
return <Error error={error as AxiosError} />;
case TraceFlamegraphStates.NO_DATA:
return <NoData id={traceId} />;
case TraceFlamegraphStates.SUCCESS:
case TraceFlamegraphStates.FETCHING_WITH_OLD_DATA_PRESENT:
return (
<Success
spans={spans}
firstSpanAtFetchLevel={firstSpanAtFetchLevel}
setFirstSpanAtFetchLevel={setFirstSpanAtFetchLevel}
traceMetadata={{
startTime: data?.payload?.startTimestampMillis || 0,
endTime: data?.payload?.endTimestampMillis || 0,
}}
/>
);
default:
return <Spinner tip="Fetching the trace!" />;
}
}, [
data?.payload?.endTimestampMillis,
data?.payload?.startTimestampMillis,
error,
firstSpanAtFetchLevel,
spans,
traceFlamegraphState,
traceId,
]);
return (
<div className="flamegraph">
<div className="flamegraph-chart">{getContent}</div>
<div className="flamegraph-stats">
<div className="exec-time-service">% exec time</div>
<div className="stats">
{Object.keys(serviceExecTime).map((service) => {
const spread = endTime - startTime;
const value = (serviceExecTime[service] * 100) / spread;
const color = generateColor(
service,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
return (
<div key={service} className="value-row">
<section className="service-name">
<div className="square-box" style={{ backgroundColor: color }} />
<Typography.Text className="service-text">{service}</Typography.Text>
</section>
<section className="progress-service">
<Progress
percent={parseFloat(value.toFixed(2))}
className="service-progress-indicator"
showInfo={false}
/>
<Typography.Text className="percent-value">
{parseFloat(value.toFixed(2))}%
</Typography.Text>
</section>
</div>
);
})}
</div>
</div>
</div>
);
}
export default TraceFlamegraph;

View File

@@ -0,0 +1,23 @@
.error-flamegraph {
display: flex;
gap: 4px;
flex-direction: column;
justify-content: center;
align-items: center;
height: 15vh;
.error-flamegraph-img {
height: 32px;
width: 32px;
}
.no-data-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}

View File

@@ -0,0 +1,29 @@
import './Error.styles.scss';
import { Tooltip, Typography } from 'antd';
import { AxiosError } from 'axios';
interface IErrorProps {
error: AxiosError;
}
function Error(props: IErrorProps): JSX.Element {
const { error } = props;
return (
<div className="error-flamegraph">
<img
src="/Icons/no-data.svg"
alt="error-flamegraph"
className="error-flamegraph-img"
/>
<Tooltip title={error?.message}>
<Typography.Text className="no-data-text">
{error?.message || 'Something went wrong!'}
</Typography.Text>
</Tooltip>
</div>
);
}
export default Error;

View File

@@ -0,0 +1,12 @@
import { Typography } from 'antd';
interface INoDataProps {
id: string;
}
function NoData(props: INoDataProps): JSX.Element {
const { id } = props;
return <Typography.Text>No Trace found with the id: {id} </Typography.Text>;
}
export default NoData;

View File

@@ -0,0 +1,28 @@
.trace-flamegraph {
height: 80%;
overflow-x: hidden;
overflow-y: auto;
.trace-flamegraph-virtuoso {
overflow-x: hidden;
.flamegraph-row {
display: flex;
align-items: center;
height: 18px;
padding-bottom: 6px;
.span-item {
position: absolute;
height: 12px;
background-color: yellow;
border-radius: 6px;
cursor: pointer;
}
}
&::-webkit-scrollbar {
width: 0rem;
}
}
}

View File

@@ -0,0 +1,141 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './Success.styles.scss';
import { Tooltip } from 'antd';
import TimelineV2 from 'components/TimelineV2/TimelineV2';
import { themeColors } from 'constants/theme';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useRef,
} from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
interface ITraceMetadata {
startTime: number;
endTime: number;
}
interface ISuccessProps {
spans: FlamegraphSpan[][];
firstSpanAtFetchLevel: string;
setFirstSpanAtFetchLevel: Dispatch<SetStateAction<string>>;
traceMetadata: ITraceMetadata;
}
function Success(props: ISuccessProps): JSX.Element {
const {
spans,
setFirstSpanAtFetchLevel,
traceMetadata,
firstSpanAtFetchLevel,
} = props;
const { search } = useLocation();
const history = useHistory();
const isDarkMode = useIsDarkMode();
const virtuosoRef = useRef<VirtuosoHandle>(null);
const renderSpanLevel = useCallback(
(_: number, spans: FlamegraphSpan[]): JSX.Element => (
<div className="flamegraph-row">
{spans.map((span) => {
const spread = traceMetadata.endTime - traceMetadata.startTime;
const leftOffset =
((span.timestamp - traceMetadata.startTime) * 100) / spread;
let width = ((span.durationNano / 1e6) * 100) / spread;
if (width > 100) {
width = 100;
}
const toolTipText = `${span.name}`;
const searchParams = new URLSearchParams(search);
let color = generateColor(
span.serviceName,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
if (span.hasError) {
color = `var(--bg-cherry-500)`;
}
return (
<Tooltip title={toolTipText} key={span.spanId}>
<div
className="span-item"
style={{
left: `${leftOffset}%`,
width: `${width}%`,
backgroundColor: color,
}}
onClick={(event): void => {
event.stopPropagation();
event.preventDefault();
searchParams.set('spanId', span.spanId);
history.replace({ search: searchParams.toString() });
}}
/>
</Tooltip>
);
})}
</div>
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[traceMetadata.endTime, traceMetadata.startTime],
);
const handleRangeChanged = useCallback(
(range: ListRange) => {
// if there are less than 50 levels on any load that means a single API call is sufficient
if (spans.length < 50) {
return;
}
const { startIndex, endIndex } = range;
if (startIndex === 0 && spans[0][0].level !== 0) {
setFirstSpanAtFetchLevel(spans[0][0].spanId);
}
if (endIndex === spans.length - 1) {
setFirstSpanAtFetchLevel(spans[spans.length - 1][0].spanId);
}
},
[setFirstSpanAtFetchLevel, spans],
);
useEffect(() => {
const index = spans.findIndex(
(span) => span[0].spanId === firstSpanAtFetchLevel,
);
virtuosoRef.current?.scrollToIndex({
index,
behavior: 'auto',
});
}, [firstSpanAtFetchLevel, spans]);
return (
<>
<div className="trace-flamegraph">
<Virtuoso
ref={virtuosoRef}
className="trace-flamegraph-virtuoso"
data={spans}
itemContent={renderSpanLevel}
rangeChanged={handleRangeChanged}
/>
</div>
<TimelineV2
startTimestamp={traceMetadata.startTime}
endTimestamp={traceMetadata.endTime}
timelineHeight={22}
/>
</>
);
}
export default Success;

View File

@@ -0,0 +1,7 @@
export enum TraceFlamegraphStates {
LOADING = 'LOADING',
SUCCESS = 'SUCCSS',
NO_DATA = 'NO_DATA',
ERROR = 'ERROR',
FETCHING_WITH_OLD_DATA_PRESENT = 'FETCHING_WTIH_OLD_DATA_PRESENT',
}

View File

@@ -0,0 +1,172 @@
.trace-metadata {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px 16px 0px 16px;
.metadata-info {
display: flex;
flex-direction: column;
gap: 10px;
.first-row {
display: flex;
align-items: center;
.previous-btn {
display: flex;
height: 30px;
padding: 6px 8px;
align-items: center;
gap: 4px;
border: 1px solid var(--bg-slate-300);
background: var(--bg-slate-500);
border-radius: 4px;
box-shadow: none;
}
.trace-name {
display: flex;
padding: 6px 8px;
margin-left: 6px;
align-items: center;
gap: 4px;
border: 1px solid var(--bg-slate-300);
border-radius: 4px 0px 0px 4px;
background: var(--bg-slate-500);
.drafting {
color: white;
}
.trace-id {
color: #fff;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
.trace-id-value {
display: flex;
padding: 6px 8px;
justify-content: center;
align-items: center;
gap: 10px;
background: var(--bg-slate-400);
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
border: 1px solid var(--bg-slate-300);
border-left: unset;
border-radius: 0px 4px 4px 0px;
}
}
.second-row {
display: flex;
gap: 24px;
.service-entry-info {
display: flex;
gap: 6px;
color: var(--bg-vanilla-400);
align-items: center;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.trace-duration {
display: flex;
gap: 6px;
color: var(--bg-vanilla-400);
align-items: center;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.start-time-info {
display: flex;
gap: 6px;
color: var(--bg-vanilla-400);
align-items: center;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}
.datapoints-info {
display: flex;
gap: 16px;
.separator {
width: 1px;
background: #1d212d;
}
.data-point {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 4px;
.text {
color: var(--bg-vanilla-400);
text-align: center;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.value {
color: var(--bg-vanilla-100);
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
font-family: Inter;
font-size: 20px;
font-style: normal;
font-weight: 500;
line-height: 28px; /* 140% */
letter-spacing: -0.1px;
text-transform: uppercase;
text-align: right;
}
}
}
}

View File

@@ -0,0 +1,87 @@
import './TraceMetadata.styles.scss';
import { Button, Typography } from 'antd';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { ArrowLeft, CalendarClock, DraftingCompass, Timer } from 'lucide-react';
import { formatEpochTimestamp } from 'utils/timeUtils';
export interface ITraceMetadataProps {
traceID: string;
rootServiceName: string;
rootSpanName: string;
startTime: number;
duration: number;
totalSpans: number;
totalErrorSpans: number;
notFound: boolean;
}
function TraceMetadata(props: ITraceMetadataProps): JSX.Element {
const {
traceID,
rootServiceName,
rootSpanName,
startTime,
duration,
totalErrorSpans,
totalSpans,
notFound,
} = props;
return (
<div className="trace-metadata">
<section className="metadata-info">
<div className="first-row">
<Button className="previous-btn">
<ArrowLeft
size={14}
onClick={(): void => history.push(ROUTES.TRACES_EXPLORER)}
/>
</Button>
<div className="trace-name">
<DraftingCompass size={14} className="drafting" />
<Typography.Text className="trace-id">Trace ID</Typography.Text>
</div>
<Typography.Text className="trace-id-value">{traceID}</Typography.Text>
</div>
{!notFound && (
<div className="second-row">
<div className="service-entry-info">
<Typography.Text className="text">{rootServiceName}</Typography.Text>
&#8212;
<Typography.Text className="text">{rootSpanName}</Typography.Text>
</div>
<div className="trace-duration">
<Timer size={14} />
<Typography.Text className="text">
{getYAxisFormattedValue(`${duration}`, 'ms')}
</Typography.Text>
</div>
<div className="start-time-info">
<CalendarClock size={14} />
<Typography.Text className="text">
{formatEpochTimestamp(startTime * 1000)}
</Typography.Text>
</div>
</div>
)}
</section>
{!notFound && (
<section className="datapoints-info">
<div className="data-point">
<Typography.Text className="text">Total Spans</Typography.Text>
<Typography.Text className="value">{totalSpans}</Typography.Text>
</div>
<div className="separator" />
<div className="data-point">
<Typography.Text className="text">Error Spans</Typography.Text>
<Typography.Text className="value">{totalErrorSpans}</Typography.Text>
</div>
</section>
)}
</div>
);
}
export default TraceMetadata;

View File

@@ -0,0 +1,9 @@
.trace-waterfall {
height: 50vh;
.loading-skeleton {
justify-content: center;
align-items: center;
padding: 20px;
}
}

View File

@@ -0,0 +1,124 @@
import './TraceWaterfall.styles.scss';
import { Skeleton } from 'antd';
import { AxiosError } from 'axios';
import Spinner from 'components/Spinner';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetTraceV2SuccessResponse } from 'types/api/trace/getTraceV2';
import { TraceWaterfallStates } from './constants';
import Error from './TraceWaterfallStates/Error/Error';
import NoData from './TraceWaterfallStates/NoData/NoData';
import Success from './TraceWaterfallStates/Success/Success';
export interface IInterestedSpan {
spanId: string;
isUncollapsed: boolean;
}
interface ITraceWaterfallProps {
traceId: string;
uncollapsedNodes: string[];
traceData:
| SuccessResponse<GetTraceV2SuccessResponse, unknown>
| ErrorResponse
| undefined;
isFetchingTraceData: boolean;
errorFetchingTraceData: unknown;
interestedSpanId: IInterestedSpan;
setInterestedSpanId: Dispatch<SetStateAction<IInterestedSpan>>;
}
function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
const {
traceData,
isFetchingTraceData,
errorFetchingTraceData,
interestedSpanId,
traceId,
uncollapsedNodes,
setInterestedSpanId,
} = props;
// get the current state of trace waterfall based on the API lifecycle
const traceWaterfallState = useMemo(() => {
if (isFetchingTraceData) {
if (
traceData &&
traceData.payload &&
traceData.payload.spans &&
traceData.payload.spans.length > 0
) {
return TraceWaterfallStates.FETCHING_WITH_OLD_DATA_PRESENT;
}
return TraceWaterfallStates.LOADING;
}
if (errorFetchingTraceData) {
return TraceWaterfallStates.ERROR;
}
if (
traceData &&
traceData.payload &&
traceData.payload.spans &&
traceData.payload.spans.length === 0
) {
return TraceWaterfallStates.NO_DATA;
}
return TraceWaterfallStates.SUCCESS;
}, [errorFetchingTraceData, isFetchingTraceData, traceData]);
// capture the spans from the response, since we do not need to do any manipulation on the same we will keep this as a simple constant [ memoized ]
const spans = useMemo(() => traceData?.payload?.spans || [], [
traceData?.payload?.spans,
]);
// get the content based on the current state of the trace waterfall
const getContent = useMemo(() => {
switch (traceWaterfallState) {
case TraceWaterfallStates.LOADING:
return (
<div className="loading-skeleton">
<Skeleton active paragraph={{ rows: 6 }} />
</div>
);
case TraceWaterfallStates.ERROR:
return <Error error={errorFetchingTraceData as AxiosError} />;
case TraceWaterfallStates.NO_DATA:
return <NoData id={traceId} />;
case TraceWaterfallStates.SUCCESS:
case TraceWaterfallStates.FETCHING_WITH_OLD_DATA_PRESENT:
return (
<Success
spans={spans}
traceMetadata={{
traceId,
startTime: traceData?.payload?.startTimestampMillis || 0,
endTime: traceData?.payload?.endTimestampMillis || 0,
hasMissingSpans: traceData?.payload?.hasMissingSpans || false,
}}
interestedSpanId={interestedSpanId || ''}
uncollapsedNodes={uncollapsedNodes}
setInterestedSpanId={setInterestedSpanId}
/>
);
default:
return <Spinner tip="Fetching the trace!" />;
}
}, [
errorFetchingTraceData,
interestedSpanId,
setInterestedSpanId,
spans,
traceData?.payload?.endTimestampMillis,
traceData?.payload?.hasMissingSpans,
traceData?.payload?.startTimestampMillis,
traceId,
traceWaterfallState,
uncollapsedNodes,
]);
return <div className="trace-waterfall">{getContent}</div>;
}
export default TraceWaterfall;

View File

@@ -0,0 +1,30 @@
.error-waterfall {
display: flex;
padding: 12px;
margin: 20px;
gap: 12px;
align-items: flex-start;
border-radius: 4px;
background: var(--bg-cherry-500);
.text {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
flex-shrink: 0;
}
.value {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}

View File

@@ -0,0 +1,25 @@
import './Error.styles.scss';
import { Tooltip, Typography } from 'antd';
import { AxiosError } from 'axios';
interface IErrorProps {
error: AxiosError;
}
function Error(props: IErrorProps): JSX.Element {
const { error } = props;
return (
<div className="error-waterfall">
<Typography.Text className="text">Something went wrong!</Typography.Text>
<Tooltip title={error?.message}>
<Typography.Text className="value" ellipsis>
{error?.message}
</Typography.Text>
</Tooltip>
</div>
);
}
export default Error;

View File

@@ -0,0 +1,12 @@
import { Typography } from 'antd';
interface INoDataProps {
id: string;
}
function NoData(props: INoDataProps): JSX.Element {
const { id } = props;
return <Typography.Text>No Trace found with the id: {id} </Typography.Text>;
}
export default NoData;

View File

@@ -0,0 +1,89 @@
.attributes-table {
display: flex;
flex-direction: column;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
background: rgba(171, 189, 255, 0.04);
.no-attributes {
display: flex;
align-items: center;
justify-content: center;
height: 40vh;
background-color: var(--bg-ink-400);
}
.attributes-header {
display: flex;
gap: 16px;
padding: 1px 12px;
height: 38px;
justify-content: space-between;
align-items: center;
.attributes-tag {
display: flex;
flex-shrink: 0;
width: fit-content;
border-radius: 2px;
background: rgba(113, 144, 249, 0.08);
color: var(--bg-robin-400);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
.search-input {
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: unset;
border: none;
box-shadow: none;
}
}
.resize-trace-attribute-table {
.ant-table-thead {
display: none;
}
.attribute-name {
background: unset;
border: 1px solid var(--bg-slate-500);
.field-key {
color: var(--bg-robin-400);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
.attribute-value {
display: flex;
border: 1px solid var(--bg-slate-500);
background: rgba(22, 25, 34, 0.4);
.field-value {
display: flex;
padding: 2px 8px;
width: fit-content;
align-items: center;
gap: 8px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
}
}
}
}

View File

@@ -0,0 +1,94 @@
import './AttributesTable.styles.scss';
import { Button, Input, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ResizeTable } from 'components/ResizeTable';
import { flattenObject } from 'container/LogDetailedView/utils';
import { Search } from 'lucide-react';
import { useMemo, useState } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import NoData from '../NoData/NoData';
interface IAttributesTable {
span: Span;
}
function AttributesTable(props: IAttributesTable): JSX.Element {
const { span } = props;
const [searchVisible, setSearchVisible] = useState<boolean>(false);
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
const flattenSpanData: Record<string, string> = useMemo(
() => (span.tagMap ? flattenObject(span.tagMap) : {}),
[span],
);
const datasource = Object.keys(flattenSpanData)
.filter((attribute) => attribute.includes(fieldSearchInput))
.map((key) => ({ field: key, value: flattenSpanData[key] }));
const columns: ColumnsType<Record<string, string>> = [
{
title: 'Field',
dataIndex: 'field',
key: 'field',
width: 50,
align: 'left',
ellipsis: true,
className: 'attribute-name',
render: (field: string): JSX.Element => (
<Typography.Text className="field-key">{field}</Typography.Text>
),
},
{
title: 'Value',
key: 'value',
width: 50,
ellipsis: false,
className: 'attribute-value',
render: (fieldValue): JSX.Element => (
<Typography.Text className="field-value">
{fieldValue.value}
</Typography.Text>
),
},
];
return (
<div className="attributes-table">
{datasource.length > 0 ? (
<>
<section className="attributes-header">
<Typography.Text className="attributes-tag">Attributes</Typography.Text>
{searchVisible && (
<Input
autoFocus
placeholder="Search for attribute..."
className="search-input"
value={fieldSearchInput}
onChange={(e): void => setFieldSearchInput(e.target.value)}
/>
)}
<Button
className="action-btn"
icon={<Search size={12} />}
onClick={(e): void => {
e.stopPropagation();
setSearchVisible((prev) => !prev);
}}
/>
</section>
<section className="resize-trace-attribute-table">
<ResizeTable columns={columns} dataSource={datasource} />
</section>
</>
) : (
<div className="no-attributes">
<NoData name="attributes" />
</div>
)}
</div>
);
}
export default AttributesTable;

View File

@@ -0,0 +1,134 @@
.trace-drawer-descriptive-content {
display: flex;
flex-direction: column;
gap: 24px;
.span-name-duration {
display: flex;
align-items: center;
justify-content: space-between;
.span-name {
display: flex;
gap: 8px;
.info-pill {
display: flex;
.text {
display: flex;
padding: 6px 8px;
align-items: center;
gap: 4px;
border-right: 1px solid var(--bg-slate-300);
background: var(--bg-slate-500);
color: #fff;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.value {
display: flex;
padding: 6px 8px;
justify-content: center;
align-items: center;
gap: 10px;
background: var(--bg-slate-400);
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.success {
border: 1px solid var(--bg-robin-500);
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
}
.error {
border: 1px solid var(--bg-cherry-500);
background: var(--bg-cherry-500);
color: var(--bg-vanilla-100);
}
}
}
.span-duration {
display: flex;
gap: 24px;
.item {
display: flex;
gap: 6px;
align-items: center;
.value {
color: var(--bg-vanilla-400);
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
}
}
.span-metadata {
display: flex;
gap: 24px;
.item {
display: flex;
flex-direction: column;
gap: 8px;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.value {
.dot {
width: 4px;
height: 4px;
border-radius: 4px;
filter: drop-shadow(0px 0px 6px rgba(37, 225, 146, 0.8));
}
display: flex;
padding: 2px 8px;
align-items: center;
gap: 8px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
}
}

View File

@@ -0,0 +1,95 @@
import './DrawerDescriptiveContent.styles.scss';
import { Typography } from 'antd';
import cx from 'classnames';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { themeColors } from 'constants/theme';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { CalendarClock, Timer } from 'lucide-react';
import { useMemo } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import { formatEpochTimestamp } from 'utils/timeUtils';
interface IDrawerDescriptiveContentProps {
span: Span;
}
function DrawerDescriptiveContent(
props: IDrawerDescriptiveContentProps,
): JSX.Element {
const { span } = props;
const isDarkMode = useIsDarkMode();
const color = generateColor(
span.serviceName,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
const statusCodeClassName = useMemo(() => {
if (span.statusCodeString === 'unset') {
return '';
}
const statusCode = parseFloat(span.statusCodeString);
if (statusCode >= 200 && statusCode < 300) {
return 'success';
}
if (statusCode >= 400) {
return 'error';
}
return '';
}, [span.statusCodeString]);
return (
<div className="trace-drawer-descriptive-content">
<section className="span-name-duration">
<section className="span-name">
<section className="info-pill">
<Typography.Text className="text">Span name</Typography.Text>
<Typography.Text className="value">{span.name}</Typography.Text>
</section>
<section className="info-pill">
<Typography.Text className="text">Status code</Typography.Text>
<Typography.Text className={cx('value', statusCodeClassName)}>
{span.statusCodeString}
</Typography.Text>
</section>
</section>
<section className="span-duration">
<Typography.Text className="item">
<Timer size={14} />
<Typography.Text className="value">
{getYAxisFormattedValue(`${span.durationNano}`, 'ns')}
</Typography.Text>
</Typography.Text>
<Typography.Text className="item">
<CalendarClock size={14} />
<Typography.Text className="value">
{formatEpochTimestamp(span.timestamp)}
</Typography.Text>
</Typography.Text>
</section>
</section>
<section className="span-metadata">
<div className="item">
<Typography.Text className="text">Span ID</Typography.Text>
<Typography.Text className="value">{span.spanId}</Typography.Text>
</div>
<div className="item">
<Typography.Text className="text">Service</Typography.Text>
<Typography.Text className="value">
<div className="dot" style={{ backgroundColor: color }} />
{span.serviceName}
</Typography.Text>
</div>
<div className="item">
<Typography.Text className="text">Span kind</Typography.Text>
<Typography.Text className="value">{span.spanKind}</Typography.Text>
</div>
</section>
</div>
);
}
export default DrawerDescriptiveContent;

View File

@@ -0,0 +1,125 @@
.events-table {
.no-events {
display: flex;
justify-content: center;
align-items: center;
height: 40vh;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.events-container {
display: flex;
flex-direction: column;
gap: 12px;
.event {
.ant-collapse {
border: none;
}
.ant-collapse-content {
border-top: none;
}
.ant-collapse-item {
border-bottom: 0px;
}
.ant-collapse-content-box {
border: 1px solid var(--bg-slate-500);
border-top: none;
}
.ant-collapse-header {
display: flex;
padding: 8px 6px;
align-items: center;
justify-content: space-between;
gap: 16px;
border-radius: 2px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
.ant-collapse-expand-icon {
padding-inline-start: 0px;
padding-inline-end: 0px;
}
.collapse-title {
display: flex;
align-items: center;
gap: 6px;
.diamond {
fill: var(--bg-cherry-500);
}
}
}
.event-details {
display: flex;
flex-direction: column;
gap: 16px;
.attribute-container {
display: flex;
flex-direction: column;
gap: 8px;
.attribute-key {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.timestamp-container {
display: flex;
gap: 4px;
align-items: center;
.timestamp-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.attribute-value {
display: flex;
padding: 2px 8px;
width: fit-content;
align-items: center;
gap: 8px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
}
}
.attribute-value {
display: flex;
padding: 2px 8px;
width: fit-content;
align-items: center;
gap: 8px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
}
}
}
}
}
}

View File

@@ -0,0 +1,95 @@
import './EventsTable.styles.scss';
import { Collapse, Typography } from 'antd';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Diamond } from 'lucide-react';
import { useMemo } from 'react';
import { Event, Span } from 'types/api/trace/getTraceV2';
import NoData from '../NoData/NoData';
interface IEventsTableProps {
span: Span;
startTime: number;
}
function EventsTable(props: IEventsTableProps): JSX.Element {
const { span, startTime } = props;
const events: Event[] = useMemo(() => {
const tempEvents = [];
for (let i = 0; i < span.event?.length; i++) {
const parsedEvent = JSON.parse(span.event[i]);
tempEvents.push(parsedEvent);
}
return tempEvents;
}, [span.event]);
return (
<div className="events-table">
{events.length === 0 && (
<div className="no-events">
<NoData name="events" />
</div>
)}
<div className="events-container">
{events.map((event) => (
<div
className="event"
key={`${event.name} ${JSON.stringify(event.attributeMap)}`}
>
<Collapse
size="small"
defaultActiveKey="1"
expandIconPosition="right"
items={[
{
key: '1',
label: (
<div className="collapse-title">
<Diamond size={14} className="diamond" />
<Typography.Text className="collapse-title-name">
{span.name}
</Typography.Text>
</div>
),
children: (
<div className="event-details">
<div className="attribute-container" key="timeUnixNano">
<Typography.Text className="attribute-key">
Start Time
</Typography.Text>
<div className="timestamp-container">
<Typography.Text className="attribute-value">
{getYAxisFormattedValue(
`${event.timeUnixNano / 1e6 - startTime}`,
'ms',
)}
</Typography.Text>
<Typography.Text className="timestamp-text">
after the start
</Typography.Text>
</div>
</div>
{event.attributeMap &&
Object.keys(event.attributeMap).map((attributeKey) => (
<div className="attribute-container" key={attributeKey}>
<Typography.Text className="attribute-key">
{attributeKey}
</Typography.Text>
<Typography.Text className="attribute-value">
{event.attributeMap[attributeKey]}
</Typography.Text>
</div>
))}
</div>
),
},
]}
/>
</div>
))}
</div>
</div>
);
}
export default EventsTable;

View File

@@ -0,0 +1,20 @@
.no-data {
display: flex;
gap: 4px;
flex-direction: column;
.no-data-img {
height: 32px;
width: 32px;
}
.no-data-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}

View File

@@ -0,0 +1,22 @@
import './NoData.styles.scss';
import { Typography } from 'antd';
interface INoDataProps {
name: string;
}
function NoData(props: INoDataProps): JSX.Element {
const { name } = props;
return (
<div className="no-data">
<img src="/Icons/no-data.svg" alt="no-data" className="no-data-img" />
<Typography.Text className="no-data-text">
No {name} found for selected span
</Typography.Text>
</div>
);
}
export default NoData;

View File

@@ -0,0 +1,447 @@
.success-content {
overflow-y: hidden;
overflow-x: hidden;
max-width: 100%;
.missing-spans {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
margin: 16px;
padding: 12px;
border-radius: 4px;
background: rgba(69, 104, 220, 0.1);
.left-info {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.text {
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.right-info {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row-reverse;
gap: 8px;
color: var(--bg-robin-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.right-info:hover {
background-color: unset;
color: var(--bg-robin-200);
}
}
.waterfall-table {
height: 50vh;
overflow: auto;
overflow-x: hidden;
padding: 0px 20px;
&::-webkit-scrollbar {
width: 0.1rem;
}
// default table overrides css for table v3
.div-table {
width: 100% !important;
border: none !important;
}
.div-thead {
position: sticky;
top: 0;
z-index: 2;
background-color: var(--bg-ink-500) !important;
.div-tr {
height: 16px;
}
}
.div-tr {
display: flex;
width: 100%;
align-items: center;
height: 54px;
}
.div-th,
.div-td {
box-shadow: none;
padding: 0px !important;
}
.div-th {
padding: 2px 4px;
position: relative;
font-weight: bold;
text-align: center;
height: 0px;
}
.div-td {
display: flex;
height: 54px;
align-items: center;
overflow: hidden;
.span-overview {
display: flex;
align-items: center;
flex-shrink: 0;
height: 100%;
width: 100%;
cursor: pointer;
.span-overview-content {
display: flex;
flex-shrink: 0;
flex-direction: column;
align-items: flex-start;
gap: 5px;
width: 100%;
.first-row {
display: flex;
align-items: center;
justify-content: space-between;
height: 20px;
width: 100%;
.span-det {
display: flex;
gap: 6px;
.collapse-uncollapse-button {
display: flex;
align-items: center;
justify-content: center;
padding: 4px 4px;
gap: 4px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
box-shadow: none;
height: 20px;
.children-count {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: 0.28px;
}
}
.span-name {
color: #fff;
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: 0.28px;
}
}
.status-code-container {
display: flex;
padding-right: 10px;
.status-code {
display: flex;
height: 20px;
padding: 3px;
align-items: center;
border-radius: 3px;
}
.success {
border: 1px solid var(--bg-robin-500);
background: var(--bg-robin-500);
}
.error {
border: 1px solid var(--bg-cherry-500);
background: var(--bg-cherry-500);
}
}
}
.second-row {
display: flex;
align-items: center;
gap: 8px;
height: 18px;
width: 100%;
.service-name {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
}
}
.span-duration {
display: flex;
flex-direction: column;
height: 54px;
position: relative;
width: 100%;
cursor: pointer;
.span-line {
position: absolute;
border-radius: 5px;
height: 12px;
top: 35%;
border-radius: 2px;
}
.span-line-text {
position: absolute;
top: 65%;
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
.interested-span {
border-radius: 4px;
background: rgba(171, 189, 255, 0.06);
}
}
.div-tr .div-th:nth-child(2) {
width: calc(100% - var(--header-span-name-size) * 1px) !important;
}
.div-tr .div-td:nth-child(2) {
width: calc(100% - var(--header-span-name-size) * 1px) !important;
}
.resizer {
width: 10px !important;
position: absolute;
top: 0;
height: 50vh;
right: 0;
width: 2px;
background: var(--bg-slate-400);
cursor: col-resize;
user-select: none;
touch-action: none;
}
.resizer.isResizing {
background: var(--bg-slate-300);
opacity: 1;
}
@media (hover: hover) {
.resizer {
opacity: 0;
}
*:hover > .resizer {
opacity: 1;
}
}
}
}
.span-dets {
.related-logs {
display: flex;
width: 160px;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 8px;
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d);
background: var(--Slate-500, #161922);
box-shadow: none;
}
}
.lightMode {
.success-content {
width: 100%;
height: 100%;
.waterfall-table {
// default table overrides css for table v3
.div-table {
border: none !important;
}
.div-tr {
display: flex;
width: fit-content;
height: 42px;
}
.div-th,
.div-td {
box-shadow: none;
padding: 0px !important;
}
.div-th {
padding: 2px 4px;
position: relative;
font-weight: bold;
text-align: center;
height: 30px;
}
.div-td {
display: flex;
height: 42px;
overflow: hidden;
.span-overview {
display: flex;
align-items: center;
flex-shrink: 0;
height: 100%;
width: 100%;
.span-overview-content {
display: flex;
flex-shrink: 0;
flex-direction: column;
align-items: flex-start;
gap: 5px;
.first-row {
display: flex;
align-items: center;
gap: 6px;
height: 20px;
.collapse-uncollapse-button {
display: flex;
align-items: center;
justify-content: center;
padding: 4px 8px;
gap: 4px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
box-shadow: none;
.children-count {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: 0.28px;
}
}
.span-name {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: 0.28px;
}
}
.second-row {
display: flex;
align-items: center;
gap: 8px;
height: 18px;
.service-name {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
}
}
.interested-span {
border-radius: 4px;
background: rgba(171, 189, 255, 0.06);
}
}
.resizer {
position: absolute;
top: 0;
height: 100%;
right: 0;
width: 5px;
background: rgba(0, 0, 0, 0.5);
cursor: col-resize;
user-select: none;
touch-action: none;
}
.resizer.isResizing {
background: blue;
opacity: 1;
}
@media (hover: hover) {
.resizer {
opacity: 0;
}
*:hover > .resizer {
opacity: 1;
}
}
}
}
}

View File

@@ -0,0 +1,469 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './Success.styles.scss';
import { ColumnDef, createColumnHelper } from '@tanstack/react-table';
import { Virtualizer } from '@tanstack/react-virtual';
import { Button, TabsProps, Typography } from 'antd';
import cx from 'classnames';
import DetailsDrawer from 'components/DetailsDrawer/DetailsDrawer';
import { TableV3 } from 'components/TableV3/TableV3';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { themeColors } from 'constants/theme';
import { getTraceToLogsQuery } from 'container/TraceDetail/SelectedSpanDetails/config';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall';
import { useIsDarkMode } from 'hooks/useDarkMode';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
// import { TraceWaterfallStates } from 'container/TraceWaterfall/constants';
import {
AlertCircle,
Anvil,
ArrowUpRight,
Bookmark,
ChevronDown,
ChevronRight,
Leaf,
} from 'lucide-react';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import { toFixed } from 'utils/toFixed';
import AttributesTable from './DrawerComponents/AttributesTable/AttributesTable';
import DrawerDescriptiveContent from './DrawerComponents/DrawerDescriptiveContent/DrawerDescriptiveContent';
import EventsTable from './DrawerComponents/EventsTable/EventsTable';
// css config
const CONNECTOR_WIDTH = 28;
const VERTICAL_CONNECTOR_WIDTH = 1;
interface ITraceMetadata {
traceId: string;
startTime: number;
endTime: number;
hasMissingSpans: boolean;
}
interface ISuccessProps {
spans: Span[];
traceMetadata: ITraceMetadata;
interestedSpanId: IInterestedSpan;
uncollapsedNodes: string[];
setInterestedSpanId: Dispatch<SetStateAction<IInterestedSpan>>;
}
function SpanOverview({
span,
isSpanCollapsed,
interestedSpanId,
handleCollapseUncollapse,
setTraceDetailsOpen,
setSpanDetails,
}: {
span: Span;
isSpanCollapsed: boolean;
interestedSpanId: string;
handleCollapseUncollapse: (id: string, collapse: boolean) => void;
setTraceDetailsOpen: Dispatch<SetStateAction<boolean>>;
setSpanDetails: Dispatch<SetStateAction<Span | undefined>>;
}): JSX.Element {
const isRootSpan = span.parentSpanId === '';
const spanRef = useRef<HTMLDivElement>(null);
const isDarkMode = useIsDarkMode();
let color = generateColor(
span.serviceName,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
if (span.hasError) {
color = `var(--bg-cherry-500)`;
}
const statusCodeClassName = useMemo(() => {
if (span.statusCodeString === 'unset') {
return '';
}
const statusCode = parseFloat(span.statusCodeString);
if (statusCode >= 200 && statusCode < 300) {
return 'success';
}
if (statusCode >= 400) {
return 'error';
}
return '';
}, [span.statusCodeString]);
return (
<div
ref={spanRef}
className={cx(
'span-overview',
interestedSpanId === span.spanId ? 'interested-span' : '',
)}
style={{
marginLeft: `${
isRootSpan
? span.level * CONNECTOR_WIDTH
: (span.level - 1) * (CONNECTOR_WIDTH + VERTICAL_CONNECTOR_WIDTH)
}px`,
borderLeft: isRootSpan ? 'none' : `1px solid lightgray`,
}}
onClick={(): void => {
setSpanDetails(span);
setTraceDetailsOpen(true);
}}
>
{!isRootSpan && (
<div
style={{
width: `${CONNECTOR_WIDTH}px`,
height: '1px',
border: '1px solid lightgray',
display: 'flex',
flexShrink: 0,
position: 'relative',
top: '-10px',
}}
/>
)}
<div className="span-overview-content">
<section className="first-row">
<div className="span-det">
{span.hasChildren ? (
<Button
onClick={(event): void => {
event.stopPropagation();
event.preventDefault();
handleCollapseUncollapse(span.spanId, !isSpanCollapsed);
}}
className="collapse-uncollapse-button"
>
{isSpanCollapsed ? (
<ChevronRight size={14} />
) : (
<ChevronDown size={14} />
)}
<Typography.Text className="children-count">
{span.subTreeNodeCount}
</Typography.Text>
</Button>
) : (
<Button className="collapse-uncollapse-button">
<Leaf size={14} />
</Button>
)}
<Typography.Text className="span-name">{span.name}</Typography.Text>
</div>
<div
className="status-code-container"
style={{
marginRight: `${
span.level * (CONNECTOR_WIDTH + VERTICAL_CONNECTOR_WIDTH)
}px`,
}}
>
{span.statusCodeString !== 'Unset' && (
<div className={cx('status-code', statusCodeClassName)}>
{span.statusCodeString}
</div>
)}
</div>
</section>
<section className="second-row">
<div style={{ width: '2px', background: color, height: '100%' }} />
<Typography.Text className="service-name">
{span.serviceName}
</Typography.Text>
</section>
</div>
</div>
);
}
function SpanDuration({
span,
interestedSpanId,
traceMetadata,
setTraceDetailsOpen,
setSpanDetails,
}: {
span: Span;
interestedSpanId: string;
traceMetadata: ITraceMetadata;
setTraceDetailsOpen: Dispatch<SetStateAction<boolean>>;
setSpanDetails: Dispatch<SetStateAction<Span | undefined>>;
}): JSX.Element {
const { time, timeUnitName } = convertTimeToRelevantUnit(
span.durationNano / 1e6,
);
const spread = traceMetadata.endTime - traceMetadata.startTime;
const leftOffset = ((span.timestamp - traceMetadata.startTime) * 1e2) / spread;
const width = (span.durationNano * 1e2) / (spread * 1e6);
const isDarkMode = useIsDarkMode();
let color = generateColor(
span.serviceName,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
if (span.hasError) {
color = `var(--bg-cherry-500)`;
}
return (
<div
className={cx(
'span-duration',
interestedSpanId === span.spanId ? 'interested-span' : '',
)}
onClick={(): void => {
setSpanDetails(span);
setTraceDetailsOpen(true);
}}
>
<div
className="span-line"
style={{
left: `${leftOffset}%`,
width: `${width}%`,
backgroundColor: color,
}}
/>
<Typography.Text
className="span-line-text"
style={{ left: `${leftOffset}%`, color }}
>{`${toFixed(time, 2)} ${timeUnitName}`}</Typography.Text>
</div>
);
}
// table config
const columnDefHelper = createColumnHelper<Span>();
function getWaterfallColumns({
handleCollapseUncollapse,
uncollapsedNodes,
interestedSpanId,
traceMetadata,
setTraceDetailsOpen,
setSpanDetails,
}: {
handleCollapseUncollapse: (id: string, collapse: boolean) => void;
uncollapsedNodes: string[];
interestedSpanId: IInterestedSpan;
traceMetadata: ITraceMetadata;
setTraceDetailsOpen: Dispatch<SetStateAction<boolean>>;
setSpanDetails: Dispatch<SetStateAction<Span | undefined>>;
}): ColumnDef<Span, any>[] {
const waterfallColumns: ColumnDef<Span, any>[] = [
columnDefHelper.display({
id: 'span-name',
header: '',
cell: (props): JSX.Element => (
<SpanOverview
span={props.row.original}
handleCollapseUncollapse={handleCollapseUncollapse}
isSpanCollapsed={!uncollapsedNodes.includes(props.row.original.spanId)}
interestedSpanId={interestedSpanId.spanId}
setTraceDetailsOpen={setTraceDetailsOpen}
setSpanDetails={setSpanDetails}
/>
),
size: 450,
}),
columnDefHelper.display({
id: 'span-duration',
header: () => <div />,
enableResizing: false,
cell: (props): JSX.Element => (
<SpanDuration
span={props.row.original}
interestedSpanId={interestedSpanId.spanId}
traceMetadata={traceMetadata}
setTraceDetailsOpen={setTraceDetailsOpen}
setSpanDetails={setSpanDetails}
/>
),
}),
];
return waterfallColumns;
}
function getItems(span: Span, startTime: number): TabsProps['items'] {
return [
{
label: (
<Button
type="text"
icon={<Bookmark size="14" />}
className="flamegraph-waterfall-toggle"
>
Attributes
</Button>
),
key: 'attributes',
children: <AttributesTable span={span} />,
},
{
label: (
<Button
type="text"
icon={<Anvil size="14" />}
className="flamegraph-waterfall-toggle"
>
Events
</Button>
),
key: 'events',
children: <EventsTable span={span} startTime={startTime} />,
},
];
}
function Success(props: ISuccessProps): JSX.Element {
const {
spans,
traceMetadata,
interestedSpanId,
uncollapsedNodes,
setInterestedSpanId,
} = props;
const virtualizerRef = useRef<Virtualizer<HTMLDivElement, Element>>();
const [traceDetailsOpen, setTraceDetailsOpen] = useState<boolean>(false);
const [spanDetails, setSpanDetails] = useState<Span>();
const handleCollapseUncollapse = useCallback(
(spanId: string, collapse: boolean) => {
setInterestedSpanId({ spanId, isUncollapsed: !collapse });
},
[setInterestedSpanId],
);
const handleVirtualizerInstanceChanged = (
instance: Virtualizer<HTMLDivElement, Element>,
): void => {
const { range } = instance;
if (spans.length < 500) return;
if (range?.startIndex === 0 && instance.isScrolling) {
if (spans[0].parentSpanId !== '') {
setInterestedSpanId({ spanId: spans[0].spanId, isUncollapsed: false });
}
return;
}
if (range?.endIndex === spans.length - 1 && instance.isScrolling) {
setInterestedSpanId({
spanId: spans[spans.length - 1].spanId,
isUncollapsed: false,
});
}
};
const columns = useMemo(
() =>
getWaterfallColumns({
handleCollapseUncollapse,
uncollapsedNodes,
interestedSpanId,
traceMetadata,
setTraceDetailsOpen,
setSpanDetails,
}),
[handleCollapseUncollapse, uncollapsedNodes, interestedSpanId, traceMetadata],
);
useEffect(() => {
if (interestedSpanId.spanId !== '' && virtualizerRef.current) {
const idx = spans.findIndex(
(span) => span.spanId === interestedSpanId.spanId,
);
if (idx !== -1)
virtualizerRef.current.scrollToIndex(idx, {
align: 'center',
behavior: 'auto',
});
}
}, [interestedSpanId, spans]);
const handleGoToRelatedLogs = useCallback(() => {
const query = getTraceToLogsQuery(
traceMetadata.traceId,
traceMetadata.startTime,
traceMetadata.endTime,
);
history.push(
`${ROUTES.LOGS_EXPLORER}?${createQueryParams({
[QueryParams.compositeQuery]: JSON.stringify(query),
// we subtract 1000 milliseconds from the start time to handle the cases when the trace duration is in nanoseconds
[QueryParams.startTime]: traceMetadata.startTime - 1000,
// we add 1000 milliseconds to the end time for nano second duration traces
[QueryParams.endTime]: traceMetadata.endTime + 1000,
})}`,
);
}, [traceMetadata.endTime, traceMetadata.startTime, traceMetadata.traceId]);
return (
<div className="success-content">
{traceMetadata.hasMissingSpans && (
<div className="missing-spans">
<section className="left-info">
<AlertCircle size={14} />
<Typography.Text className="text">
This trace has missing spans
</Typography.Text>
</section>
<Button
icon={<ArrowUpRight size={14} />}
className="right-info"
type="text"
>
Learn More
</Button>
</div>
)}
<TableV3
columns={columns}
data={spans}
config={{
handleVirtualizerInstanceChanged,
}}
customClassName="waterfall-table"
virtualiserRef={virtualizerRef}
/>
{spanDetails && (
<DetailsDrawer
open={traceDetailsOpen}
setOpen={setTraceDetailsOpen}
title="Span Details"
detailsDrawerClassName="span-dets"
defaultActiveKey="attributes"
items={getItems(spanDetails, traceMetadata.startTime)}
descriptiveContent={<DrawerDescriptiveContent span={spanDetails} />}
tabBarExtraContent={
<Button className="related-logs" onClick={handleGoToRelatedLogs}>
Go to Related Logs
</Button>
}
/>
)}
</div>
);
}
export default Success;

View File

@@ -0,0 +1,7 @@
export enum TraceWaterfallStates {
LOADING = 'LOADING',
SUCCESS = 'SUCCSS',
NO_DATA = 'NO_DATA',
ERROR = 'ERROR',
FETCHING_WITH_OLD_DATA_PRESENT = 'FETCHING_WTIH_OLD_DATA_PRESENT',
}

View File

@@ -0,0 +1,27 @@
import getTraceFlamegraph from 'api/trace/getTraceFlamegraph';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
GetTraceFlamegraphPayloadProps,
GetTraceFlamegraphSuccessResponse,
} from 'types/api/trace/getTraceFlamegraph';
const useGetTraceFlamegraph = (
props: GetTraceFlamegraphPayloadProps,
): UseLicense =>
useQuery({
queryFn: () => getTraceFlamegraph(props),
// if any of the props changes then we need to trigger an API call as the older data will be obsolete
queryKey: [REACT_QUERY_KEY.GET_TRACE_V2, props.traceId, props.selectedSpanId],
enabled: !!props.traceId,
keepPreviousData: true,
refetchOnWindowFocus: false,
});
type UseLicense = UseQueryResult<
SuccessResponse<GetTraceFlamegraphSuccessResponse> | ErrorResponse,
unknown
>;
export default useGetTraceFlamegraph;

View File

@@ -0,0 +1,30 @@
import getTraceV2 from 'api/trace/getTraceV2';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
GetTraceV2PayloadProps,
GetTraceV2SuccessResponse,
} from 'types/api/trace/getTraceV2';
const useGetTraceV2 = (props: GetTraceV2PayloadProps): UseLicense =>
useQuery({
queryFn: () => getTraceV2(props),
// if any of the props changes then we need to trigger an API call as the older data will be obsolete
queryKey: [
REACT_QUERY_KEY.GET_TRACE_V2,
props.traceId,
props.selectedSpanId,
props.isSelectedSpanIDUnCollapsed,
],
enabled: !!props.traceId,
keepPreviousData: true,
refetchOnWindowFocus: false,
});
type UseLicense = UseQueryResult<
SuccessResponse<GetTraceV2SuccessResponse> | ErrorResponse,
unknown
>;
export default useGetTraceV2;

View File

@@ -0,0 +1,33 @@
.old-trace-container {
display: flex;
flex-direction: column;
height: 100%;
.top-header {
display: flex;
flex-direction: row-reverse;
padding: 5px;
border-bottom: 1px solid var(--bg-slate-400);
.new-cta-btn {
display: flex;
padding: 4px 6px;
align-items: center;
gap: 8px;
color: var(--Vanilla-400, #c0c1c3);
/* Bifrost (Ancient)/Content/sm */
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
box-shadow: none;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
}
}

View File

@@ -1,10 +1,14 @@
import { Typography } from 'antd';
import './TraceDetail.styles.scss';
import { Button, Typography } from 'antd';
import getTraceItem from 'api/trace/getTraceItem';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import TraceDetailContainer from 'container/TraceDetail';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo } from 'react';
import { Undo } from 'lucide-react';
import TraceDetailsPage from 'pages/TraceDetailV2';
import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useParams } from 'react-router-dom';
import { Props as TraceDetailProps } from 'types/api/trace/getTraceItem';
@@ -13,6 +17,7 @@ import { noEventMessage } from './constants';
function TraceDetail(): JSX.Element {
const { id } = useParams<TraceDetailProps>();
const [showNewTraceDetails, setShowNewTraceDetails] = useState<boolean>(false);
const urlQuery = useUrlQuery();
const { spanId, levelUp, levelDown } = useMemo(
() => ({
@@ -31,6 +36,10 @@ function TraceDetail(): JSX.Element {
},
);
if (showNewTraceDetails) {
return <TraceDetailsPage />;
}
if (traceDetailResponse?.error || error || isError) {
return (
<Typography>
@@ -47,7 +56,21 @@ function TraceDetail(): JSX.Element {
return <NotFound text={noEventMessage} />;
}
return <TraceDetailContainer response={traceDetailResponse.payload} />;
return (
<div className="old-trace-container">
<div className="top-header">
<Button
onClick={(): void => setShowNewTraceDetails(true)}
icon={<Undo size={14} />}
type="text"
className="new-cta-btn"
>
New Trace Detail
</Button>
</div>
<TraceDetailContainer response={traceDetailResponse.payload} />;
</div>
);
}
export default TraceDetail;

View File

@@ -0,0 +1,125 @@
.not-found-trace {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60vh;
width: 500px;
gap: 24px;
margin: 0 auto;
.description {
display: flex;
flex-direction: column;
gap: 6px;
.not-found-img {
height: 32px;
width: 32px;
}
.not-found-text-1 {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.not-found-text-2 {
color: var(--Vanilla-100, #fff);
}
}
}
.reasons {
display: flex;
flex-direction: column;
gap: 12px;
.reason-1 {
display: flex;
padding: 12px;
align-items: flex-start;
gap: 12px;
border-radius: 4px;
background: rgba(171, 189, 255, 0.04);
.construction-img {
height: 16px;
width: 16px;
}
.text {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.reason-2 {
display: flex;
padding: 12px;
align-items: flex-start;
gap: 12px;
border-radius: 4px;
background: rgba(171, 189, 255, 0.04);
.broom-img {
height: 16px;
width: 16px;
}
.text {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
.none-of-above {
display: flex;
flex-direction: column;
gap: 12px;
.text {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.action-btns {
display: flex;
gap: 8px;
.action-btn {
display: flex;
width: 160px;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 8px;
border-radius: 2px;
border: 1px solid var(--Slate-400, #1d212d);
background: var(--Slate-500, #161922);
box-shadow: none;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
}
}
}

View File

@@ -0,0 +1,65 @@
import './NoData.styles.scss';
import { Button, Typography } from 'antd';
import { LifeBuoy, RefreshCw } from 'lucide-react';
import { handleContactSupport } from 'pages/Integrations/utils';
import { isCloudUser } from 'utils/app';
function NoData(): JSX.Element {
const isCloudUserVal = isCloudUser();
return (
<div className="not-found-trace">
<section className="description">
<img src="/Icons/no-data.svg" alt="no-data" className="not-found-img" />
<Typography.Text className="not-found-text-1">
Uh-oh! We cannot show the selected trace.
<span className="not-found-text-2">
This can happen in either of the two scenraios -
</span>
</Typography.Text>
</section>
<section className="reasons">
<div className="reason-1">
<img
src="/Icons/construction.svg"
alt="no-data"
className="construction-img"
/>
<Typography.Text className="text">
The trace data has not been rendered on your SigNoz server yet. You can
wait for a bit and refresh this page if this is the case.
</Typography.Text>
</div>
<div className="reason-2">
<img src="/Icons/broom.svg" alt="no-data" className="broom-img" />
<Typography.Text className="text">
The trace has been deleted as the data has crossed its retention period.
</Typography.Text>
</div>
</section>
<section className="none-of-above">
<Typography.Text className="text">
If you feel the issue is none of the above, please contact support.
</Typography.Text>
<div className="action-btns">
<Button
className="action-btn"
icon={<RefreshCw size={14} />}
onClick={(): void => window.location.reload()}
>
Refresh this page
</Button>
<Button
className="action-btn"
icon={<LifeBuoy size={14} />}
onClick={(): void => handleContactSupport(isCloudUserVal)}
>
Contact Support
</Button>
</div>
</section>
</div>
);
}
export default NoData;

View File

@@ -0,0 +1,134 @@
.traces-module-container {
.ant-tabs-tab {
.tab-item {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.ant-tabs-tab-active {
.tab-item {
color: var(--bg-vanilla-100);
}
}
.ant-tabs-nav {
margin: 0px;
padding: 0px !important;
}
.ant-tabs-nav-list {
transform: translate(15px, 0px) !important;
}
.old-switch {
display: flex;
align-items: center;
color: var(--Vanilla-400, #c0c1c3);
/* Bifrost (Ancient)/Content/sm */
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
.trace-layout {
display: flex;
flex-direction: column;
gap: 25px;
padding-top: 16px;
.flamegraph-waterfall-toggle {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
height: 31px;
color: var(--bg-vanilla-400);
padding: 5px 20px;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.ant-btn-icon {
margin-inline-end: 0px !important;
}
}
.span-list-toggle {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
height: 31px;
padding: 5px 20px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.ant-btn-icon {
margin-inline-end: 0px !important;
}
}
.ant-tabs-tab {
border-radius: 2px 0px 0px 0px;
background: var(--bg-ink-400);
border-radius: 2px 2px 0px 0px;
border: 1px solid var(--bg-slate-400);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
height: 31px;
}
.ant-tabs-tab-active {
background-color: var(--bg-ink-500);
.ant-btn {
color: var(--bg-vanilla-100) !important;
}
}
.ant-tabs-tab + .ant-tabs-tab {
margin: 0px;
border-left: 0px;
}
.ant-tabs-ink-bar {
height: 1px !important;
background: var(--bg-ink-500) !important;
}
.ant-tabs-nav-list {
transform: translate(15px, 0px) !important;
}
.ant-tabs-nav::before {
border-bottom: 1px solid var(--bg-slate-400);
}
.ant-tabs-nav {
margin: 0px;
padding: 0px !important;
}
}
}

View File

@@ -0,0 +1,110 @@
import './TraceDetailV2.styles.scss';
import { Button, Tabs } from 'antd';
import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph';
import TraceMetadata from 'container/TraceMetadata/TraceMetadata';
import TraceWaterfall, {
IInterestedSpan,
} from 'container/TraceWaterfall/TraceWaterfall';
import useGetTraceV2 from 'hooks/trace/useGetTraceV2';
import useUrlQuery from 'hooks/useUrlQuery';
import { DraftingCompass } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { TraceDetailV2URLProps } from 'types/api/trace/getTraceV2';
import NoData from './NoData/NoData';
function TraceDetailsV2(): JSX.Element {
const { id: traceId } = useParams<TraceDetailV2URLProps>();
const urlQuery = useUrlQuery();
const [interestedSpanId, setInterestedSpanId] = useState<IInterestedSpan>(
() => ({
spanId: urlQuery.get('spanId') || '',
isUncollapsed: urlQuery.get('spanId') !== '',
}),
);
useEffect(() => {
setInterestedSpanId({
spanId: urlQuery.get('spanId') || '',
isUncollapsed: urlQuery.get('spanId') !== '',
});
}, [urlQuery]);
const [uncollapsedNodes, setUncollapsedNodes] = useState<string[]>([]);
const {
data: traceData,
isFetching: isFetchingTraceData,
error: errorFetchingTraceData,
} = useGetTraceV2({
traceId,
uncollapsedSpans: uncollapsedNodes,
selectedSpanId: interestedSpanId.spanId,
isSelectedSpanIDUnCollapsed: interestedSpanId.isUncollapsed,
});
useEffect(() => {
if (traceData && traceData.payload && traceData.payload.uncollapsedSpans) {
setUncollapsedNodes(traceData.payload.uncollapsedSpans);
}
}, [traceData]);
const items = [
{
label: (
<Button
type="text"
icon={<DraftingCompass size="14" />}
className="flamegraph-waterfall-toggle"
>
Flamegraph
</Button>
),
key: 'flamegraph',
children: (
<>
<TraceFlamegraph
serviceExecTime={traceData?.payload?.serviceNameToTotalDurationMap || {}}
startTime={traceData?.payload?.startTimestampMillis || 0}
endTime={traceData?.payload?.endTimestampMillis || 0}
/>
<TraceWaterfall
traceData={traceData}
isFetchingTraceData={isFetchingTraceData}
errorFetchingTraceData={errorFetchingTraceData}
traceId={traceId}
interestedSpanId={interestedSpanId}
setInterestedSpanId={setInterestedSpanId}
uncollapsedNodes={uncollapsedNodes}
/>
</>
),
},
];
return (
<div className="trace-layout">
<TraceMetadata
traceID={traceId}
duration={
(traceData?.payload?.endTimestampMillis || 0) -
(traceData?.payload?.startTimestampMillis || 0)
}
startTime={(traceData?.payload?.startTimestampMillis || 0) / 1e3}
rootServiceName={traceData?.payload?.rootServiceName || ''}
rootSpanName={traceData?.payload?.rootServiceEntryPoint || ''}
totalErrorSpans={traceData?.payload?.totalErrorSpansCount || 0}
totalSpans={traceData?.payload?.totalSpansCount || 0}
notFound={(traceData?.payload?.spans.length || 0) === 0}
/>
{(traceData?.payload?.spans.length || 0) > 0 ? (
<Tabs items={items} animated className="settings-tabs" />
) : (
<NoData />
)}
</div>
);
}
export default TraceDetailsV2;

View File

@@ -0,0 +1,67 @@
import './TraceDetailV2.styles.scss';
import { Button, Tabs } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { Compass, TowerControl, Undo } from 'lucide-react';
import TraceDetail from 'pages/TraceDetail';
import { useCallback, useState } from 'react';
import TraceDetailsV2 from './TraceDetailV2';
export default function TraceDetailsPage(): JSX.Element {
const [showOldTraceDetails, setShowOldTraceDetails] = useState<boolean>(false);
const items = [
{
label: (
<div className="tab-item">
<Compass size={16} /> Explorer
</div>
),
key: 'trace-details',
children: <TraceDetailsV2 />,
},
{
label: (
<div className="tab-item">
<TowerControl size={16} /> Views
</div>
),
key: 'saved-views',
children: <div />,
},
];
const handleOldTraceDetails = useCallback(() => {
setShowOldTraceDetails(true);
}, []);
return showOldTraceDetails ? (
<TraceDetail />
) : (
<div className="traces-module-container">
<Tabs
items={items}
animated
className="trace-module"
onTabClick={(activeKey): void => {
if (activeKey === 'saved-views') {
history.push(ROUTES.TRACES_SAVE_VIEWS);
}
if (activeKey === 'trace-details') {
history.push(ROUTES.TRACES_EXPLORER);
}
}}
tabBarExtraContent={
<Button
type="text"
onClick={handleOldTraceDetails}
className="old-switch"
icon={<Undo size={14} />}
>
Old Trace Details
</Button>
}
/>
</div>
);
}

View File

@@ -0,0 +1,26 @@
export interface TraceDetailFlamegraphURLProps {
id: string;
}
export interface GetTraceFlamegraphPayloadProps {
traceId: string;
selectedSpanId: string;
}
export interface FlamegraphSpan {
timestamp: number;
durationNano: number;
spanId: string;
parentSpanId: string;
traceId: string;
hasError: boolean;
serviceName: string;
name: string;
level: number;
}
export interface GetTraceFlamegraphSuccessResponse {
spans: FlamegraphSpan[][];
startTimestampMillis: number;
endTimestampMillis: number;
}

View File

@@ -0,0 +1,52 @@
export interface TraceDetailV2URLProps {
id: string;
}
export interface GetTraceV2PayloadProps {
traceId: string;
selectedSpanId: string;
uncollapsedSpans: string[];
isSelectedSpanIDUnCollapsed: boolean;
}
export interface Event {
name: string;
timeUnixNano: number;
attributeMap: Record<string, string>;
}
export interface Span {
timestamp: number;
durationNano: number;
spanId: string;
rootSpanId: string;
parentSpanId: string;
traceId: string;
hasError: boolean;
kind: number;
serviceName: string;
name: string;
references: any;
tagMap: Record<string, string>;
event: string[];
rootName: string;
statusMessage: string;
statusCodeString: string;
spanKind: string;
hasChildren: boolean;
hasSibling: boolean;
subTreeNodeCount: number;
level: number;
}
export interface GetTraceV2SuccessResponse {
spans: Span[];
hasMissingSpans: boolean;
uncollapsedSpans: string[];
startTimestampMillis: number;
endTimestampMillis: number;
totalSpansCount: number;
totalErrorSpansCount: number;
rootServiceName: string;
rootServiceEntryPoint: string;
serviceNameToTotalDurationMap: Record<string, number>;
}

View File

@@ -27,6 +27,16 @@ export const getFormattedDateWithMinutes = (epochTimestamp: number): string => {
return date.format('DD MMM YYYY HH:mm');
};
export const getFormattedDateWithMinutesAndSeconds = (
epochTimestamp: number,
): string => {
// Convert epoch timestamp to a date
const date = dayjs.unix(epochTimestamp);
// Format the date as "18 Nov 2013"
return date.format('DD MMM YYYY HH:mm:ss');
};
export const getRemainingDays = (billingEndDate: number): number => {
// Convert Epoch timestamps to Date objects
const startDate = new Date(); // Convert seconds to milliseconds

View File

@@ -3690,11 +3690,23 @@
dependencies:
"@tanstack/table-core" "8.20.5"
"@tanstack/react-virtual@3.11.2":
version "3.11.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322"
integrity sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==
dependencies:
"@tanstack/virtual-core" "3.11.2"
"@tanstack/table-core@8.20.5":
version "8.20.5"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
"@tanstack/virtual-core@3.11.2":
version "3.11.2"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212"
integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==
"@testing-library/dom@^8.5.0":
version "8.20.0"
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"

View File

@@ -33,6 +33,8 @@ import (
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/jmoiron/sqlx"
"go.signoz.io/signoz/pkg/cache"
cacheV2 "go.signoz.io/signoz/pkg/cache"
promModel "github.com/prometheus/common/model"
"go.uber.org/zap"
@@ -156,6 +158,9 @@ type ClickHouseReader struct {
traceLocalTableName string
traceResourceTableV3 string
traceSummaryTable string
fluxInterval time.Duration
cacheV2 cacheV2.Cache
}
// NewTraceReader returns a TraceReader for the database
@@ -169,6 +174,8 @@ func NewReader(
cluster string,
useLogsNewSchema bool,
useTraceNewSchema bool,
fluxInterval time.Duration,
cacheV2 cacheV2.Cache,
) *ClickHouseReader {
datasource := os.Getenv("ClickHouseUrl")
@@ -179,7 +186,7 @@ func NewReader(
zap.L().Fatal("failed to initialize ClickHouse", zap.Error(err))
}
return NewReaderFromClickhouseConnection(db, options, localDB, configFile, featureFlag, cluster, useLogsNewSchema, useTraceNewSchema)
return NewReaderFromClickhouseConnection(db, options, localDB, configFile, featureFlag, cluster, useLogsNewSchema, useTraceNewSchema, fluxInterval, cacheV2)
}
func NewReaderFromClickhouseConnection(
@@ -191,6 +198,8 @@ func NewReaderFromClickhouseConnection(
cluster string,
useLogsNewSchema bool,
useTraceNewSchema bool,
fluxInterval time.Duration,
cacheV2 cacheV2.Cache,
) *ClickHouseReader {
alertManager, err := am.New()
if err != nil {
@@ -277,6 +286,9 @@ func NewReaderFromClickhouseConnection(
traceTableName: traceTableName,
traceResourceTableV3: options.primary.TraceResourceTableV3,
traceSummaryTable: options.primary.TraceSummaryTable,
fluxInterval: fluxInterval,
cacheV2: cacheV2,
}
}
@@ -1442,6 +1454,516 @@ func (r *ClickHouseReader) SearchTraces(ctx context.Context, params *model.Searc
return &searchSpansResult, nil
}
type Interval struct {
StartTime uint64
Duration uint64
Service string
}
func calculateServiceTime(serviceIntervals map[string][]Interval) map[string]uint64 {
totalTimes := make(map[string]uint64)
for service, serviceIntervals := range serviceIntervals {
sort.Slice(serviceIntervals, func(i, j int) bool {
return serviceIntervals[i].StartTime < serviceIntervals[j].StartTime
})
mergedIntervals := mergeIntervals(serviceIntervals)
totalTime := uint64(0)
for _, interval := range mergedIntervals {
totalTime += interval.Duration
}
totalTimes[service] = totalTime
}
return totalTimes
}
func mergeIntervals(intervals []Interval) []Interval {
if len(intervals) == 0 {
return nil
}
var merged []Interval
current := intervals[0]
for i := 1; i < len(intervals); i++ {
next := intervals[i]
if current.StartTime+current.Duration >= next.StartTime {
endTime := max(current.StartTime+current.Duration, next.StartTime+next.Duration)
current.Duration = endTime - current.StartTime
} else {
merged = append(merged, current)
current = next
}
}
// Add the last interval
merged = append(merged, current)
return merged
}
func max(a, b uint64) uint64 {
if a > b {
return a
}
return b
}
func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Context, traceID string, req *model.GetWaterfallSpansForTraceWithMetadataParams) (*model.GetWaterfallSpansForTraceWithMetadataResponse, *model.ApiError) {
response := new(model.GetWaterfallSpansForTraceWithMetadataResponse)
var startTime, endTime, durationNano, totalErrorSpans uint64
var spanIdToSpanNodeMap = map[string]*model.Span{}
var traceRoots []*model.Span
var serviceNameToTotalDurationMap = map[string]uint64{}
var useCache bool = true
cachedTraceData := new(model.GetWaterfallSpansForTraceWithMetadataCache)
cacheStatus, err := r.cacheV2.Retrieve(ctx, fmt.Sprintf("getWaterfallSpansForTraceWithMetadata-%v", traceID), cachedTraceData, false)
if err != nil {
zap.L().Debug("error in retrieving getWaterfallSpansForTraceWithMetadata cache", zap.Error(err))
useCache = false
}
if cacheStatus != cache.RetrieveStatusHit {
useCache = false
}
if err == nil && cacheStatus == cache.RetrieveStatusHit {
if time.Since(time.UnixMilli(int64(cachedTraceData.EndTime))) < r.fluxInterval {
useCache = false
}
if useCache {
zap.L().Info("cache is successfully hit, applying cache for getWaterfallSpansForTraceWithMetadata", zap.String("traceID", traceID))
startTime = cachedTraceData.StartTime
endTime = cachedTraceData.EndTime
durationNano = cachedTraceData.DurationNano
spanIdToSpanNodeMap = cachedTraceData.SpanIdToSpanNodeMap
serviceNameToTotalDurationMap = cachedTraceData.ServiceNameToTotalDurationMap
traceRoots = cachedTraceData.TraceRoots
response.TotalSpansCount = cachedTraceData.TotalSpans
totalErrorSpans = cachedTraceData.TotalErrorSpans
}
}
if !useCache {
zap.L().Info("cache miss for getWaterfallSpansForTraceWithMetadata", zap.String("traceID", traceID))
var traceSummary model.TraceSummary
summaryQuery := fmt.Sprintf("SELECT * from %s.%s WHERE trace_id=$1", r.TraceDB, r.traceSummaryTable)
err := r.db.QueryRow(ctx, summaryQuery, traceID).Scan(&traceSummary.TraceID, &traceSummary.Start, &traceSummary.End, &traceSummary.NumSpans)
if err != nil {
if err == sql.ErrNoRows {
return response, nil
}
zap.L().Error("Error in processing sql query", zap.Error(err))
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query: %w", err)}
}
response.TotalSpansCount = traceSummary.NumSpans
var searchScanResponses []model.SpanItemV2
query := fmt.Sprintf("SELECT timestamp, duration_nano, span_id, trace_id, has_error, kind, resource_string_service$$name, name, references, attributes_string, attributes_number, attributes_bool, resources_string, events, status_message, status_code_string, kind_string , parent_span_id FROM %s.%s WHERE trace_id=$1 and ts_bucket_start>=$2 and ts_bucket_start<=$3 ORDER BY timestamp ASC, name ASC", r.TraceDB, r.traceTableName)
start := time.Now()
err = r.db.Select(ctx, &searchScanResponses, query, traceID, strconv.FormatInt(traceSummary.Start.Unix()-1800, 10), strconv.FormatInt(traceSummary.End.Unix(), 10))
zap.L().Info(query)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query: %w", err)}
}
end := time.Now()
zap.L().Debug("GetWaterfallSpansForTraceWithMetadata took: ", zap.Duration("duration", end.Sub(start)))
var serviceNameIntervalMap = map[string][]Interval{}
for _, item := range searchScanResponses {
ref := []model.OtelSpanRef{}
err := json.Unmarshal([]byte(item.References), &ref)
if err != nil {
zap.L().Error("Error unmarshalling references", zap.Error(err))
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error in unmarshalling references: %w", err)}
}
// merge attributes_number and attributes_bool to attributes_string
for k, v := range item.Attributes_bool {
item.Attributes_string[k] = fmt.Sprintf("%v", v)
}
for k, v := range item.Attributes_number {
item.Attributes_string[k] = fmt.Sprintf("%v", v)
}
for k, v := range item.Resources_string {
item.Attributes_string[k] = v
}
jsonItem := model.Span{
SpanID: item.SpanID,
TraceID: item.TraceID,
ServiceName: item.ServiceName,
Name: item.Name,
Kind: int32(item.Kind),
DurationNano: item.DurationNano,
HasError: item.HasError,
StatusMessage: item.StatusMessage,
StatusCodeString: item.StatusCodeString,
SpanKind: item.SpanKind,
References: ref,
Events: item.Events,
TagMap: item.Attributes_string,
ParentSpanId: item.ParentSpanId,
Children: make([]*model.Span, 0),
}
jsonItem.TimeUnixNano = uint64(item.TimeUnixNano.UnixNano() / 1000000)
serviceNameIntervalMap[jsonItem.ServiceName] =
append(serviceNameIntervalMap[jsonItem.ServiceName], Interval{StartTime: jsonItem.TimeUnixNano, Duration: jsonItem.DurationNano / 1000000, Service: jsonItem.ServiceName})
spanIdToSpanNodeMap[jsonItem.SpanID] = &jsonItem
if startTime == 0 || jsonItem.TimeUnixNano < startTime {
startTime = jsonItem.TimeUnixNano
}
if endTime == 0 || (jsonItem.TimeUnixNano+(jsonItem.DurationNano/1000000)) > endTime {
endTime = jsonItem.TimeUnixNano + (jsonItem.DurationNano / 1000000)
}
if durationNano == 0 || jsonItem.DurationNano > durationNano {
durationNano = jsonItem.DurationNano
}
if jsonItem.HasError {
totalErrorSpans = totalErrorSpans + 1
}
}
serviceNameToTotalDurationMap = calculateServiceTime(serviceNameIntervalMap)
// traverse through the map and append each node to the children array of the parent node
// capture the root nodes as well
for _, spanNode := range spanIdToSpanNodeMap {
hasParentRelationship := false
for _, reference := range spanNode.References {
if reference.RefType == "CHILD_OF" && reference.SpanId != "" {
hasParentRelationship = true
if parentNode, exists := spanIdToSpanNodeMap[reference.SpanId]; exists {
parentNode.Children = append(parentNode.Children, spanNode)
} else {
// insert the missing spans
missingSpan := model.Span{
SpanID: reference.SpanId,
TraceID: spanNode.TraceID,
ServiceName: "",
Name: "Missing Span",
TimeUnixNano: spanNode.TimeUnixNano,
Kind: 0,
DurationNano: spanNode.DurationNano,
HasError: false,
StatusMessage: "",
StatusCodeString: "",
SpanKind: "",
Children: make([]*model.Span, 0),
}
missingSpan.Children = append(missingSpan.Children, spanNode)
spanIdToSpanNodeMap[missingSpan.SpanID] = &missingSpan
traceRoots = append(traceRoots, &missingSpan)
}
}
}
if !hasParentRelationship {
traceRoots = append(traceRoots, spanNode)
}
}
sort.Slice(traceRoots, func(i, j int) bool {
if traceRoots[i].TimeUnixNano == traceRoots[j].TimeUnixNano {
return traceRoots[i].Name < traceRoots[j].Name
}
return traceRoots[i].TimeUnixNano < traceRoots[j].TimeUnixNano
})
traceCache := model.GetWaterfallSpansForTraceWithMetadataCache{
StartTime: startTime,
EndTime: endTime,
DurationNano: durationNano,
TotalSpans: traceSummary.NumSpans,
TotalErrorSpans: totalErrorSpans,
SpanIdToSpanNodeMap: spanIdToSpanNodeMap,
ServiceNameToTotalDurationMap: serviceNameToTotalDurationMap,
TraceRoots: traceRoots,
}
err = r.cacheV2.Store(ctx, fmt.Sprintf("getWaterfallSpansForTraceWithMetadata-%v", traceID), &traceCache, time.Minute*5)
if err != nil {
zap.L().Debug("failed to store cache fpr getWaterfallSpansForTraceWithMetadata", zap.String("traceID", traceID), zap.Error(err))
}
}
var preOrderTraversal = []*model.Span{}
uncollapsedSpans := req.UncollapsedSpans
selectedSpanIndex := -1
for _, rootSpanID := range traceRoots {
if rootNode, exists := spanIdToSpanNodeMap[rootSpanID.SpanID]; exists {
_, _spansFromRootToNode := getPathFromRootToSelectedSpanId(rootNode, req.SelectedSpanID, uncollapsedSpans, req.IsSelectedSpanIDUnCollapsed)
uncollapsedSpans = append(uncollapsedSpans, _spansFromRootToNode...)
_preOrderTraversal := traverseTraceAndAddRequiredMetadata(rootNode, uncollapsedSpans, 0, true, false, req.SelectedSpanID)
_selectedSpanIndex := findIndexForSelectedSpanFromPreOrder(_preOrderTraversal, req.SelectedSpanID)
if _selectedSpanIndex != -1 {
selectedSpanIndex = _selectedSpanIndex + len(preOrderTraversal)
}
preOrderTraversal = append(preOrderTraversal, _preOrderTraversal...)
if response.RootServiceName == "" {
response.RootServiceName = rootNode.ServiceName
}
if response.RootServiceEntryPoint == "" {
response.RootServiceEntryPoint = rootNode.Name
}
}
}
// the index of the interested span id shouldn't be -1 as the span should exist
if selectedSpanIndex == -1 && req.SelectedSpanID != "" {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("selected span ID not found in the traversal")}
}
// get the 0.4*[span limit] before the interested span index
startIndex := selectedSpanIndex - 200
// get the 0.6*[span limit] after the intrested span index
endIndex := selectedSpanIndex + 300
// adjust the sliding window according to the available left and right spaces.
if startIndex < 0 {
endIndex = endIndex - startIndex
startIndex = 0
}
if endIndex > len(preOrderTraversal) {
startIndex = startIndex - (endIndex - len(preOrderTraversal))
endIndex = len(preOrderTraversal)
}
if startIndex < 0 {
startIndex = 0
}
selectedSpans := preOrderTraversal[startIndex:endIndex]
// generate the response [ spans , metadata ]
response.Spans = selectedSpans
response.UncollapsedSpans = uncollapsedSpans
response.StartTimestampMillis = startTime
response.EndTimestampMillis = endTime
response.TotalErrorSpansCount = totalErrorSpans
response.ServiceNameToTotalDurationMap = serviceNameToTotalDurationMap
response.HasMissingSpans = len(traceRoots) > 1
return response, nil
}
func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, traceID string, req *model.GetFlamegraphSpansForTraceParams) (*model.GetFlamegraphSpansForTraceResponse, *model.ApiError) {
trace := new(model.GetFlamegraphSpansForTraceResponse)
var startTime, endTime, durationNano uint64
var spanIdToSpanNodeMap = map[string]*model.FlamegraphSpan{}
// map[traceID][level]span
var traceIdLevelledFlamegraph = map[string]map[int64][]*model.FlamegraphSpan{}
var selectedSpans = [][]*model.FlamegraphSpan{}
var traceRoots []*model.FlamegraphSpan
var useCache bool = true
// get the trace tree from cache!
cachedTraceData := new(model.GetFlamegraphSpansForTraceCache)
cacheStatus, err := r.cacheV2.Retrieve(ctx, fmt.Sprintf("getFlamegraphSpansForTrace-%v", traceID), cachedTraceData, false)
if err != nil {
zap.L().Debug("error in retrieving getFlamegraphSpansForTrace cache", zap.Error(err))
useCache = false
}
if cacheStatus != cache.RetrieveStatusHit {
useCache = false
}
if err == nil && cacheStatus == cache.RetrieveStatusHit {
if time.Since(time.UnixMilli(int64(cachedTraceData.EndTime))) < r.fluxInterval {
useCache = false
}
if useCache {
zap.L().Info("cache is successfully hit, applying cache for getFlamegraphSpansForTrace", zap.String("traceID", traceID))
startTime = cachedTraceData.StartTime
endTime = cachedTraceData.EndTime
durationNano = cachedTraceData.DurationNano
selectedSpans = cachedTraceData.SelectedSpans
traceRoots = cachedTraceData.TraceRoots
}
}
if !useCache {
zap.L().Info("cache miss for getFlamegraphSpansForTrace", zap.String("traceID", traceID))
// fetch the start, end and number of spans from the summary table, start and end are required for the trace query
var traceSummary model.TraceSummary
summaryQuery := fmt.Sprintf("SELECT * from %s.%s WHERE trace_id=$1", r.TraceDB, r.traceSummaryTable)
err := r.db.QueryRow(ctx, summaryQuery, traceID).Scan(&traceSummary.TraceID, &traceSummary.Start, &traceSummary.End, &traceSummary.NumSpans)
if err != nil {
if err == sql.ErrNoRows {
return trace, nil
}
zap.L().Error("Error in processing sql query", zap.Error(err))
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query: %w", err)}
}
// fetch all the spans belonging to the trace from the main table
var searchScanResponses []model.SpanItemV2
query := fmt.Sprintf("SELECT timestamp, duration_nano, span_id, trace_id, has_error,references, resource_string_service$$name, name,parent_span_id FROM %s.%s WHERE trace_id=$1 and ts_bucket_start>=$2 and ts_bucket_start<=$3 ORDER BY timestamp ASC, name ASC", r.TraceDB, r.traceTableName)
start := time.Now()
err = r.db.Select(ctx, &searchScanResponses, query, traceID, strconv.FormatInt(traceSummary.Start.Unix()-1800, 10), strconv.FormatInt(traceSummary.End.Unix(), 10))
zap.L().Info(query)
if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err))
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query: %w", err)}
}
end := time.Now()
zap.L().Debug("getFlamegraphSpansForTrace took: ", zap.Duration("duration", end.Sub(start)))
// create the trace tree based on the spans fetched above
// create a map of [spanId]: spanNode
for _, item := range searchScanResponses {
ref := []model.OtelSpanRef{}
err := json.Unmarshal([]byte(item.References), &ref)
if err != nil {
zap.L().Error("Error unmarshalling references", zap.Error(err))
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error in unmarshalling references: %w", err)}
}
// create the span node
jsonItem := model.FlamegraphSpan{
SpanID: item.SpanID,
TraceID: item.TraceID,
ServiceName: item.ServiceName,
Name: item.Name,
DurationNano: (item.DurationNano),
HasError: item.HasError,
ParentSpanId: item.ParentSpanId,
References: ref,
Children: make([]*model.FlamegraphSpan, 0),
}
jsonItem.TimeUnixNano = uint64(item.TimeUnixNano.UnixNano() / 1000000)
// assign the span node to the span map
spanIdToSpanNodeMap[jsonItem.SpanID] = &jsonItem
// metadata calculation
if startTime == 0 || jsonItem.TimeUnixNano < startTime {
startTime = jsonItem.TimeUnixNano
}
if endTime == 0 || (jsonItem.TimeUnixNano+(jsonItem.DurationNano/1000000)) > endTime {
endTime = jsonItem.TimeUnixNano + (jsonItem.DurationNano / 1000000)
}
if durationNano == 0 || uint64(jsonItem.DurationNano) > durationNano {
durationNano = uint64(jsonItem.DurationNano)
}
}
// traverse through the map and append each node to the children array of the parent node
for _, spanNode := range spanIdToSpanNodeMap {
hasParentRelationship := false
for _, reference := range spanNode.References {
if reference.RefType == "CHILD_OF" && reference.SpanId != "" {
hasParentRelationship = true
if parentNode, exists := spanIdToSpanNodeMap[reference.SpanId]; exists {
parentNode.Children = append(parentNode.Children, spanNode)
} else {
// insert the missing spans
missingSpan := model.FlamegraphSpan{
SpanID: reference.SpanId,
TraceID: spanNode.TraceID,
ServiceName: "",
Name: "Missing Span",
TimeUnixNano: spanNode.TimeUnixNano,
DurationNano: spanNode.DurationNano,
HasError: false,
Children: make([]*model.FlamegraphSpan, 0),
}
missingSpan.Children = append(missingSpan.Children, spanNode)
spanIdToSpanNodeMap[missingSpan.SpanID] = &missingSpan
traceRoots = append(traceRoots, &missingSpan)
}
}
}
if !hasParentRelationship {
traceRoots = append(traceRoots, spanNode)
}
}
sort.Slice(traceRoots, func(i, j int) bool {
if traceRoots[i].TimeUnixNano == traceRoots[j].TimeUnixNano {
return traceRoots[i].Name < traceRoots[j].Name
}
return traceRoots[i].TimeUnixNano < traceRoots[j].TimeUnixNano
})
var bfsMapForTrace = map[int64][]*model.FlamegraphSpan{}
for _, rootSpanID := range traceRoots {
if rootNode, exists := spanIdToSpanNodeMap[rootSpanID.SpanID]; exists {
bfsMapForTrace = map[int64][]*model.FlamegraphSpan{}
bfsTraversalForTrace(rootNode, 0, &bfsMapForTrace)
traceIdLevelledFlamegraph[rootSpanID.SpanID] = bfsMapForTrace
}
}
for _, trace := range traceRoots {
keys := make([]int64, 0, len(traceIdLevelledFlamegraph[trace.SpanID]))
for key := range traceIdLevelledFlamegraph[trace.SpanID] {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
return keys[i] < keys[j]
})
for _, level := range keys {
if ok, exists := traceIdLevelledFlamegraph[trace.SpanID][level]; exists {
selectedSpans = append(selectedSpans, ok)
}
}
}
traceCache := model.GetFlamegraphSpansForTraceCache{
StartTime: startTime,
EndTime: endTime,
DurationNano: durationNano,
SelectedSpans: selectedSpans,
TraceRoots: traceRoots,
}
err = r.cacheV2.Store(ctx, fmt.Sprintf("getFlamegraphSpansForTrace-%v", traceID), &traceCache, time.Minute*5)
if err != nil {
zap.L().Debug("failed to store cache for getFlamegraphSpansForTrace", zap.String("traceID", traceID), zap.Error(err))
}
}
var selectedIndex int64 = 0
if req.SelectedSpanID != "" {
selectedIndex = findIndexForSelectedSpan(selectedSpans, req.SelectedSpanID)
}
lowerLimit := selectedIndex - 20
upperLimit := selectedIndex + 30
if lowerLimit < 0 {
upperLimit = upperLimit - lowerLimit
lowerLimit = 0
}
if upperLimit > int64(len(selectedSpans)) {
lowerLimit = lowerLimit - (upperLimit - int64(len(selectedSpans)))
upperLimit = int64(len(selectedSpans))
}
if lowerLimit < 0 {
lowerLimit = 0
}
trace.Spans = selectedSpans[lowerLimit:upperLimit]
trace.StartTimestampMillis = startTime
trace.EndTimestampMillis = endTime
return trace, nil
}
func (r *ClickHouseReader) GetDependencyGraph(ctx context.Context, queryParams *model.GetServicesParams) (*[]model.ServiceMapDependencyResponseItem, error) {
response := []model.ServiceMapDependencyResponseItem{}

View File

@@ -0,0 +1,125 @@
package clickhouseReader
import (
"sort"
"go.signoz.io/signoz/pkg/query-service/model"
)
func contains(slice []string, item string) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string, uncollapsedSpans []string, isSelectedSpanIDUnCollapsed bool) (bool, []string) {
spansFromRootToNode := []string{}
if node.SpanID == selectedSpanId {
if isSelectedSpanIDUnCollapsed {
spansFromRootToNode = append(spansFromRootToNode, node.SpanID)
}
return true, spansFromRootToNode
}
isPresentInSubtreeForTheNode := false
for _, child := range node.Children {
isPresentInThisSubtree, _spansFromRootToNode := getPathFromRootToSelectedSpanId(child, selectedSpanId, uncollapsedSpans, isSelectedSpanIDUnCollapsed)
// if the interested node is present in the given subtree then add the span node to uncollapsed node list
if isPresentInThisSubtree {
if !contains(uncollapsedSpans, node.SpanID) {
spansFromRootToNode = append(spansFromRootToNode, node.SpanID)
}
isPresentInSubtreeForTheNode = true
spansFromRootToNode = append(spansFromRootToNode, _spansFromRootToNode...)
}
}
return isPresentInSubtreeForTheNode, spansFromRootToNode
}
func traverseTraceAndAddRequiredMetadata(span *model.Span, uncollapsedSpans []string, level uint64, isPartOfPreorder bool, hasSibling bool, selectedSpanId string) []*model.Span {
preOrderTraversal := []*model.Span{}
sort.Slice(span.Children, func(i, j int) bool {
if span.Children[i].TimeUnixNano == span.Children[j].TimeUnixNano {
return span.Children[i].Name < span.Children[j].Name
}
return span.Children[i].TimeUnixNano < span.Children[j].TimeUnixNano
})
span.SubTreeNodeCount = 0
nodeWithoutChildren := model.Span{
SpanID: span.SpanID,
TraceID: span.TraceID,
ServiceName: span.ServiceName,
TimeUnixNano: span.TimeUnixNano,
Name: span.Name,
Kind: int32(span.Kind),
DurationNano: span.DurationNano,
HasError: span.HasError,
StatusMessage: span.StatusMessage,
StatusCodeString: span.StatusCodeString,
SpanKind: span.SpanKind,
References: span.References,
Events: span.Events,
TagMap: span.TagMap,
ParentSpanId: span.ParentSpanId,
Children: make([]*model.Span, 0),
HasChildren: len(span.Children) > 0,
Level: level,
HasSiblings: hasSibling,
SubTreeNodeCount: 0,
}
if isPartOfPreorder {
preOrderTraversal = append(preOrderTraversal, &nodeWithoutChildren)
}
for index, child := range span.Children {
_childTraversal := traverseTraceAndAddRequiredMetadata(child, uncollapsedSpans, level+1, isPartOfPreorder && contains(uncollapsedSpans, span.SpanID), index != (len(span.Children)-1), selectedSpanId)
preOrderTraversal = append(preOrderTraversal, _childTraversal...)
nodeWithoutChildren.SubTreeNodeCount += child.SubTreeNodeCount + 1
span.SubTreeNodeCount += child.SubTreeNodeCount + 1
}
return preOrderTraversal
}
func bfsTraversalForTrace(span *model.FlamegraphSpan, level int64, bfsMap *map[int64][]*model.FlamegraphSpan) {
ok, exists := (*bfsMap)[level]
span.Level = level
if exists {
(*bfsMap)[level] = append(ok, span)
} else {
(*bfsMap)[level] = []*model.FlamegraphSpan{span}
}
for _, child := range span.Children {
bfsTraversalForTrace(child, level+1, bfsMap)
}
span.Children = make([]*model.FlamegraphSpan, 0)
}
func findIndexForSelectedSpan(spans [][]*model.FlamegraphSpan, selectedSpanId string) int64 {
var selectedSpanLevel int64 = 0
for index, _spans := range spans {
if len(_spans) > 0 && _spans[0].SpanID == selectedSpanId {
selectedSpanLevel = int64(index)
break
}
}
return selectedSpanLevel
}
func findIndexForSelectedSpanFromPreOrder(spans []*model.Span, selectedSpanId string) int {
var selectedSpanIndex = -1
for index, span := range spans {
if span.SpanID == selectedSpanId {
selectedSpanIndex = index
break
}
}
return selectedSpanIndex
}

View File

@@ -546,6 +546,8 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
router.HandleFunc("/api/v2/traces/fields", am.ViewAccess(aH.traceFields)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/traces/fields", am.EditAccess(aH.updateTraceField)).Methods(http.MethodPost)
router.HandleFunc("/api/v2/traces/flamegraph/{traceId}", am.ViewAccess(aH.GetFlamegraphSpansForTrace)).Methods(http.MethodPost)
router.HandleFunc("/api/v2/traces/{traceId}", am.ViewAccess(aH.GetWaterfallSpansForTraceWithMetadata)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/version", am.OpenAccess(aH.getVersion)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
@@ -1777,6 +1779,52 @@ func (aH *APIHandler) SearchTraces(w http.ResponseWriter, r *http.Request) {
}
func (aH *APIHandler) GetWaterfallSpansForTraceWithMetadata(w http.ResponseWriter, r *http.Request) {
traceID := mux.Vars(r)["traceId"]
if traceID == "" {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: errors.New("traceID is required")}, nil)
return
}
req := new(model.GetWaterfallSpansForTraceWithMetadataParams)
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
result, apiErr := aH.reader.GetWaterfallSpansForTraceWithMetadata(r.Context(), traceID, req)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
aH.WriteJSON(w, r, result)
}
func (aH *APIHandler) GetFlamegraphSpansForTrace(w http.ResponseWriter, r *http.Request) {
traceID := mux.Vars(r)["traceId"]
if traceID == "" {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: errors.New("traceID is required")}, nil)
return
}
req := new(model.GetFlamegraphSpansForTraceParams)
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
result, apiErr := aH.reader.GetFlamegraphSpansForTrace(r.Context(), traceID, req)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
aH.WriteJSON(w, r, result)
}
func (aH *APIHandler) listErrors(w http.ResponseWriter, r *http.Request) {
query, err := parseListErrorsRequest(r)

View File

@@ -1384,6 +1384,8 @@ func Test_querier_runWindowBasedListQuery(t *testing.T) {
"",
true,
true,
time.Duration(time.Second),
nil,
)
q := &querier{

View File

@@ -1438,6 +1438,8 @@ func Test_querier_runWindowBasedListQuery(t *testing.T) {
"",
true,
true,
time.Duration(time.Second),
nil,
)
q := &querier{

View File

@@ -122,6 +122,20 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
readerReady := make(chan bool)
var c cache.Cache
if serverOptions.CacheConfigPath != "" {
cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath)
if err != nil {
return nil, err
}
c = cache.NewCache(cacheOpts)
}
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
if err != nil {
return nil, err
}
var reader interfaces.Reader
storage := os.Getenv("STORAGE")
if storage == "clickhouse" {
@@ -136,6 +150,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
fluxInterval,
nil,
)
go clickhouseReader.Start(readerReady)
reader = clickhouseReader
@@ -150,14 +166,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
}
var c cache.Cache
if serverOptions.CacheConfigPath != "" {
cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath)
if err != nil {
return nil, err
}
c = cache.NewCache(cacheOpts)
}
<-readerReady
rm, err := makeRulesManager(
@@ -168,11 +176,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
if err != nil {
return nil, err
}
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, fmt.Errorf("couldn't create integrations controller: %w", err)

View File

@@ -41,6 +41,8 @@ type Reader interface {
// Search Interfaces
SearchTraces(ctx context.Context, params *model.SearchTracesParams, smartTraceAlgorithm func(payload []model.SearchSpanResponseItem, targetSpanId string, levelUp int, levelDown int, spanLimit int) ([]model.SearchSpansResult, error)) (*[]model.SearchSpansResult, error)
GetWaterfallSpansForTraceWithMetadata(ctx context.Context, traceID string, req *model.GetWaterfallSpansForTraceWithMetadataParams) (*model.GetWaterfallSpansForTraceWithMetadataResponse, *model.ApiError)
GetFlamegraphSpansForTrace(ctx context.Context, traceID string, req *model.GetFlamegraphSpansForTraceParams) (*model.GetFlamegraphSpansForTraceResponse, *model.ApiError)
// Setter Interfaces
SetTTL(ctx context.Context, ttlParams *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError)

View File

@@ -315,6 +315,16 @@ type SearchTracesParams struct {
MaxSpansInTrace int `json:"maxSpansInTrace"`
}
type GetWaterfallSpansForTraceWithMetadataParams struct {
SelectedSpanID string `json:"selectedSpanId"`
IsSelectedSpanIDUnCollapsed bool `json:"isSelectedSpanIDUnCollapsed"`
UncollapsedSpans []string `json:"uncollapsedSpans"`
}
type GetFlamegraphSpansForTraceParams struct {
SelectedSpanID string `json:"selectedSpanId"`
}
type SpanFilterParams struct {
TraceID []string `json:"traceID"`
Status []string `json:"status"`

View File

@@ -269,6 +269,102 @@ type SearchSpanResponseItem struct {
SpanKind string `json:"spanKind"`
}
type Span struct {
TimeUnixNano uint64 `json:"timestamp"`
DurationNano uint64 `json:"durationNano"`
SpanID string `json:"spanId"`
RootSpanID string `json:"rootSpanId"`
ParentSpanId string `json:"parentSpanId"`
TraceID string `json:"traceId"`
HasError bool `json:"hasError"`
Kind int32 `json:"kind"`
ServiceName string `json:"serviceName"`
Name string `json:"name"`
References []OtelSpanRef `json:"references,omitempty"`
TagMap map[string]string `json:"tagMap"`
Events []string `json:"event"`
RootName string `json:"rootName"`
StatusMessage string `json:"statusMessage"`
StatusCodeString string `json:"statusCodeString"`
SpanKind string `json:"spanKind"`
Children []*Span `json:"children"`
// the below two fields are for frontend to render the spans
SubTreeNodeCount uint64 `json:"subTreeNodeCount"`
HasChildren bool `json:"hasChildren"`
HasSiblings bool `json:"hasSiblings"`
Level uint64 `json:"level"`
}
type FlamegraphSpan struct {
TimeUnixNano uint64 `json:"timestamp"`
DurationNano uint64 `json:"durationNano"`
SpanID string `json:"spanId"`
ParentSpanId string `json:"parentSpanId"`
TraceID string `json:"traceId"`
HasError bool `json:"hasError"`
ServiceName string `json:"serviceName"`
Name string `json:"name"`
Level int64 `json:"level"`
References []OtelSpanRef `json:"references,omitempty"`
Children []*FlamegraphSpan `json:"children"`
}
type GetWaterfallSpansForTraceWithMetadataCache struct {
StartTime uint64 `json:"startTime"`
EndTime uint64 `json:"endTime"`
DurationNano uint64 `json:"durationNano"`
TotalSpans uint64 `json:"totalSpans"`
TotalErrorSpans uint64 `json:"totalErrorSpans"`
ServiceNameToTotalDurationMap map[string]uint64 `json:"serviceNameToTotalDurationMap"`
SpanIdToSpanNodeMap map[string]*Span `json:"spanIdToSpanNodeMap"`
TraceRoots []*Span `json:"traceRoots"`
}
func (c *GetWaterfallSpansForTraceWithMetadataCache) MarshalBinary() (data []byte, err error) {
return json.Marshal(c)
}
func (c *GetWaterfallSpansForTraceWithMetadataCache) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, c)
}
type GetFlamegraphSpansForTraceCache struct {
StartTime uint64 `json:"startTime"`
EndTime uint64 `json:"endTime"`
DurationNano uint64 `json:"durationNano"`
SelectedSpans [][]*FlamegraphSpan `json:"selectedSpans"`
TraceRoots []*FlamegraphSpan `json:"traceRoots"`
}
func (c *GetFlamegraphSpansForTraceCache) MarshalBinary() (data []byte, err error) {
return json.Marshal(c)
}
func (c *GetFlamegraphSpansForTraceCache) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, c)
}
type GetWaterfallSpansForTraceWithMetadataResponse struct {
StartTimestampMillis uint64 `json:"startTimestampMillis"`
EndTimestampMillis uint64 `json:"endTimestampMillis"`
DurationNano uint64 `json:"durationNano"`
RootServiceName string `json:"rootServiceName"`
RootServiceEntryPoint string `json:"rootServiceEntryPoint"`
TotalSpansCount uint64 `json:"totalSpansCount"`
TotalErrorSpansCount uint64 `json:"totalErrorSpansCount"`
ServiceNameToTotalDurationMap map[string]uint64 `json:"serviceNameToTotalDurationMap"`
Spans []*Span `json:"spans"`
HasMissingSpans bool `json:"hasMissingSpans"`
// this is needed for frontend and query service sync
UncollapsedSpans []string `json:"uncollapsedSpans"`
}
type GetFlamegraphSpansForTraceResponse struct {
StartTimestampMillis uint64 `json:"startTimestampMillis"`
EndTimestampMillis uint64 `json:"endTimestampMillis"`
DurationNano uint64 `json:"durationNano"`
Spans [][]*FlamegraphSpan `json:"spans"`
}
type OtelSpanRef struct {
TraceId string `json:"traceId,omitempty"`
SpanId string `json:"spanId,omitempty"`

View File

@@ -20,6 +20,7 @@ type SpanItemV2 struct {
StatusMessage string `ch:"status_message"`
StatusCodeString string `ch:"status_code_string"`
SpanKind string `ch:"kind_string"`
ParentSpanId string `ch:"parent_span_id"`
}
type TraceSummary struct {

View File

@@ -1241,7 +1241,7 @@ func TestThresholdRuleUnitCombinations(t *testing.T) {
}
options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace")
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true)
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil)
rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true)
rule.TemporalityMap = map[string]map[v3.Temporality]bool{
@@ -1340,7 +1340,7 @@ func TestThresholdRuleNoData(t *testing.T) {
}
options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace")
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true)
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil)
rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true)
rule.TemporalityMap = map[string]map[v3.Temporality]bool{
@@ -1448,7 +1448,7 @@ func TestThresholdRuleTracesLink(t *testing.T) {
}
options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace")
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true)
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil)
rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true)
rule.TemporalityMap = map[string]map[v3.Temporality]bool{
@@ -1573,7 +1573,7 @@ func TestThresholdRuleLogsLink(t *testing.T) {
}
options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace")
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true)
reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil)
rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true)
rule.TemporalityMap = map[string]map[v3.Temporality]bool{

View File

@@ -47,6 +47,8 @@ func NewMockClickhouseReader(
"",
true,
true,
time.Duration(time.Second),
nil,
)
return reader, mockDB