feat: improve service map (#1467)

* feat: improve service map
This commit is contained in:
Srikanth Chekuri 2022-08-04 12:38:53 +05:30 committed by GitHub
parent 5bfc2af51b
commit 3968f11b3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 101 additions and 109 deletions

View File

@ -42,8 +42,9 @@ export interface Option {
} }
export const ServiceMapOptions: Option[] = [ export const ServiceMapOptions: Option[] = [
{ value: '1min', label: 'Last 1 min' },
{ value: '5min', label: 'Last 5 min' }, { value: '5min', label: 'Last 5 min' },
{ value: '15min', label: 'Last 15 min' },
{ value: '30min', label: 'Last 30 min' },
]; ];
export const getDefaultOption = (route: string): Time => { export const getDefaultOption = (route: string): Time => {

View File

@ -45,6 +45,9 @@ interface graphLink {
source: string; source: string;
target: string; target: string;
value: number; value: number;
callRate: number;
errorRate: number;
p99: number;
} }
export interface graphDataType { export interface graphDataType {
nodes: graphNode[]; nodes: graphNode[];
@ -96,16 +99,16 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
const graphData = { nodes, links }; const graphData = { nodes, links };
return ( return (
<Container> <Container>
<SelectService {/* <SelectService
services={serviceMap.services} services={serviceMap.items}
zoomToService={zoomToService} zoomToService={zoomToService}
zoomToDefault={zoomToDefault} zoomToDefault={zoomToDefault}
/> /> */}
<ForceGraph2D <ForceGraph2D
ref={fgRef} ref={fgRef}
cooldownTicks={100} cooldownTicks={100}
graphData={graphData} graphData={graphData}
nodeLabel={getTooltip} linkLabel={getTooltip}
linkAutoColorBy={(d) => d.target} linkAutoColorBy={(d) => d.target}
linkDirectionalParticles="value" linkDirectionalParticles="value"
linkDirectionalParticleSpeed={(d) => d.value} linkDirectionalParticleSpeed={(d) => d.value}
@ -124,7 +127,7 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
ctx.fillStyle = isDarkMode ? '#ffffff' : '#000000'; ctx.fillStyle = isDarkMode ? '#ffffff' : '#000000';
ctx.fillText(label, node.x, node.y); ctx.fillText(label, node.x, node.y);
}} }}
onNodeClick={(node) => { onLinkHover={(node) => {
const tooltip = document.querySelector('.graph-tooltip'); const tooltip = document.querySelector('.graph-tooltip');
if (tooltip && node) { if (tooltip && node) {
tooltip.innerHTML = getTooltip(node); tooltip.innerHTML = getTooltip(node);

View File

@ -1,12 +1,13 @@
/*eslint-disable*/ /*eslint-disable*/
//@ts-nocheck //@ts-nocheck
import { cloneDeep, find, maxBy, uniq, uniqBy } from 'lodash-es'; import { cloneDeep, find, maxBy, uniq, uniqBy, groupBy, sumBy } from 'lodash-es';
import { graphDataType } from './ServiceMap'; import { graphDataType } from './ServiceMap';
const MIN_WIDTH = 10; const MIN_WIDTH = 10;
const MAX_WIDTH = 20; const MAX_WIDTH = 20;
const DEFAULT_FONT_SIZE = 6; const DEFAULT_FONT_SIZE = 6;
export const getDimensions = (num, highest) => { export const getDimensions = (num, highest) => {
const percentage = (num / highest) * 100; const percentage = (num / highest) * 100;
const width = (percentage * (MAX_WIDTH - MIN_WIDTH)) / 100 + MIN_WIDTH; const width = (percentage * (MAX_WIDTH - MIN_WIDTH)) / 100 + MIN_WIDTH;
@ -18,19 +19,30 @@ export const getDimensions = (num, highest) => {
}; };
export const getGraphData = (serviceMap, isDarkMode): graphDataType => { export const getGraphData = (serviceMap, isDarkMode): graphDataType => {
const { items, services } = serviceMap; const { items } = serviceMap;
const services = Object.values(groupBy(items, 'child')).map((e) => {
return {
serviceName: e[0].child,
errorRate: sumBy(e, 'errorRate'),
callRate: sumBy(e, 'callRate'),
}
});
const highestCallCount = maxBy(items, (e) => e?.callCount)?.callCount; const highestCallCount = maxBy(items, (e) => e?.callCount)?.callCount;
const highestCallRate = maxBy(services, (e) => e?.callRate)?.callRate; const highestCallRate = maxBy(services, (e) => e?.callRate)?.callRate;
const divNum = Number( const divNum = Number(
String(1).padEnd(highestCallCount.toString().length, '0'), String(1).padEnd(highestCallCount.toString().length, '0'),
); );
const links = cloneDeep(items).map((node) => { const links = cloneDeep(items).map((node) => {
const { parent, child, callCount } = node; const { parent, child, callCount, callRate, errorRate, p99 } = node;
return { return {
source: parent, source: parent,
target: child, target: child,
value: (100 - callCount / divNum) * 0.03, value: (100 - callCount / divNum) * 0.03,
callRate,
errorRate,
p99,
}; };
}); });
const uniqParent = uniqBy(cloneDeep(items), 'parent').map((e) => e.parent); const uniqParent = uniqBy(cloneDeep(items), 'parent').map((e) => e.parent);
@ -47,15 +59,10 @@ export const getGraphData = (serviceMap, isDarkMode): graphDataType => {
width: MIN_WIDTH, width: MIN_WIDTH,
color, color,
nodeVal: MIN_WIDTH, nodeVal: MIN_WIDTH,
callRate: 0,
errorRate: 0,
p99: 0,
}; };
} }
if (service.errorRate > 0) { if (service.errorRate > 0) {
color = isDarkMode ? '#DB836E' : '#F98989'; color = isDarkMode ? '#DB836E' : '#F98989';
} else if (service.fourXXRate > 0) {
color = isDarkMode ? '#C79931' : '#F9DA7B';
} }
const { fontSize, width } = getDimensions(service.callRate, highestCallRate); const { fontSize, width } = getDimensions(service.callRate, highestCallRate);
return { return {
@ -65,9 +72,6 @@ export const getGraphData = (serviceMap, isDarkMode): graphDataType => {
width, width,
color, color,
nodeVal: width, nodeVal: width,
callRate: service.callRate.toFixed(2),
errorRate: service.errorRate,
p99: service.p99,
}; };
}); });
return { return {
@ -90,25 +94,31 @@ export const getZoomPx = (): number => {
return 190; return 190;
}; };
export const getTooltip = (node: { const getRound2DigitsAfterDecimal = (num: number) => {
if (num === 0) {
return 0;
}
return num.toFixed(20).match(/^-?\d*\.?0*\d{0,2}/)[0];
}
export const getTooltip = (link: {
p99: number; p99: number;
errorRate: number; errorRate: number;
callRate: number; callRate: number;
id: string; id: string;
}) => { }) => {
return `<div style="color:#333333;padding:12px;background: white;border-radius: 2px;"> return `<div style="color:#333333;padding:12px;background: white;border-radius: 2px;">
<div style="font-weight:bold; margin-bottom:16px;">${node.id}</div>
<div class="keyval"> <div class="keyval">
<div class="key">P99 latency:</div> <div class="key">P99 latency:</div>
<div class="val">${node.p99 / 1000000}ms</div> <div class="val">${getRound2DigitsAfterDecimal(link.p99/ 1000000)}ms</div>
</div> </div>
<div class="keyval"> <div class="keyval">
<div class="key">Request:</div> <div class="key">Request:</div>
<div class="val">${node.callRate}/sec</div> <div class="val">${getRound2DigitsAfterDecimal(link.callRate)}/sec</div>
</div> </div>
<div class="keyval"> <div class="keyval">
<div class="key">Error Rate:</div> <div class="key">Error Rate:</div>
<div class="val">${node.errorRate}%</div> <div class="val">${getRound2DigitsAfterDecimal(link.errorRate)}%</div>
</div> </div>
</div>`; </div>`;
}; };

View File

@ -6,26 +6,16 @@ import { ActionTypes } from './types';
export interface ServiceMapStore { export interface ServiceMapStore {
items: ServicesMapItem[]; items: ServicesMapItem[];
services: ServicesItem[];
loading: boolean; loading: boolean;
} }
export interface ServicesItem {
serviceName: string;
p99: number;
avgDuration: number;
numCalls: number;
callRate: number;
numErrors: number;
errorRate: number;
num4XX: number;
fourXXRate: number;
}
export interface ServicesMapItem { export interface ServicesMapItem {
parent: string; parent: string;
child: string; child: string;
callCount: number; callCount: number;
callRate: number;
errorRate: number;
p99: number;
} }
export interface ServiceMapItemAction { export interface ServiceMapItemAction {
@ -33,11 +23,6 @@ export interface ServiceMapItemAction {
payload: ServicesMapItem[]; payload: ServicesMapItem[];
} }
export interface ServicesAction {
type: ActionTypes.getServices;
payload: ServicesItem[];
}
export interface ServiceMapLoading { export interface ServiceMapLoading {
type: ActionTypes.serviceMapLoading; type: ActionTypes.serviceMapLoading;
payload: { payload: {
@ -55,19 +40,13 @@ export const getDetailedServiceMapItems = (globalTime: GlobalTime) => {
end, end,
tags: [], tags: [],
}; };
const [serviceMapDependenciesResponse, response] = await Promise.all([ const [dependencyGraphResponse] = await Promise.all([
api.post<ServicesMapItem[]>(`/serviceMapDependencies`, serviceMapPayload), api.post<ServicesMapItem[]>(`/dependency_graph`, serviceMapPayload),
api.post<ServicesItem[]>(`/services`, serviceMapPayload),
]); ]);
dispatch<ServicesAction>({
type: ActionTypes.getServices,
payload: response.data,
});
dispatch<ServiceMapItemAction>({ dispatch<ServiceMapItemAction>({
type: ActionTypes.getServiceMapItems, type: ActionTypes.getServiceMapItems,
payload: serviceMapDependenciesResponse.data, payload: dependencyGraphResponse.data,
}); });
dispatch<ServiceMapLoading>({ dispatch<ServiceMapLoading>({

View File

@ -1,8 +1,4 @@
import { import { ServiceMapItemAction, ServiceMapLoading } from './serviceMap';
ServiceMapItemAction,
ServiceMapLoading,
ServicesAction,
} from './serviceMap';
import { GetUsageDataAction } from './usage'; import { GetUsageDataAction } from './usage';
export enum ActionTypes { export enum ActionTypes {
@ -17,6 +13,5 @@ export enum ActionTypes {
export type Action = export type Action =
| GetUsageDataAction | GetUsageDataAction
| ServicesAction
| ServiceMapItemAction | ServiceMapItemAction
| ServiceMapLoading; | ServiceMapLoading;

View File

@ -2,7 +2,6 @@ import { Action, ActionTypes, ServiceMapStore } from 'store/actions';
const initialState: ServiceMapStore = { const initialState: ServiceMapStore = {
items: [], items: [],
services: [],
loading: true, loading: true,
}; };
@ -16,11 +15,6 @@ export const ServiceMapReducer = (
...state, ...state,
items: action.payload, items: action.payload,
}; };
case ActionTypes.getServices:
return {
...state,
services: action.payload,
};
case ActionTypes.serviceMapLoading: { case ActionTypes.serviceMapLoading: {
return { return {
...state, ...state,

View File

@ -25,6 +25,7 @@ const (
defaultErrorTable string = "signoz_error_index_v2" defaultErrorTable string = "signoz_error_index_v2"
defaultDurationTable string = "durationSortMV" defaultDurationTable string = "durationSortMV"
defaultSpansTable string = "signoz_spans" defaultSpansTable string = "signoz_spans"
defaultDependencyGraphTable string = "dependency_graph_minutes"
defaultTopLevelOperationsTable string = "top_level_operations" defaultTopLevelOperationsTable string = "top_level_operations"
defaultWriteBatchDelay time.Duration = 5 * time.Second defaultWriteBatchDelay time.Duration = 5 * time.Second
defaultWriteBatchSize int = 10000 defaultWriteBatchSize int = 10000
@ -53,6 +54,7 @@ type namespaceConfig struct {
DurationTable string DurationTable string
SpansTable string SpansTable string
ErrorTable string ErrorTable string
DependencyGraphTable string
TopLevelOperationsTable string TopLevelOperationsTable string
WriteBatchDelay time.Duration WriteBatchDelay time.Duration
WriteBatchSize int WriteBatchSize int
@ -113,6 +115,7 @@ func NewOptions(datasource string, primaryNamespace string, otherNamespaces ...s
ErrorTable: defaultErrorTable, ErrorTable: defaultErrorTable,
DurationTable: defaultDurationTable, DurationTable: defaultDurationTable,
SpansTable: defaultSpansTable, SpansTable: defaultSpansTable,
DependencyGraphTable: defaultDependencyGraphTable,
TopLevelOperationsTable: defaultTopLevelOperationsTable, TopLevelOperationsTable: defaultTopLevelOperationsTable,
WriteBatchDelay: defaultWriteBatchDelay, WriteBatchDelay: defaultWriteBatchDelay,
WriteBatchSize: defaultWriteBatchSize, WriteBatchSize: defaultWriteBatchSize,

View File

@ -83,6 +83,7 @@ type ClickHouseReader struct {
indexTable string indexTable string
errorTable string errorTable string
spansTable string spansTable string
dependencyGraphTable string
topLevelOperationsTable string topLevelOperationsTable string
queryEngine *promql.Engine queryEngine *promql.Engine
remoteStorage *remote.Storage remoteStorage *remote.Storage
@ -121,6 +122,7 @@ func NewReader(localDB *sqlx.DB, configFile string) *ClickHouseReader {
errorTable: options.primary.ErrorTable, errorTable: options.primary.ErrorTable,
durationTable: options.primary.DurationTable, durationTable: options.primary.DurationTable,
spansTable: options.primary.SpansTable, spansTable: options.primary.SpansTable,
dependencyGraphTable: options.primary.DependencyGraphTable,
topLevelOperationsTable: options.primary.TopLevelOperationsTable, topLevelOperationsTable: options.primary.TopLevelOperationsTable,
promConfigFile: configFile, promConfigFile: configFile,
} }
@ -1698,48 +1700,50 @@ func interfaceArrayToStringArray(array []interface{}) []string {
return strArray return strArray
} }
func (r *ClickHouseReader) GetServiceMapDependencies(ctx context.Context, queryParams *model.GetServicesParams) (*[]model.ServiceMapDependencyResponseItem, error) { func (r *ClickHouseReader) GetDependencyGraph(ctx context.Context, queryParams *model.GetServicesParams) (*[]model.ServiceMapDependencyResponseItem, error) {
serviceMapDependencyItems := []model.ServiceMapDependencyItem{}
query := fmt.Sprintf(`SELECT spanID, parentSpanID, serviceName FROM %s.%s WHERE timestamp>='%s' AND timestamp<='%s'`, r.traceDB, r.indexTable, strconv.FormatInt(queryParams.Start.UnixNano(), 10), strconv.FormatInt(queryParams.End.UnixNano(), 10)) response := []model.ServiceMapDependencyResponseItem{}
err := r.db.Select(ctx, &serviceMapDependencyItems, query) args := []interface{}{}
args = append(args,
clickhouse.Named("start", uint64(queryParams.Start.Unix())),
clickhouse.Named("end", uint64(queryParams.End.Unix())),
clickhouse.Named("duration", uint64(queryParams.End.Unix()-queryParams.Start.Unix())),
)
zap.S().Info(query) query := fmt.Sprintf(`
WITH
quantilesMergeState(0.5, 0.75, 0.9, 0.95, 0.99)(duration_quantiles_state) AS duration_quantiles_state,
finalizeAggregation(duration_quantiles_state) AS result
SELECT
src as parent,
dest as child,
result[1] AS p50,
result[2] AS p75,
result[3] AS p90,
result[4] AS p95,
result[5] AS p99,
sum(total_count) as callCount,
sum(total_count)/ @duration AS callRate,
sum(error_count)/sum(total_count) as errorRate
FROM %s.%s
WHERE toUInt64(toDateTime(timestamp)) >= @start AND toUInt64(toDateTime(timestamp)) <= @end
GROUP BY
src,
dest`,
r.traceDB, r.dependencyGraphTable,
)
zap.S().Debug(query, args)
err := r.db.Select(ctx, &response, query, args...)
if err != nil { if err != nil {
zap.S().Debug("Error in processing sql query: ", err) zap.S().Error("Error in processing sql query: ", err)
return nil, fmt.Errorf("Error in processing sql query") return nil, fmt.Errorf("Error in processing sql query")
} }
serviceMap := make(map[string]*model.ServiceMapDependencyResponseItem) return &response, nil
spanId2ServiceNameMap := make(map[string]string)
for i := range serviceMapDependencyItems {
spanId2ServiceNameMap[serviceMapDependencyItems[i].SpanId] = serviceMapDependencyItems[i].ServiceName
}
for i := range serviceMapDependencyItems {
parent2childServiceName := spanId2ServiceNameMap[serviceMapDependencyItems[i].ParentSpanId] + "-" + spanId2ServiceNameMap[serviceMapDependencyItems[i].SpanId]
if _, ok := serviceMap[parent2childServiceName]; !ok {
serviceMap[parent2childServiceName] = &model.ServiceMapDependencyResponseItem{
Parent: spanId2ServiceNameMap[serviceMapDependencyItems[i].ParentSpanId],
Child: spanId2ServiceNameMap[serviceMapDependencyItems[i].SpanId],
CallCount: 1,
}
} else {
serviceMap[parent2childServiceName].CallCount++
}
}
retMe := make([]model.ServiceMapDependencyResponseItem, 0, len(serviceMap))
for _, dependency := range serviceMap {
if dependency.Parent == "" {
continue
}
retMe = append(retMe, *dependency)
}
return &retMe, nil
} }
func (r *ClickHouseReader) GetFilteredSpansAggregates(ctx context.Context, queryParams *model.GetFilteredSpanAggregatesParams) (*model.GetFilteredSpansAggregatesResponse, *model.ApiError) { func (r *ClickHouseReader) GetFilteredSpansAggregates(ctx context.Context, queryParams *model.GetFilteredSpanAggregatesParams) (*model.GetFilteredSpansAggregatesResponse, *model.ApiError) {
@ -1979,7 +1983,7 @@ func (r *ClickHouseReader) SetTTL(ctx context.Context,
switch params.Type { switch params.Type {
case constants.TraceTTL: case constants.TraceTTL:
tableNameArray := []string{signozTraceDBName + "." + signozTraceTableName, signozTraceDBName + "." + signozDurationMVTable, signozTraceDBName + "." + signozSpansTable, signozTraceDBName + "." + signozErrorIndexTable} tableNameArray := []string{signozTraceDBName + "." + signozTraceTableName, signozTraceDBName + "." + signozDurationMVTable, signozTraceDBName + "." + signozSpansTable, signozTraceDBName + "." + signozErrorIndexTable, signozTraceDBName + "." + defaultDependencyGraphTable}
for _, tableName = range tableNameArray { for _, tableName = range tableNameArray {
statusItem, err := r.checkTTLStatusItem(ctx, tableName) statusItem, err := r.checkTTLStatusItem(ctx, tableName)
if err != nil { if err != nil {

View File

@ -326,7 +326,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
router.HandleFunc("/api/v1/service/top_level_operations", ViewAccess(aH.getServicesTopLevelOps)).Methods(http.MethodPost) router.HandleFunc("/api/v1/service/top_level_operations", ViewAccess(aH.getServicesTopLevelOps)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/traces/{traceId}", ViewAccess(aH.searchTraces)).Methods(http.MethodGet) router.HandleFunc("/api/v1/traces/{traceId}", ViewAccess(aH.searchTraces)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/usage", ViewAccess(aH.getUsage)).Methods(http.MethodGet) router.HandleFunc("/api/v1/usage", ViewAccess(aH.getUsage)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/serviceMapDependencies", ViewAccess(aH.serviceMapDependencies)).Methods(http.MethodPost) router.HandleFunc("/api/v1/dependency_graph", ViewAccess(aH.dependencyGraph)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/settings/ttl", AdminAccess(aH.setTTL)).Methods(http.MethodPost) router.HandleFunc("/api/v1/settings/ttl", AdminAccess(aH.setTTL)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/settings/ttl", ViewAccess(aH.getTTL)).Methods(http.MethodGet) router.HandleFunc("/api/v1/settings/ttl", ViewAccess(aH.getTTL)).Methods(http.MethodGet)
@ -1185,14 +1185,14 @@ func (aH *APIHandler) getServices(w http.ResponseWriter, r *http.Request) {
aH.writeJSON(w, r, result) aH.writeJSON(w, r, result)
} }
func (aH *APIHandler) serviceMapDependencies(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) dependencyGraph(w http.ResponseWriter, r *http.Request) {
query, err := parseGetServicesRequest(r) query, err := parseGetServicesRequest(r)
if aH.handleError(w, err, http.StatusBadRequest) { if aH.handleError(w, err, http.StatusBadRequest) {
return return
} }
result, err := (*aH.reader).GetServiceMapDependencies(r.Context(), query) result, err := (*aH.reader).GetDependencyGraph(r.Context(), query)
if aH.handleError(w, err, http.StatusBadRequest) { if aH.handleError(w, err, http.StatusBadRequest) {
return return
} }

View File

@ -25,7 +25,8 @@ type Reader interface {
GetTopOperations(ctx context.Context, query *model.GetTopOperationsParams) (*[]model.TopOperationsItem, *model.ApiError) GetTopOperations(ctx context.Context, query *model.GetTopOperationsParams) (*[]model.TopOperationsItem, *model.ApiError)
GetUsage(ctx context.Context, query *model.GetUsageParams) (*[]model.UsageItem, error) GetUsage(ctx context.Context, query *model.GetUsageParams) (*[]model.UsageItem, error)
GetServicesList(ctx context.Context) (*[]string, error) GetServicesList(ctx context.Context) (*[]string, error)
GetServiceMapDependencies(ctx context.Context, query *model.GetServicesParams) (*[]model.ServiceMapDependencyResponseItem, error) GetDependencyGraph(ctx context.Context, query *model.GetServicesParams) (*[]model.ServiceMapDependencyResponseItem, error)
GetTTL(ctx context.Context, ttlParams *model.GetTTLParams) (*model.GetTTLResponseItem, *model.ApiError) GetTTL(ctx context.Context, ttlParams *model.GetTTLParams) (*model.GetTTLResponseItem, *model.ApiError)
// GetDisks returns a list of disks configured in the underlying DB. It is supported by // GetDisks returns a list of disks configured in the underlying DB. It is supported by

View File

@ -206,12 +206,6 @@ func (item *SearchSpanReponseItem) GetValues() []interface{} {
return returnArray return returnArray
} }
type ServiceMapDependencyItem struct {
SpanId string `json:"spanId,omitempty" ch:"spanID"`
ParentSpanId string `json:"parentSpanId,omitempty" ch:"parentSpanID"`
ServiceName string `json:"serviceName,omitempty" ch:"serviceName"`
}
type UsageItem struct { type UsageItem struct {
Time time.Time `json:"time,omitempty" ch:"time"` Time time.Time `json:"time,omitempty" ch:"time"`
Timestamp uint64 `json:"timestamp" ch:"timestamp"` Timestamp uint64 `json:"timestamp" ch:"timestamp"`
@ -233,10 +227,18 @@ type TagFilters struct {
type TagValues struct { type TagValues struct {
TagValues string `json:"tagValues" ch:"tagValues"` TagValues string `json:"tagValues" ch:"tagValues"`
} }
type ServiceMapDependencyResponseItem struct { type ServiceMapDependencyResponseItem struct {
Parent string `json:"parent,omitempty" ch:"parent"` Parent string `json:"parent" ch:"parent"`
Child string `json:"child,omitempty" ch:"child"` Child string `json:"child" ch:"child"`
CallCount int `json:"callCount,omitempty" ch:"callCount"` CallCount uint64 `json:"callCount" ch:"callCount"`
CallRate float64 `json:"callRate" ch:"callRate"`
ErrorRate float64 `json:"errorRate" ch:"errorRate"`
P99 float64 `json:"p99" ch:"p99"`
P95 float64 `json:"p95" ch:"p95"`
P90 float64 `json:"p90" ch:"p90"`
P75 float64 `json:"p75" ch:"p75"`
P50 float64 `json:"p50" ch:"p50"`
} }
type GetFilteredSpansAggregatesResponse struct { type GetFilteredSpansAggregatesResponse struct {