Inital Commit

This commit is contained in:
Ankit Nayan 2021-01-03 18:15:44 +05:30
parent 940788afb5
commit 1bc2fc038b
162 changed files with 26706 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
frontend/node_modules
frontend/.pnp
*.pnp.js
# testing
frontend/coverage
# production
frontend/build
frontend/.vscode
frontend/.yarnclean
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
frontend/npm-debug.log*
frontend/yarn-debug.log*
frontend/yarn-error.log*
**/.vscode
*.tgz

0
CHANGELOG.md Normal file
View File

0
CONTRIBUTING.md Normal file
View File

0
LICENSE Normal file
View File

0
deploy/docker/README.md Normal file
View File

View File

@ -0,0 +1,61 @@
version: "3"
services:
# Zookeeper
zookeeper:
image: 'bitnami/zookeeper:3'
ports:
- '2181:2181'
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
# Kafka
kafka:
image: 'bitnami/kafka:2'
ports:
- '9092:9092'
environment:
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
- ALLOW_PLAINTEXT_LISTENER=yes
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
- KAFKA_CFG_LISTENERS=PLAINTEXT://:29092,PLAINTEXT_HOST://:9092
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
depends_on:
- zookeeper
# Divolte container
divolte:
image: divolte/divolte-collector
container_name: divolte
environment:
- DIVOLTE_KAFKA_BROKER_LIST=kafka:29092
volumes:
- ./conf/divolte/:/opt/divolte/divolte-collector/conf/
ports:
- 8290:8290
depends_on:
- kafka
# Druid container
druid:
image: fokkodriesprong/docker-druid
container_name: druid
ports:
- 8081:8081
- 8082:8082
- 8888:8888
depends_on:
- kafka
# Superset container
superset:
image: amancevice/superset:0.18.5
container_name: superset
ports:
- 8088:8088
# Superset container
app:
build: app/
container_name: app
ports:
- 8090:8090

View File

View File

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: retention-config
data:
retention-spec.json: |
[{"period":"P3D","includeFuture":true,"tieredReplicants":{"_default_tier":1},"type":"loadByPeriod"},{"type":"dropForever"}]

View File

@ -0,0 +1,29 @@
apiVersion: batch/v1
kind: Job
metadata:
name: set-retention
annotations:
"helm.sh/hook": post-install,post-upgrade
spec:
# ttlSecondsAfterFinished: 100
template:
spec:
containers:
- name: set-retention
image: theithollow/hollowapp-blog:curl
volumeMounts:
- name: retention-config-volume
mountPath: /app/retention-spec.json
subPath: retention-spec.json
args:
- /bin/sh
- -c
- "curl -X POST -H 'Content-Type: application/json' -d @/app/retention-spec.json http://signoz-druid-router:8888/druid/coordinator/v1/rules/flattened_spans"
volumes:
- name: retention-config-volume
configMap:
name: retention-config
restartPolicy: Never
backoffLimit: 4

View File

@ -0,0 +1,59 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: supervisor-config
data:
supervisor-spec.json: |
{
"type": "kafka",
"dataSchema": {
"dataSource": "flattened_spans",
"parser": {
"type": "string",
"parseSpec": {
"format": "json",
"timestampSpec": {
"column": "StartTimeUnixNano",
"format": "nano"
},
"dimensionsSpec": {
"dimensions": [
"TraceId",
"SpanId",
"ParentSpanId",
"Name",
"ServiceName",
"References",
"Tags",
"TagsKeys",
"TagsValues",
{ "name": "DurationNano", "type": "Long" },
{ "name": "Kind", "type": "int" }
]
}
}
},
"metricsSpec" : [
{ "type": "quantilesDoublesSketch", "name": "QuantileDuration", "fieldName": "DurationNano" }
],
"granularitySpec": {
"type": "uniform",
"segmentGranularity": "DAY",
"queryGranularity": "NONE",
"rollup": false
}
},
"tuningConfig": {
"type": "kafka",
"reportParseExceptions": true
},
"ioConfig": {
"topic": "flattened_spans",
"replicas": 1,
"taskDuration": "PT20M",
"completionTimeout": "PT30M",
"consumerProperties": {
"bootstrap.servers": "signoz-kafka:9092"
}
}
}

View File

@ -0,0 +1,27 @@
apiVersion: batch/v1
kind: Job
metadata:
name: create-supervisor
annotations:
"helm.sh/hook": post-install,post-upgrade
spec:
# ttlSecondsAfterFinished: 100
template:
spec:
containers:
- name: create-supervisor
image: theithollow/hollowapp-blog:curl
volumeMounts:
- name: supervisor-config-volume
mountPath: /app/supervisor-spec.json
subPath: supervisor-spec.json
args:
- /bin/sh
- -c
- "curl -X POST -H 'Content-Type: application/json' -d @/app/supervisor-spec.json http://signoz-druid-router:8888/druid/indexer/v1/supervisor"
volumes:
- name: supervisor-config-volume
configMap:
name: supervisor-config
restartPolicy: Never
backoffLimit: 4

View File

@ -0,0 +1,43 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-conf
labels:
app: opentelemetry
component: otel-collector-conf
data:
otel-collector-config: |
receivers:
jaeger:
protocols:
grpc:
thrift_http:
processors:
batch:
memory_limiter:
# Same as --mem-ballast-size-mib CLI argument
ballast_size_mib: 683
# 80% of maximum memory up to 2G
limit_mib: 1500
# 25% of limit up to 2G
spike_limit_mib: 512
check_interval: 5s
queued_retry:
num_workers: 4
queue_size: 100
retry_on_failure: true
extensions:
health_check: {}
zpages: {}
exporters:
kafka:
brokers:
- signoz-kafka:9092
protocol_version: 2.0.0
service:
extensions: [health_check, zpages]
pipelines:
traces:
receivers: [jaeger]
processors: [memory_limiter, batch, queued_retry]
exporters: [kafka]

View File

@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector
labels:
app: opentelemetry
component: otel-collector
spec:
selector:
matchLabels:
app: opentelemetry
component: otel-collector
minReadySeconds: 5
progressDeadlineSeconds: 120
replicas: 1 #TODO - adjust this to your own requirements
template:
metadata:
labels:
app: opentelemetry
component: otel-collector
spec:
containers:
- command:
- "/otelcol"
- "--config=/conf/otel-collector-config.yaml"
# Memory Ballast size should be max 1/3 to 1/2 of memory.
- "--mem-ballast-size-mib=683"
image: otel/opentelemetry-collector-dev:latest
name: otel-collector
resources:
limits:
cpu: 1
memory: 2Gi
requests:
cpu: 200m
memory: 400Mi
ports:
- containerPort: 55679 # Default endpoint for ZPages.
- containerPort: 55680 # Default endpoint for OpenTelemetry receiver.
- containerPort: 14250 # Default endpoint for Jaeger HTTP receiver.
- containerPort: 14268 # Default endpoint for Jaeger HTTP receiver.
- containerPort: 9411 # Default endpoint for Zipkin receiver.
- containerPort: 8888 # Default endpoint for querying metrics.
volumeMounts:
- name: otel-collector-config-vol
mountPath: /conf
# - name: otel-collector-secrets
# mountPath: /secrets
livenessProbe:
httpGet:
path: /
port: 13133 # Health Check extension default port.
readinessProbe:
httpGet:
path: /
port: 13133 # Health Check extension default port.
volumes:
- configMap:
name: otel-collector-conf
items:
- key: otel-collector-config
path: otel-collector-config.yaml
name: otel-collector-config-vol
# - secret:
# name: otel-collector-secrets
# items:
# - key: cert.pem
# path: cert.pem
# - key: key.pem
# path: key.pem

View File

@ -0,0 +1,23 @@
apiVersion: v1
kind: Service
metadata:
name: otel-collector
labels:
app: opentelemetry
component: otel-collector
spec:
ports:
- name: otlp # Default endpoint for OpenTelemetry receiver.
port: 55680
protocol: TCP
targetPort: 55680
- name: jaeger-grpc # Default endpoing for Jaeger gRPC receiver
port: 14250
- name: jaeger-thrift-http # Default endpoint for Jaeger HTTP receiver.
port: 14268
- name: zipkin # Default endpoint for Zipkin receiver.
port: 9411
- name: metrics # Default endpoint for querying metrics.
port: 8888
selector:
component: otel-collector

View File

@ -0,0 +1,21 @@
dependencies:
- name: zookeeper
repository: https://charts.bitnami.com/bitnami
version: 6.0.0
- name: kafka
repository: https://charts.bitnami.com/bitnami
version: 12.0.0
- name: druid
repository: https://charts.helm.sh/incubator
version: 0.2.18
- name: flattener-processor
repository: file://./signoz-charts/flattener-processor
version: 0.1.1
- name: query-service
repository: file://./signoz-charts/query-service
version: 0.1.1
- name: frontend
repository: file://./signoz-charts/frontend
version: 0.1.4
digest: sha256:f29f2d0a5e58617075b0e1fd513bf7699138e20dfb94f1cddcfc8ff6a9bcb371
generated: "2021-01-02T01:14:15.321064+05:30"

View File

@ -0,0 +1,43 @@
apiVersion: v2
name: signoz-platform
description: SigNoz Observability Platform Helm Chart
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.6
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 0.1.1
dependencies:
- name: zookeeper
repository: "https://charts.bitnami.com/bitnami"
version: 6.0.0
- name: kafka
repository: "https://charts.bitnami.com/bitnami"
version: 12.0.0
- name: druid
repository: "https://charts.helm.sh/incubator"
version: 0.2.18
- name: flattener-processor
repository: "file://./signoz-charts/flattener-processor"
version: 0.1.1
- name: query-service
repository: "file://./signoz-charts/query-service"
version: 0.1.1
- name: frontend
repository: "file://./signoz-charts/frontend"
version: 0.1.4

View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@ -0,0 +1,21 @@
apiVersion: v2
name: flattener-processor
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.1.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application.
appVersion: v0.1.0

View File

@ -0,0 +1,21 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "flattener-processor.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "flattener-processor.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "flattener-processor.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "flattener-processor.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80
{{- end }}

View File

@ -0,0 +1,63 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "flattener-processor.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "flattener-processor.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "flattener-processor.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "flattener-processor.labels" -}}
helm.sh/chart: {{ include "flattener-processor.chart" . }}
{{ include "flattener-processor.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/*
Selector labels
*/}}
{{- define "flattener-processor.selectorLabels" -}}
app.kubernetes.io/name: {{ include "flattener-processor.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "flattener-processor.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "flattener-processor.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

View File

@ -0,0 +1,65 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "flattener-processor.fullname" . }}
labels:
{{- include "flattener-processor.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "flattener-processor.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "flattener-processor.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "flattener-processor.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command:
- "/root/flattener"
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: KAFKA_BROKER
value: {{ .Values.configVars.KAFKA_BROKER }}
- name: KAFKA_INPUT_TOPIC
value: {{ .Values.configVars.KAFKA_INPUT_TOPIC }}
- name: KAFKA_OUTPUT_TOPIC
value: {{ .Values.configVars.KAFKA_OUTPUT_TOPIC }}
# livenessProbe:
# httpGet:
# path: /
# port: http
# readinessProbe:
# httpGet:
# path: /
# port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "flattener-processor.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "flattener-processor.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ . }}
backend:
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "flattener-processor.fullname" . }}
labels:
{{- include "flattener-processor.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "flattener-processor.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "flattener-processor.serviceAccountName" . }}
labels:
{{- include "flattener-processor.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end -}}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "flattener-processor.fullname" . }}-test-connection"
labels:
{{- include "flattener-processor.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "flattener-processor.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@ -0,0 +1,74 @@
# Default values for flattener-processor.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: signoz/flattener-processor
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
configVars:
KAFKA_BROKER: signoz-kafka:9092
KAFKA_INPUT_TOPIC: otlp_spans
KAFKA_OUTPUT_TOPIC: flattened_spans
serviceAccount:
# Specifies whether a service account should be created
create: false
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name:
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths: []
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}

View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@ -0,0 +1,21 @@
apiVersion: v2
name: frontend
description: A Helm chart for SigNoz Frontend Service
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.1.4
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application.
appVersion: v0.1.3

View File

@ -0,0 +1,21 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "frontend.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "frontend.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "frontend.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "frontend.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80
{{- end }}

View File

@ -0,0 +1,63 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "frontend.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "frontend.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "frontend.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "frontend.labels" -}}
helm.sh/chart: {{ include "frontend.chart" . }}
{{ include "frontend.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/*
Selector labels
*/}}
{{- define "frontend.selectorLabels" -}}
app.kubernetes.io/name: {{ include "frontend.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "frontend.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "frontend.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

View File

@ -0,0 +1,27 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.config.name }}
labels:
release: {{ .Release.Name }}
data:
default.conf: |-
server {
listen {{ .Values.service.port }};
server_name _;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://{{ .Values.config.queryServiceUrl }}/api;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -0,0 +1,64 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "frontend.fullname" . }}
labels:
{{- include "frontend.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "frontend.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "frontend.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
volumes:
- name: nginx-config
configMap:
name: {{ .Values.config.name }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
env:
- name: REACT_APP_QUERY_SERVICE_URL
value: {{ .Values.configVars.REACT_APP_QUERY_SERVICE_URL }}
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d
# livenessProbe:
# httpGet:
# path: /
# port: http
# readinessProbe:
# httpGet:
# path: /
# port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "frontend.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "frontend.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ . }}
backend:
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "frontend.fullname" . }}
labels:
{{- include "frontend.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "frontend.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "frontend.serviceAccountName" . }}
labels:
{{- include "frontend.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end -}}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "frontend.fullname" . }}-test-connection"
labels:
{{- include "frontend.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "frontend.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@ -0,0 +1,76 @@
# Default values for frontend.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: signoz/frontend
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
configVars:
REACT_APP_QUERY_SERVICE_URL: http://localhost:3000/api/v1/
config:
name: signoz-nginx-config
queryServiceUrl: signoz-query-service:8080
serviceAccount:
# Specifies whether a service account should be created
create: false
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name:
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 3000
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths: []
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}

View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@ -0,0 +1,21 @@
apiVersion: v2
name: query-service
description: A Helm chart for running SigNoz Query Service in Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.1.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application.
appVersion: v0.1.0

View File

@ -0,0 +1,21 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "query-service.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "query-service.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "query-service.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "query-service.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80
{{- end }}

View File

@ -0,0 +1,63 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "query-service.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "query-service.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "query-service.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "query-service.labels" -}}
helm.sh/chart: {{ include "query-service.chart" . }}
{{ include "query-service.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/*
Selector labels
*/}}
{{- define "query-service.selectorLabels" -}}
app.kubernetes.io/name: {{ include "query-service.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "query-service.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "query-service.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

View File

@ -0,0 +1,62 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "query-service.fullname" . }}
labels:
{{- include "query-service.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "query-service.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "query-service.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "query-service.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: DruidClientUrl
value: {{ .Values.configVars.DruidClientUrl }}
- name: DruidDatasource
value: {{ .Values.configVars.DruidDatasource }}
# livenessProbe:
# httpGet:
# path: /
# port: http
# readinessProbe:
# httpGet:
# path: /
# port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "query-service.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "query-service.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ . }}
backend:
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "query-service.fullname" . }}
labels:
{{- include "query-service.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "query-service.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "query-service.serviceAccountName" . }}
labels:
{{- include "query-service.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end -}}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "query-service.fullname" . }}-test-connection"
labels:
{{- include "query-service.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "query-service.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@ -0,0 +1,74 @@
# Default values for query-service.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: signoz/query-service
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
configVars:
DruidClientUrl: http://signoz-druid-router:8888
DruidDatasource: flattened_spans
serviceAccount:
# Specifies whether a service account should be created
create: false
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name:
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths: []
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}

View File

@ -0,0 +1,45 @@
kafka:
zookeeper:
enabled: false
externalZookeeper:
servers: ["signoz-zookeeper:2181"]
zookeeperConnectionTimeoutMs: 6000
druid:
configVars:
druid_extensions_loadList: '["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]'
druid_storage_type: local
# druid_storage_type: s3
# druid_storage_bucket: signoz-druid
# druid_storage_baseKey: baseKey
# aws_accessKeyId: AKIAI6WYDSRKPE57PYPA
# aws_secretKey: 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
# druid_s3_accessKey: AKIAI6WYDSRKPE57PYPA
# druid_s3_secretKey: 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
# AWS_ACCESS_KEY_ID: AKIAI6WYDSRKPE57PYPA
# AWS_SECRET_ACCESS_KEY: 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
# AWS_REGION: ap-south-1
historical:
persistence:
size: "12Gi"
zkHosts: "signoz-zookeeper:2181"
zookeeper:
enabled: false
flattener-processor:
configVars:
KAFKA_BROKER: signoz-kafka:9092
KAFKA_INPUT_TOPIC: otlp_spans
KAFKA_OUTPUT_TOPIC: flattened_spans
query-service:
configVars:
DruidClientUrl: http://signoz-druid-router:8888
DruidDatasource: flattened_spans
frontend:
configVars:
REACT_APP_QUERY_SERVICE_URL: http://localhost:3000/api/v1/

148
deploy/kubernetes/signoz.sh Normal file
View File

@ -0,0 +1,148 @@
#!/usr/bin/env bash
set -eEu -o functrace
script=$0
function clean() {
EXIT_CODE=0
kubectl get ns ${HT_KUBE_NAMESPACE} ${KUBE_FLAGS} >/dev/null 2>&1 || EXIT_CODE=$?
if [ ${EXIT_CODE} != 0 ]; then
echo "[ERROR] Couldn't find namespace to uninstall. namespace: ${HT_KUBE_NAMESPACE}, context: ${HT_KUBE_CONTEXT}"
exit 1
fi
EXIT_CODE=0
helm get all hypertrace-platform-services ${HELM_FLAGS} >/dev/null 2>&1 || EXIT_CODE=$?
if [ ${EXIT_CODE} == 0 ]; then
echo "[INFO] found existing platform services deployment. running helm uninstall. release: 'hypertrace-platform-services'"
helm uninstall hypertrace-platform-services ${HELM_FLAGS}
else
echo "[INFO] Couldn't find helm release 'hypertrace-platform-services'"
fi
EXIT_CODE=0
helm get all hypertrace-data-services ${HELM_FLAGS} >/dev/null 2>&1 || EXIT_CODE=$?
if [ ${EXIT_CODE} == 0 ]; then
echo "[INFO] found existing data services deployment. running helm uninstall. release: 'hypertrace-data-services'"
helm uninstall hypertrace-data-services ${HELM_FLAGS}
else
echo "[INFO] Couldn't find helm release 'hypertrace-data-services'"
fi
echo "[INFO] We are clean! good to go!"
}
error_report() {
local retval=$?
echo "${script}: Error at line#: $1, command: $2, error code: ${retval}"
exit ${retval}
}
trap 'error_report ${LINENO} ${BASH_COMMAND}' ERR
function usage() {
echo "usage: $script {install|uninstall} [option]"
echo " "
echo "available options:"
echo " "
echo "--clean removes previous deployments of Hypertrace and do clean install"
exit 1
}
HYPERTRACE_HOME="`dirname \"$0\"`"
HYPERTRACE_HOME="`( cd \"${HYPERTRACE_HOME}\" && pwd )`" # absolutized and normalized
HYPERTRACE_CONFIG=${HYPERTRACE_HOME}/config/hypertrace.properties
if [ ! -f "${HYPERTRACE_CONFIG}" ]; then
echo "Configuration file not found: '${HYPERTRACE_CONFIG}'"
fi
source ${HYPERTRACE_CONFIG}
LOG_DIR=${HYPERTRACE_HOME}/logs
LOG_FILE=hypertrace.log
mkdir -p ${LOG_DIR}
if [ x${HT_KUBE_CONTEXT} == "x" ]; then
HT_KUBE_CONTEXT="$(kubectl config current-context)"
fi
HELM_FLAGS="--kube-context=${HT_KUBE_CONTEXT} --namespace=${HT_KUBE_NAMESPACE} --log-dir=${LOG_DIR} --log-file=${LOG_FILE} --log-file-max-size=16 --alsologtostderr=true --logtostderr=false"
KUBE_FLAGS="--context=${HT_KUBE_CONTEXT} --namespace=${HT_KUBE_NAMESPACE} --log-dir=${LOG_DIR} --log-file=${LOG_FILE} --log-file-max-size=16 --alsologtostderr=true --logtostderr=false"
if [[ "$HT_ENABLE_DEBUG" == "true" ]]; then
HELM_FLAGS="$HELM_FLAGS --debug --v=4"
KUBE_FLAGS="$KUBE_FLAGS --v=4"
fi
if [ "$#" -gt 1 ]; then
if [[ $2 == "--clean" ]]; then
clean
else
usage
fi
elif [ "$#" -ne 1 ]; then
echo "[ERROR] Illegal number of parameters"
echo "-------------------------------------"
usage
fi
subcommand=$1; shift
case "$subcommand" in
install)
EXIT_CODE=0;
# namespace - create when it doesn't exists. ignore, otherwise.
kubectl get ns ${HT_KUBE_NAMESPACE} ${KUBE_FLAGS} >/dev/null 2>&1 || EXIT_CODE=$?
if [ ${EXIT_CODE} == 0 ]; then
echo "[WARN] Skipping namespace creation. Namespace already exists. namespace: ${HT_KUBE_NAMESPACE}, context: ${HT_KUBE_CONTEXT}"
else
echo "[INFO] Creating namespace. namespace: ${HT_KUBE_NAMESPACE}, context: ${HT_KUBE_CONTEXT}"
kubectl create ns ${HT_KUBE_NAMESPACE} ${KUBE_FLAGS}
fi
echo "[INFO] cleaning up any helm temporary working directory"
rm -rf ${HYPERTRACE_HOME}/data-services/charts
rm -rf ${HYPERTRACE_HOME}/data-services/tmpcharts
rm -rf ${HYPERTRACE_HOME}/data-services/Chart.lock
rm -rf ${HYPERTRACE_HOME}/platform-services/charts
rm -rf ${HYPERTRACE_HOME}/platform-services/tmpcharts
rm -rf ${HYPERTRACE_HOME}/platform-services/Chart.lock
echo "[INFO] installing hypertrace data services. namespace: ${HT_KUBE_NAMESPACE}, context: ${HT_KUBE_CONTEXT}"
helm dependency update ${HYPERTRACE_HOME}/data-services ${HELM_FLAGS}
helm upgrade hypertrace-data-services ${HYPERTRACE_HOME}/data-services -f ${HYPERTRACE_HOME}/data-services/values.yaml -f ${HYPERTRACE_HOME}/clusters/$HT_PROFILE/values.yaml --install --wait ${HELM_FLAGS} --timeout ${HT_INSTALL_TIMEOUT}m --set htEnv=${HT_ENV}
echo "[INFO] installing hypertrace platform services. namespace: ${HT_KUBE_NAMESPACE}, context: ${HT_KUBE_CONTEXT}"
helm dependency update ${HYPERTRACE_HOME}/platform-services ${HELM_FLAGS}
helm upgrade hypertrace-platform-services ${HYPERTRACE_HOME}/platform-services -f ${HYPERTRACE_HOME}/platform-services/values.yaml -f ${HYPERTRACE_HOME}/clusters/$HT_PROFILE/values.yaml --install ${HELM_FLAGS} --timeout ${HT_INSTALL_TIMEOUT}m --set htEnv=${HT_ENV}
echo "[INFO] Hypertrace installation completed"
;;
uninstall)
echo "[INFO] Uninstalling Hypertrace deletes the hypertrace namespace and deletes any data stored."
echo "Choose an option to continue"
select yn in "Yes" "No"; do
case $yn in
Yes )
clean
kubectl delete ns ${HT_KUBE_NAMESPACE} ${KUBE_FLAGS}
echo "[INFO] Uninstall successful."
break;;
No )
echo "[INFO] Uninstall cancelled."
exit;;
esac
done
;;
*)
echo "[ERROR] Unknown command: ${subcommand}"
usage
;;
esac

32
frontend/Dockerfile Normal file
View File

@ -0,0 +1,32 @@
# stage1 as builder
FROM node:14-alpine as builder
# copy the package.json to install dependencies
COPY package.json ./
# Install the dependencies and make the folder
RUN npm install && mkdir /react-ui && mv ./node_modules ./react-ui
WORKDIR /react-ui
COPY . .
# Build the project and copy the files
RUN npm run build
FROM nginx:1.15-alpine
#!/bin/sh
COPY conf/default.conf /etc/nginx/conf.d/default.conf
## Remove default nginx index page
RUN rm -rf /usr/share/nginx/html/*
# Copy from the stahg 1
COPY --from=builder /react-ui/build /usr/share/nginx/html
EXPOSE 3000
ENTRYPOINT ["nginx", "-g", "daemon off;"]

70
frontend/README.md Normal file
View File

@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `yarn build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

View File

@ -0,0 +1,20 @@
server {
listen 3000;
server_name _;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location = /api {
proxy_pass http://signoz-query-service:8080/api;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

28
frontend/gulpfile.js Normal file
View File

@ -0,0 +1,28 @@
const gulp = require('gulp')
const gulpless = require('gulp-less')
const postcss = require('gulp-postcss')
const debug = require('gulp-debug')
var csso = require('gulp-csso')
const autoprefixer = require('autoprefixer')
const NpmImportPlugin = require('less-plugin-npm-import')
gulp.task('less', function () {
const plugins = [autoprefixer()]
return gulp
.src('src/themes/*-theme.less')
.pipe(debug({title: 'Less files:'}))
.pipe(
gulpless({
javascriptEnabled: true,
plugins: [new NpmImportPlugin({prefix: '~'})],
}),
)
.pipe(postcss(plugins))
.pipe(
csso({
debug: true,
}),
)
.pipe(gulp.dest('./public'))
})

87
frontend/package.json Normal file
View File

@ -0,0 +1,87 @@
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.0.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/chart.js": "^2.9.28",
"@types/d3": "^6.2.0",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.7",
"@types/react": "^17.0.0",
"@types/react-dom": "^16.9.9",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
"@types/redux": "^3.6.0",
"@types/styled-components": "^5.1.4",
"@types/vis": "^4.21.21",
"antd": "^4.8.0",
"axios": "^0.21.0",
"chart.js": "^2.9.4",
"d3": "^6.2.0",
"d3-array": "^2.8.0",
"d3-ease": "^2.0.0",
"d3-flame-graph": "^3.1.1",
"d3-tip": "^0.9.1",
"material-ui-chip-input": "^2.0.0-beta.2",
"prop-types": "^15.6.2",
"react": "17.0.0",
"react-chartjs-2": "^2.11.1",
"react-chips": "^0.8.0",
"react-css-theme-switcher": "^0.1.6",
"react-dom": "17.0.0",
"react-graph-vis": "^1.0.5",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.0",
"react-vis": "^1.11.7",
"recharts": "^1.8.5",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"styled-components": "^5.2.1",
"typescript": "^4.0.5",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"storybook": "start-storybook -p 6006 -s public --no-dll",
"build-storybook": "build-storybook -s public --no-dll"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/preset-typescript": "^7.12.7",
"autoprefixer": "^9.0.0",
"babel-plugin-styled-components": "^1.12.0",
"gulp": "^4.0.2",
"gulp-csso": "^4.0.1",
"gulp-debug": "^4.0.0",
"gulp-less": "^4.0.1",
"gulp-postcss": "^9.0.0",
"less-plugin-npm-import": "^2.1.0",
"react-is": "^17.0.1"
}
}

File diff suppressed because one or more lines are too long

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

File diff suppressed because one or more lines are too long

BIN
frontend/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
frontend/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,5 @@
<svg width="320" height="120" viewBox="0 0 320 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M165.742 72.0469C165.742 70.7188 165.273 69.7031 164.336 69C163.398 68.2812 161.711 67.5312 159.273 66.75C156.836 65.9531 154.906 65.1719 153.484 64.4062C149.609 62.3125 147.672 59.4922 147.672 55.9453C147.672 54.1016 148.188 52.4609 149.219 51.0234C150.266 49.5703 151.758 48.4375 153.695 47.625C155.648 46.8125 157.836 46.4062 160.258 46.4062C162.695 46.4062 164.867 46.8516 166.773 47.7422C168.68 48.6172 170.156 49.8594 171.203 51.4688C172.266 53.0781 172.797 54.9062 172.797 56.9531H165.766C165.766 55.3906 165.273 54.1797 164.289 53.3203C163.305 52.4453 161.922 52.0078 160.141 52.0078C158.422 52.0078 157.086 52.375 156.133 53.1094C155.18 53.8281 154.703 54.7812 154.703 55.9688C154.703 57.0781 155.258 58.0078 156.367 58.7578C157.492 59.5078 159.141 60.2109 161.312 60.8672C165.312 62.0703 168.227 63.5625 170.055 65.3438C171.883 67.125 172.797 69.3438 172.797 72C172.797 74.9531 171.68 77.2734 169.445 78.9609C167.211 80.6328 164.203 81.4688 160.422 81.4688C157.797 81.4688 155.406 80.9922 153.25 80.0391C151.094 79.0703 149.445 77.75 148.305 76.0781C147.18 74.4062 146.617 72.4688 146.617 70.2656H153.672C153.672 74.0312 155.922 75.9141 160.422 75.9141C162.094 75.9141 163.398 75.5781 164.336 74.9062C165.273 74.2188 165.742 73.2656 165.742 72.0469ZM184.281 81H177.484V55.6406H184.281V81ZM177.086 49.0781C177.086 48.0625 177.422 47.2266 178.094 46.5703C178.781 45.9141 179.711 45.5859 180.883 45.5859C182.039 45.5859 182.961 45.9141 183.648 46.5703C184.336 47.2266 184.68 48.0625 184.68 49.0781C184.68 50.1094 184.328 50.9531 183.625 51.6094C182.938 52.2656 182.023 52.5938 180.883 52.5938C179.742 52.5938 178.82 52.2656 178.117 51.6094C177.43 50.9531 177.086 50.1094 177.086 49.0781ZM188.898 68.1328C188.898 64.2422 189.82 61.1094 191.664 58.7344C193.523 56.3594 196.023 55.1719 199.164 55.1719C201.945 55.1719 204.109 56.125 205.656 58.0312L205.938 55.6406H212.078V80.1562C212.078 82.375 211.57 84.3047 210.555 85.9453C209.555 87.5859 208.141 88.8359 206.312 89.6953C204.484 90.5547 202.344 90.9844 199.891 90.9844C198.031 90.9844 196.219 90.6094 194.453 89.8594C192.688 89.125 191.352 88.1719 190.445 87L193.445 82.875C195.133 84.7656 197.18 85.7109 199.586 85.7109C201.383 85.7109 202.781 85.2266 203.781 84.2578C204.781 83.3047 205.281 81.9453 205.281 80.1797V78.8203C203.719 80.5859 201.664 81.4688 199.117 81.4688C196.07 81.4688 193.602 80.2812 191.711 77.9062C189.836 75.5156 188.898 72.3516 188.898 68.4141V68.1328ZM195.672 68.625C195.672 70.9219 196.133 72.7266 197.055 74.0391C197.977 75.3359 199.242 75.9844 200.852 75.9844C202.914 75.9844 204.391 75.2109 205.281 73.6641V63C204.375 61.4531 202.914 60.6797 200.898 60.6797C199.273 60.6797 197.992 61.3438 197.055 62.6719C196.133 64 195.672 65.9844 195.672 68.625ZM245.5 81H238.469L224.781 58.5469V81H217.75V46.875H224.781L238.492 69.375V46.875H245.5V81ZM250.141 68.0859C250.141 65.5703 250.625 63.3281 251.594 61.3594C252.562 59.3906 253.953 57.8672 255.766 56.7891C257.594 55.7109 259.711 55.1719 262.117 55.1719C265.539 55.1719 268.328 56.2188 270.484 58.3125C272.656 60.4062 273.867 63.25 274.117 66.8438L274.164 68.5781C274.164 72.4688 273.078 75.5938 270.906 77.9531C268.734 80.2969 265.82 81.4688 262.164 81.4688C258.508 81.4688 255.586 80.2969 253.398 77.9531C251.227 75.6094 250.141 72.4219 250.141 68.3906V68.0859ZM256.914 68.5781C256.914 70.9844 257.367 72.8281 258.273 74.1094C259.18 75.375 260.477 76.0078 262.164 76.0078C263.805 76.0078 265.086 75.3828 266.008 74.1328C266.93 72.8672 267.391 70.8516 267.391 68.0859C267.391 65.7266 266.93 63.8984 266.008 62.6016C265.086 61.3047 263.789 60.6562 262.117 60.6562C260.461 60.6562 259.18 61.3047 258.273 62.6016C257.367 63.8828 256.914 65.875 256.914 68.5781ZM285.742 75.5391H298.141V81H277.094V76.875L289.023 61.125H277.445V55.6406H297.766V59.6484L285.742 75.5391Z" fill="#F2F2F2"/>
<path opacity="0.9" d="M69.1393 109.354C41.2841 109.354 19 87.4976 19 60.1771C19 33.1297 41.2841 11 69.1393 11H100.616C110.922 11 119 19.1962 119 29.0316V60.1771C119 87.4976 96.7159 109.354 69.1393 109.354Z" fill="#002B76"/>
<path d="M68.746 38.8879C47.9401 38.8879 36.2207 56.1168 35.7319 56.8501C34.5269 58.6568 34.5269 60.9932 35.7328 62.8011C36.2207 63.5331 47.9401 80.762 68.746 80.762C89.5517 80.762 101.271 63.5331 101.76 62.7997C102.965 60.9932 102.965 58.6568 101.759 56.8489C101.271 56.1168 89.5517 38.8879 68.746 38.8879ZM38.6117 60.8807C38.1839 60.2393 38.1839 59.4106 38.6117 58.7691C39.0048 58.1792 47.3488 45.9228 62.2671 42.9827C55.5068 45.5922 50.6965 52.1553 50.6965 59.825C50.6965 67.4946 55.5068 74.0577 62.2671 76.6672C47.3488 73.7271 39.0048 61.4706 38.6117 60.8807ZM60.4284 59.8249C60.4284 60.7806 59.6538 61.5552 58.698 61.5552C57.7423 61.5552 56.9676 60.7806 56.9676 59.8249C56.9676 53.3304 62.2513 48.0468 68.7458 48.0468C69.7015 48.0468 70.4762 48.8213 70.4762 49.7771C70.4762 50.7329 69.7015 51.5074 68.7458 51.5074C64.1595 51.5074 60.4284 55.2384 60.4284 59.8249ZM68.746 64.0317C66.4263 64.0317 64.5392 62.1446 64.5392 59.825C64.5392 57.5053 66.4263 55.6181 68.746 55.6181C71.0658 55.6181 72.9528 57.5052 72.9528 59.825C72.9528 62.1447 71.0658 64.0317 68.746 64.0317ZM98.8802 60.8807C98.4871 61.4706 90.1431 73.7269 75.2249 76.6672C81.9853 74.0575 86.7955 67.4945 86.7955 59.825C86.7955 52.1554 81.9853 45.5923 75.2249 42.9828C90.1431 45.923 98.4871 58.1793 98.8802 58.7692C99.308 59.4106 99.308 60.2393 98.8802 60.8807Z" fill="#F9F2F9"/>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,78 @@
import { ActionTypes } from './types';
import { Moment } from 'moment'
export type DateTimeRangeType = [Moment|null,Moment|null]|null;
export interface GlobalTime {
maxTime: number;
minTime: number;
}
export interface updateTimeIntervalAction {
type: ActionTypes.updateTimeInterval;
payload: GlobalTime;
}
export const updateTimeInterval = (interval:string, datetimeRange?:[number,number]) => {
let maxTime: number = 0;
let minTime: number = 0;
console.log('interval', interval, typeof(interval))
// if interval string is custom, then datetimRange should be present and max & min time should be
// set directly based on that. Assuming datetimeRange values are in ms, and minTime is 0th element
switch (interval) {
case '15min':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-15*60*1000)*1000000;
console.log('max time 15 in case',maxTime)
break;
case '30min':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-30*60*1000)*1000000;
console.log('max time in 30 min case',maxTime)
break;
case '1hr':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-1*60*60*1000)*1000000;
break;
case '6hr':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-6*60*60*1000)*1000000;
break;
case '1day':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-24*60*60*1000)*1000000;
break;
case '1week':
maxTime=Date.now()*1000000; // in nano sec
minTime=(Date.now()-7*24*60*60*1000)*1000000;
break;
case 'custom':
if (datetimeRange !== undefined)
{
maxTime=datetimeRange[1]*1000000;// in nano sec
minTime=datetimeRange[0]*1000000;// in nano sec
}
break;
default:
console.log('not found matching case')
}
return {
type: ActionTypes.updateTimeInterval,
payload: {maxTime:maxTime, minTime:minTime},
};
};

View File

@ -0,0 +1,6 @@
export * from './types';
export * from './traceFilters';
export * from './traces';
export * from './metrics';
export * from './usage';
export * from './global';

View File

@ -0,0 +1,113 @@
import { Dispatch } from 'redux';
import metricsAPI from '../api/metricsAPI';
import { GlobalTime } from './global';
import { ActionTypes } from './types';
export interface servicesListItem{
"serviceName": string;
"p99": number;
"avgDuration": number;
"numCalls": number;
"callRate": number;
"numErrors": number;
"errorRate": number;
};
export interface metricItem{
"timestamp":number;
"p50":number;
"p90":number;
"p99":number;
"numCalls":number;
"callRate":number;
"numErrors":number;
"errorRate":number;
}
export interface topEndpointListItem{
"p50": number;
"p90": number;
"p99": number;
"numCalls": number;
"name": string;
};
export interface customMetricsItem{
"timestamp": number;
"value": number;
};
export interface getServicesListAction {
type: ActionTypes.getServicesList;
payload: servicesListItem[];
}
export interface getServiceMetricsAction{
type: ActionTypes.getServiceMetrics;
payload: metricItem[];
}
export interface getTopEndpointsAction {
type: ActionTypes.getTopEndpoints;
payload: topEndpointListItem[];
}
export interface getFilteredTraceMetricsAction{
type: ActionTypes.getFilteredTraceMetrics;
payload: customMetricsItem[];
}
export const getServicesList = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = 'services?start='+globalTime.minTime+'&end='+globalTime.maxTime;
console.log(request_string);
const response = await metricsAPI.get<servicesListItem[]>(request_string);
dispatch<getServicesListAction>({
type: ActionTypes.getServicesList,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response
});
};
};
export const getServicesMetrics = (serviceName:string, globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = 'service/overview?service='+serviceName+'&start='+globalTime.minTime+'&end='+globalTime.maxTime+'&step=60';
const response = await metricsAPI.get<metricItem[]>(request_string);
dispatch<getServiceMetricsAction>({
type: ActionTypes.getServiceMetrics,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response
});
};
};
export const getTopEndpoints = (serviceName:string, globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = 'service/top_endpoints?service='+serviceName+'&start='+globalTime.minTime+'&end='+globalTime.maxTime;
const response = await metricsAPI.get<topEndpointListItem[]>(request_string);
dispatch<getTopEndpointsAction>({
type: ActionTypes.getTopEndpoints,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response
});
};
};
export const getFilteredTraceMetrics = (filter_params: string, globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = 'spans/aggregates?start='+globalTime.minTime+'&end='+globalTime.maxTime+'&'+filter_params;
const response = await metricsAPI.get<customMetricsItem[]>(request_string);
dispatch<getFilteredTraceMetricsAction>({
type: ActionTypes.getFilteredTraceMetrics,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response
});
};
};

View File

@ -0,0 +1,50 @@
// Action creator must have a type and optionally a payload
import { ActionTypes } from './types'
export interface TagItem {
key: string;
value: string;
operator: 'equals'|'contains';
}
export interface LatencyValue {
min:string;
max:string;
}
export interface TraceFilters{
tags?: TagItem[];
service?:string;
latency?:LatencyValue;
operation?:string;
}
//define interface for action. Action creator always returns object of this type
export interface updateTraceFiltersAction {
type: ActionTypes.updateTraceFilters,
payload: TraceFilters,
}
export const updateTraceFilters = (traceFilters: TraceFilters) => {
console.log('in update trace filters',traceFilters)
return {
type: ActionTypes.updateTraceFilters,
payload: traceFilters,
};
};
export interface updateInputTagAction {
type: ActionTypes.updateInput,
payload: string,
}
export const updateInputTag = (Input: string) => {
return {
type: ActionTypes.updateInput,
payload: Input,
};
};
//named export when you want to export multiple functions from the same file

View File

@ -0,0 +1,146 @@
import { ActionTypes } from './types';
import tracesAPI from '../api/tracesAPI';
import { Dispatch } from 'redux';
import { GlobalTime } from './global';
// PNOTE
// define trace interface - what it should return
// define action creator to show list of traces on component mount -- use useEffect ( , []) -> Mounts when loaded first time
// Date() - takes number of milliseconds as input, our API takes in microseconds
// Sample API call for traces - https://api.signoz.io/api/traces?end=1606968273667000&limit=20&lookback=2d&maxDuration=&minDuration=&service=driver&operation=&start=1606968100867000
export interface Tree{
name: string;
value: number;
children?: Tree[];
}
export interface RefItem{
refType: string;
traceID: string;
spanID: string;
}
export interface TraceTagItem{
key: string;
// type: string;
value: string;
}
export interface ProcessItem{
serviceName: string;
tags: TraceTagItem[];
}
// PNOTE - Temp DS for converting span to tree which can be consumed by flamegraph
export interface pushDStree {
id: string;
name: string;
value: number;
time: number;
startTime: number;
tags: TraceTagItem[];
children: pushDStree[];
}
export interface spanItem{
traceID: string; // index 0
spanID: string; // index 1
operationName: string; // index 2
startTime: number; // index 3
duration: number; // index 4
references: RefItem[]; // index 5
tags: []; //index 6
logs: []; // index 7
processID: string; // index 8
warnings: []; // index 9
children: pushDStree[]; // index 10 // PNOTE - added this as we are adding extra field in span item to convert trace to tree.
// Should this field be optional?
}
//let mapped_array :{ [id: string] : spanItem; } = {};
export interface traceItem{
traceID: string;
spans: spanItem[];
processes: { [id: string] : ProcessItem; } ;
warnings: [];
}
export interface traceResponse{
data: traceItem[];
total: number;
limit: number;
offset: number;
error: [];
}
export type span = [number, string, string, string, string, string, string, string|string[], string|string[], string|string[], pushDStree[]];
export interface spanList{
events: span[];
segmentID: string;
columns: string[];
}
// export interface traceResponseNew{
// [id: string] : spanList;
// }
export interface traceResponseNew{
[id: string] : spanList;
}
export interface spansWSameTraceIDResponse{
[id: string] : spanList;
}
export interface FetchTracesAction {
type: ActionTypes.fetchTraces;
payload: traceResponseNew;
}
export interface FetchTraceItemAction {
type: ActionTypes.fetchTraceItem;
payload: spansWSameTraceIDResponse;
}
export const fetchTraces = (globalTime: GlobalTime, filter_params: string ) => {
return async (dispatch: Dispatch) => {
if (globalTime){
let request_string = 'spans?limit=100&lookback=2d&start='+globalTime.minTime+'&end='+globalTime.maxTime+'&'+filter_params;
const response = await tracesAPI.get<traceResponseNew>(request_string);
dispatch<FetchTracesAction>({
type: ActionTypes.fetchTraces,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response?
});
}
};
};
export const fetchTraceItem = (traceID: string) => {
return async (dispatch: Dispatch) => {
let request_string = 'traces/'+traceID;
const response = await tracesAPI.get<spansWSameTraceIDResponse>(request_string);
dispatch<FetchTraceItemAction>({
type: ActionTypes.fetchTraceItem,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response?
});
};
};

View File

@ -0,0 +1,22 @@
import { FetchTracesAction, FetchTraceItemAction } from './traces';
import { updateTraceFiltersAction, updateInputTagAction } from './traceFilters';
import {getServicesListAction,getServiceMetricsAction, getTopEndpointsAction, getFilteredTraceMetricsAction} from './metrics'
import {getUsageDataAction} from './usage'
import {updateTimeIntervalAction} from './global';
export enum ActionTypes {
updateTraceFilters,
updateInput,
fetchTraces,
fetchTraceItem,
getServicesList,
getServiceMetrics,
getTopEndpoints,
getUsageData,
updateTimeInterval,
getFilteredTraceMetrics,
}
export type Action = FetchTraceItemAction | FetchTracesAction | updateTraceFiltersAction | updateInputTagAction |getServicesListAction| getServiceMetricsAction| getTopEndpointsAction | getUsageDataAction | updateTimeIntervalAction| getFilteredTraceMetricsAction;

View File

@ -0,0 +1,29 @@
import { Dispatch } from 'redux';
import metricsAPI from '../api/metricsAPI';
import { ActionTypes } from './types';
import { GlobalTime } from './global';
export interface usageDataItem{
"timestamp":number;
"count":number;
}
export interface getUsageDataAction {
type: ActionTypes.getUsageData;
payload: usageDataItem[];
}
export const getUsageData = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string = 'usage?start='+globalTime.minTime+'&end='+globalTime.maxTime+'&step=3600&service=driver';
//Step can only be multiple of 3600
const response = await metricsAPI.get<usageDataItem[]>(request_string);
dispatch<getUsageDataAction>({
type: ActionTypes.getUsageData,
payload: response.data
//PNOTE - response.data in the axios response has the actual API response
});
};
};

View File

@ -0,0 +1,9 @@
import axios from 'axios';
// No auth for the API
export default axios.create({
baseURL: 'https://api.signoz.io/api/prom/api/v1',
}
);

View File

@ -0,0 +1,9 @@
import axios from 'axios';
export default axios.create({
// baseURL: 'http://104.211.113.204:8080/api/v1/',
// baseURL: process.env.REACT_APP_QUERY_SERVICE_URL,
baseURL: 'http://localhost:3000/api/v1/',
// console.log('in metrics API', process.env.QUERY_SERVICE_URL)
}
);

View File

@ -0,0 +1,16 @@
import axios from 'axios';
//import { format } from 'path';
export default axios.create({
// baseURL: 'http://104.211.113.204:8080/api/v1/'
// baseURL: process.env.QUERY_SERVICE_URL,
// console.log('in traces API', process.env.QUERY_SERVICE_URL)
baseURL: 'http://localhost:3000/api/v1/',
});
//individual trace format
//https://api.signoz.io/api/traces/146754946da0552e
//http://104.211.113.204:8080/api/v1/traces/000000000000000053a5b7a93bc5e08a

View File

@ -0,0 +1,12 @@
@import "~antd/dist/antd.dark.css";
@import "~antd/dist/antd.compact.css";
/* #components-layout-demo-side .logo {
height: 32px;
margin: 16px;
background: rgba(255, 255, 255, 0.3);
}
.site-layout .site-layout-background {
background: #fff;
} */

View File

@ -0,0 +1,118 @@
import React, { Suspense, useState } from 'react';
import { Layout, Menu, Switch as ToggleButton, Spin, Row, Col } from 'antd';
import { useThemeSwitcher } from "react-css-theme-switcher";
import { NavLink, BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import {
LineChartOutlined,
BarChartOutlined,
DeploymentUnitOutlined,
AlignLeftOutlined,
} from '@ant-design/icons';
import DateTimeSelector from './DateTimeSelector';
import ShowBreadcrumbs from './ShowBreadcrumbs';
const { Content, Footer, Sider } = Layout;
const ServiceMetrics = React.lazy(() => import('./metrics/ServiceMetricsDef'));
const ServiceMap = React.lazy(() => import('./servicemap/ServiceMap'));
const TraceDetail = React.lazy(() => import('./traces/TraceDetail'));
const TraceGraph = React.lazy(() => import ('./traces/TraceGraphDef' ));
const UsageExplorer = React.lazy(() => import ('./usage/UsageExplorerDef' ));
const ServicesTable = React.lazy(() => import('./metrics/ServicesTableDef'));
//PNOTE
//React. lazy currently only supports default exports. If the module you want to import uses named exports, you can create an intermediate module that reexports it as the default. This ensures that tree shaking keeps working and that you don't pull in unused components.
const App = () => {
// state = { collapsed: false, isDarkMode: true };
const { switcher, currentTheme, status, themes } = useThemeSwitcher();
const [isDarkMode, setIsDarkMode] = useState(true);
const [collapsed, setCollapsed] = useState(false);
const toggleTheme = (isChecked :boolean) => {
setIsDarkMode(isChecked);
switcher({ theme: isChecked ? themes.dark : themes.light });
};
const onCollapse = (): void => {
// console.log(collapsed);
setCollapsed(!collapsed);
};
if (status === "loading") {
return null;
}
return (
<Router basename="/">
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse} width={160}>
<div className="logo">
<ToggleButton checked={isDarkMode} onChange={toggleTheme} />
<NavLink to='/'><img src={"signoz.svg"} alt={'SigNoz'} style={{margin: '5%', width: 100, display: !collapsed ? 'block' : 'none'}} /></NavLink>
</div>
<Menu theme="dark" defaultSelectedKeys={['1']} mode="inline">
<Menu.Item key="1" icon={<BarChartOutlined />}>
<NavLink to='/application' style={{fontSize: 12, textDecoration: 'none'}}>Metrics</NavLink>
</Menu.Item>
<Menu.Item key="2" icon={<AlignLeftOutlined />}>
<NavLink to='/traces' style={{fontSize: 12, textDecoration: 'none'}}>Traces</NavLink>
</Menu.Item>
<Menu.Item key="3" icon={<DeploymentUnitOutlined />}>
<NavLink to='/service-map' style={{fontSize: 12, textDecoration: 'none'}}>Service Map</NavLink>
</Menu.Item>
<Menu.Item key="4" icon={<LineChartOutlined />}>
<NavLink to='/usage-explorer' style={{fontSize: 12, textDecoration: 'none'}}>Usage Explorer</NavLink>
</Menu.Item>
</Menu>
</Sider>
<Layout className="site-layout">
<Content style={{ margin: '0 16px' }}>
<Row>
<Col span={20}>
<ShowBreadcrumbs />
</Col>
<Col span={4}>
<DateTimeSelector />
</Col>
</Row>
{/* <Divider /> */}
<Suspense fallback={<Spin size="large" />}>
<Switch>
<Route path="/application/:servicename" component={ServiceMetrics}/>
<Route path="/service-map" component={ServiceMap}/>
<Route path="/traces" exact component={TraceDetail}/>
<Route path="/traces/:id" component={TraceGraph}/>
<Route path="/usage-explorer" component={UsageExplorer}/>
<Route path="/" component={ServicesTable}/>
</Switch>
</Suspense>
</Content>
<Footer style={{ textAlign: 'center', fontSize: 10 }}>SigNoz Inc. ©2020 </Footer>
</Layout>
</Layout>
</Router>
);
}
export default App;

View File

@ -0,0 +1,68 @@
import React, { useState } from 'react';
import { Modal, Col, Row, DatePicker, TimePicker} from 'antd';
import { Store } from 'antd/lib/form/interface';
import {DateTimeRangeType} from '../actions'
const { RangePicker } = DatePicker;
interface CustomDateTimeModalProps {
visible: boolean;
onCreate: (dateTimeRange: DateTimeRangeType) => void; //Store is defined in antd forms library
onCancel: () => void;
}
const CustomDateTimeModal: React.FC<CustomDateTimeModalProps> = ({ //destructuring props
visible,
onCreate,
onCancel,
}) => {
// RangeValue<Moment> == [Moment|null,Moment|null]|null
const [customDateTimeRange, setCustomDateTimeRange]=useState<DateTimeRangeType>();
function handleRangePickerOk(date_time: DateTimeRangeType) {
console.log('onhandleRangePickerOk: ', date_time?date_time[0]?.toDate():null,date_time?date_time[1]?.toDate():null );
setCustomDateTimeRange(date_time);
}
// function handleApplyDateTimeModal (){
// if (customDateTimeRange !== null && customDateTimeRange !== undefined && customDateTimeRange[0] !== null && customDateTimeRange[1] !== null )
// console.log('in handleApplyDateTimeModal', customDateTimeRange[0].toDate(),customDateTimeRange[1].toDate())
// }
return (
<Modal
visible={visible}
title="Chose date and time range"
okText="Apply"
cancelText="Cancel"
onCancel={onCancel}
// style={{ position: "absolute", top: 60, left: parseInt(`${positionleft}`) }} //get position as a prop from parent component, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
style={{ position: "absolute", top: 60, right: 40 }}
onOk={() => onCreate(customDateTimeRange?customDateTimeRange:null)}
>
<RangePicker onOk={handleRangePickerOk} showTime />
{/* <RangePicker renderExtraFooter={() => 'extra footer'} showTime /> */}
{/* <Row>
<Col span={6}> <DatePicker />
</Col>
<Col span={6}>
<TimePicker />
</Col>
<Col span={6}>
<DatePicker />
</Col>
<Col span={6}>
<TimePicker />
</Col>
</Row> */}
</Modal>
);
};
export default CustomDateTimeModal;

View File

@ -0,0 +1,157 @@
import React, { useState } from 'react';
import {Select, Button,Space, Form} from 'antd';
import styled from 'styled-components';
import { withRouter } from "react-router";
import { RouteComponentProps } from 'react-router-dom';
import { connect } from 'react-redux';
import CustomDateTimeModal from './CustomDateTimeModal';
import { GlobalTime, updateTimeInterval } from '../actions';
import { StoreState } from '../reducers';
import FormItem from 'antd/lib/form/FormItem';
import {DateTimeRangeType} from '../actions'
import {
RedoOutlined,
} from '@ant-design/icons';
const { Option } = Select;
const DateTimeWrapper = styled.div`
margin-top:20px;
`;
interface DateTimeSelectorProps extends RouteComponentProps<any> {
currentpath?:string;
updateTimeInterval: Function;
globalTime: GlobalTime;
// urlTime:string;
}
const _DateTimeSelector = (props:DateTimeSelectorProps) => {
const [customDTPickerVisible,setCustomDTPickerVisible]=useState(false);
const [timeInterval,setTimeInterval]=useState('15min')
const [refreshButtonHidden, setRefreshButtonHidden]=useState(false)
const [form_dtselector] = Form.useForm();
console.log('props in DateTimeSelector',props)
const handleOnSelect = (value:string) =>
{
console.log(value);
if (value === 'custom')
{
setCustomDTPickerVisible(true);
}
else
{
props.history.push({
search: '?time='+value,
}) //pass time in URL query param for all choices except custom in datetime picker
props.updateTimeInterval(value);
// console.log('in handle on select urlTime',props.urlTime)
// console.log('global time', props.globalTime.maxTime,props.globalTime.minTime)
setTimeInterval(value);
setRefreshButtonHidden(false); // for normal intervals, show refresh button
}
}
//function called on clicking apply in customDateTimeModal
const handleOk = (dateTimeRange:DateTimeRangeType) =>
{
console.log('in handleOk of DateTimeSelector',dateTimeRange);
// pass values in ms [minTime, maxTime]
if (dateTimeRange!== null && dateTimeRange!== undefined && dateTimeRange[0]!== null && dateTimeRange[1]!== null )
{
props.updateTimeInterval('custom',[dateTimeRange[0].valueOf(),dateTimeRange[1].valueOf()])
//setting globaltime
setRefreshButtonHidden(true);
form_dtselector.setFieldsValue({interval:(dateTimeRange[0].valueOf().toString()+'-'+dateTimeRange[1].valueOf().toString()) ,})
}
setCustomDTPickerVisible(false);
}
const timeSinceLastRefresh = () => {
let timeDiffSec = Math.round((Date.now() - Math.round(props.globalTime.maxTime/1000000))/1000);
//How will Refresh button get updated? Needs to be periodically updated via timer.
// For now, not returning any text here
// if (timeDiffSec < 60)
// return timeDiffSec.toString()+' s';
// else if (timeDiffSec < 3600)
// return Math.round(timeDiffSec/60).toString()+' min';
// else
// return Math.round(timeDiffSec/3600).toString()+' hr';
return null;
}
const handleRefresh = () =>
{
console.log('refreshing time interval', timeInterval)
props.updateTimeInterval(timeInterval);
}
if (props.location.pathname.startsWith('/usage-explorer')) {
return null;
} else
{
return (
<DateTimeWrapper>
<Space>
<Form form={form_dtselector} layout='inline' initialValues={{ interval:'15min', }} style={{marginTop: 10, marginBottom:10}}>
<FormItem name='interval'>
<Select style={{ width: 300 }} onSelect={handleOnSelect} >
<Option value="custom">Custom</Option>
<Option value="15min">15 min</Option>
<Option value="30min">30 min</Option>
<Option value="1hr">1 hour</Option>
<Option value="6hr">6 hour</Option>
<Option value="1day">1 day</Option>
<Option value="1week">1 week</Option>
</Select>
</FormItem>
<FormItem hidden={refreshButtonHidden} name='refresh_button'>
<Button type="primary" onClick={handleRefresh}>Refresh {timeSinceLastRefresh()}</Button>
{/* if refresh time is more than x min, give a message? */}
</FormItem>
</Form>
<CustomDateTimeModal
visible={customDTPickerVisible}
onCreate={handleOk}
onCancel={() => {
setCustomDTPickerVisible(false);
}}
/>
</Space>
</DateTimeWrapper>
);
}
}
//const mapStateToProps = (state: StoreState, ownProps: RouteComponentProps<any> -- ownProps is of type RouteComponentProps<any>
const mapStateToProps = (state: StoreState ): { globalTime: GlobalTime} => {
return { globalTime : state.globalTime };
// Check if we should pass as urltime or how do we handle this
};
// the name mapStateToProps is only a convention
// take state and map it to props which are accessible inside this component
export const DateTimeSelector = connect(mapStateToProps, {
updateTimeInterval: updateTimeInterval,
})(_DateTimeSelector);
export default withRouter(DateTimeSelector);

View File

@ -0,0 +1,77 @@
import React from 'react';
import {Breadcrumb} from 'antd';
import { Link, withRouter } from 'react-router-dom';
import styled from 'styled-components';
const BreadCrumbWrapper = styled.div`
padding-top:20px;
padding-left:20px;
`;
const breadcrumbNameMap :any = { // PNOTE - TO DO - Remove any and do typechecking - like https://stackoverflow.com/questions/56568423/typescript-no-index-signature-with-a-parameter-of-type-string-was-found-on-ty
'/application': 'Application',
'/traces': 'Traces',
'/service-map': 'Service Map',
'/usage-explorer': 'Usage Explorer',
// only top level things should be mapped here, rest should be taken dynamically from url
// Does this work if url has 2 levels of dynamic parameters? - Currently we have only 1 level
// this structure ignores query params like time -- which is good
};
const ShowBreadcrumbs = withRouter(props => {
const { location } = props;
const pathSnippets = location.pathname.split('/').filter(i => i);
const extraBreadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
if (breadcrumbNameMap[url] === undefined){
return (
<Breadcrumb.Item key={url}>
<Link to={url}>{url.split('/').slice(-1)[0]}</Link>
</Breadcrumb.Item>
);
} else
{
return (
<Breadcrumb.Item key={url}>
<Link to={url}>{breadcrumbNameMap[url]}</Link>
</Breadcrumb.Item>
);
}
});
const breadcrumbItems = [
<Breadcrumb.Item key="home">
<Link to="/">Home</Link>
</Breadcrumb.Item>,
].concat(extraBreadcrumbItems);
return (
<BreadCrumbWrapper>
<Breadcrumb>{breadcrumbItems}</Breadcrumb>
</BreadCrumbWrapper>
);
});
// const ShowBreadcrumbs = () => {
// return (
// <Breadcrumb style={{ margin: '16px 0' , fontSize: 12 }}>
// <Breadcrumb.Item>
// <Link to="/">Home</Link></Breadcrumb.Item>
// <Breadcrumb.Item><Link to="/application">Application</Link></Breadcrumb.Item>
// </Breadcrumb>
// //programmatically populate it with links
// );
// }
export default ShowBreadcrumbs;

View File

@ -0,0 +1,295 @@
import React from 'react';
import { Line as ChartJSLine } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js';
import { withRouter } from "react-router";
import { RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import { metricItem } from '../../actions/metrics'
const ChartPopUpUnique = styled.div<{ ycoordinate: number, xcoordinate: number }>`
background-color:white;
border:1px solid rgba(219,112,147,0.5);
zIndex:10;
position:absolute;
top:${props => props.ycoordinate}px;
left:${props => props.xcoordinate}px;
font-size:10px;
border-radius:2px;
`;
const PopUpElements = styled.p`
color:black;
margin-bottom:0px;
padding-left:4px;
padding-right:4px;
&:hover {
cursor:pointer;
}
`;
// PNOTE - Check if this should be the case
const theme = 'dark';
interface ErrorRateChartProps extends RouteComponentProps<any> {
data : metricItem[],
}
interface ErrorRateChart {
chartRef: any;
}
class ErrorRateChart extends React.Component<ErrorRateChartProps>{
constructor(props: ErrorRateChartProps) {
super(props);
console.log('React CreatRef', React.createRef());
this.chartRef = React.createRef();
}
state = {
// data: props.data,
xcoordinate:0,
ycoordinate:0,
showpopUp:false,
// graphInfo:{}
}
onClickhandler = async(e:any,event:any) => {
console.log('e graph', e)
// console.log('event graph',event)
//PNOTE - e has all key values from mouse event, event has only reference to handler functions
// PNOTE - https://github.com/emn178/angular2-chartjs/issues/29 - for listening only to element points
// var firstPoin = this.chart.current.getElementAtEvent(e)
console.log('chartref',this.chartRef.current.chartInstance);
var firstPoint;
if(this.chartRef){
firstPoint = this.chartRef.current.chartInstance.getElementAtEvent(e)[0];
}
console.log('firstPoint', firstPoint);
// if (firstPoint) {
// var label = myChart.data.labels[firstPoint._index];
// var value = myChart.data.datasets[firstPoint._datasetIndex].data[firstPoint._index];
// }
if (firstPoint)
{// PNOTE - TODO - Is await needed in this expression?
await this.setState({
xcoordinate:e.offsetX+20,
ycoordinate:e.offsetY,
showpopUp:true,
// graphInfo:{...event}
})
}
// console.log(this.state.graphInfo.payload.timestamp)
//this.props.applicationTimeStamp(e.payload.x)
// this.props.history.push('/traces?timestamp=' + e.payload.timestamp + '&service=' + this.props.service.name)
}
gotoTracesHandler=()=>{
console.log('in gotoTraces handler')
this.props.history.push('/traces')
// this.props.history.push('/traces?timestamp=' + this.state.graphInfo.payload.timestamp + '&service=' + this.props.service.name)
}
gotoAlertsHandler=()=>{
console.log('in gotoAlerts handler')
this.props.history.push('/service-map')
// PNOTE - Keeping service map for now, will replace with alerts when alert page is made
}
options_charts: ChartOptions = {
onClick: this.onClickhandler,
maintainAspectRatio: true,
responsive: true,
title: {
display: true,
text: 'Error per sec',
fontSize: 20,
position:'top',
padding: 2,
fontFamily: 'Arial',
fontStyle: 'regular',
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
},
legend: {
display: true,
position: 'bottom',
align: 'center',
labels: {
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
fontSize: 10,
boxWidth : 10,
usePointStyle : true,
}
},
tooltips: {
mode: 'label',
bodyFontSize: 10,
titleFontSize: 10,
callbacks: {
label: function(tooltipItem, data) {
if (typeof(tooltipItem.yLabel) === 'number')
{
return data.datasets![tooltipItem.datasetIndex!].label +' : '+ tooltipItem.yLabel.toFixed(3);
}
else
{
return '';
}
// return data.datasets![tooltipItem.datasetIndex!].label +' : '+ tooltipItem.yLabel!.Fixed(3);
// not able to do toFixed(3) in typescript as string|number type is not working with toFixed(3) function for
// as toFixed() function only works with numbers
// using type of check gives issues in 'label' variable name
//!That's the non-null assertion operator. It is a way to tell the compiler "this expression cannot be null or undefined here, so don't complain about the possibility of it being null or undefined." Sometimes the type checker is unable to make that determination itself.
},
},
},
scales: {
yAxes: [
{
stacked: false,
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 6,
},
// scaleLabel: {
// display: true,
// labelString: 'latency in ms',
// fontSize: 6,
// padding: 4,
// },
gridLines: {
// You can change the color, the dash effect, the main axe color, etc.
borderDash: [1, 4],
color: "#D3D3D3",
lineWidth: 0.25,
}
},
],
xAxes: [{
type: 'time',
// time: {
// unit: 'second'
// },
distribution:'linear',
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
}]
},
}
GraphTracePopUp = () => {
console.log('state in GraphTracePopPup',this.state);
if (this.state.showpopUp){
return(
// <div className='applicationpopup' style={{top:`${this.state.ycoordinate}px`,zIndex:10,position:'absolute',left:`${this.state.xcoordinate}px`,backgroundColor:'white',border:'1px solid grey'}}>
// <p style={{color:'black'}} onClick={this.gotoTracesHandler}>View Traces</p>
// <p style={{color:'black'}}> Set Alerts</p>
// </div>
// <ChartPopUpUnique>
// <p style={{color:'black'}} onClick={this.gotoTracesHandler}>View Traces</p>
// <p style={{color:'black'}}> Set Alerts</p>
// </ChartPopUpUnique>
<ChartPopUpUnique xcoordinate={this.state.xcoordinate} ycoordinate={this.state.ycoordinate}>
<PopUpElements onClick={this.gotoTracesHandler}>View Traces</PopUpElements>
<PopUpElements onClick={this.gotoAlertsHandler}>Set Alerts</PopUpElements>
</ChartPopUpUnique>
)
}
else
return null;
}
render(){
const ndata = this.props.data;
// console.log("in chartJS line render function")
// console.log(ndata);
// const data_charts = data.map( s => ({label:s.ts, value:parseFloat(s.val)}) );
// if(this.chartRef.ctx)
// {
// var gradient = this.chartRef.ctx.createLinearGradient(0, 0, 0, 400);
// gradient.addColorStop(0, 'rgba(250,174,50,1)');
// gradient.addColorStop(1, 'rgba(250,174,50,0)');
// }
const data_chartJS = (canvas:any) => {
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(0, 'rgba(250,174,50,1)');
gradient.addColorStop(1, 'rgba(250,174,50,1)');
return{labels: ndata.map(s => new Date(s.timestamp/1000000)), // converting from nano second to mili second
datasets: [{
label: 'Errors per sec',
data: ndata.map(s => s.errorRate),
// backgroundColor:'#000000',
// fill: true,
// backgroundColor: gradient,
pointRadius: 0.5,
borderColor: 'rgba(227, 74, 51,1)', // Can also add transparency in border color
borderWidth: 2,
},
]}
};
return(
<div>
{this.GraphTracePopUp()}
<ChartJSLine ref={this.chartRef} data={data_chartJS} options={this.options_charts} />
</div>
);
}
}
export default withRouter(ErrorRateChart);

View File

@ -0,0 +1,108 @@
import React from 'react';
import { Bar, Line as ChartJSLine } from 'react-chartjs-2';
import styled from 'styled-components';
import { customMetricsItem } from '../../actions/metrics'
const GenVisualizationWrapper = styled.div`
height:160px;
`;
interface GenericVisualizationsProps {
chartType: string;
data: customMetricsItem[];
}
const GenericVisualizations = (props: GenericVisualizationsProps) => {
// const [serviceName, setServiceName] = useState('Frontend'); //default value of service name
const data = {
labels: props.data.map(s => new Date(s.timestamp/1000000)),
datasets: [{
data: props.data.map(s => s.value),
// label: "Africa",
borderColor: 'rgba(250,174,50,1)',// for line chart
backgroundColor: props.chartType==='bar'?'rgba(250,174,50,1)':'', // for bar chart, don't assign backgroundcolor if its not a bar chart, may be relevant for area graph though
},
// {
// data: [282,350,411,502,635,809,947,1402,3700,5267,282,350,411,502,635,809,947,1402,3700,5267],
// label: "Asia",
// borderColor: 'rgba(227, 74, 51, 1.0)',
// backgroundColor: props.chartType==='bar'?'rgba(227, 74, 51, 1.0)':'',
// },
//{
// data: [168,170,178,190,203,276,408,547,675,734],
// label: "Europe",
// borderColor: "#3cba9f",
// fill: false
// }, {
// data: [40,20,10,16,24,38,74,167,508,784],
// label: "Latin America",
// borderColor: "#e8c3b9",
// fill: false
// }, {
// data: [6,3,2,2,7,26,82,172,312,433],
// label: "North America",
// borderColor: "#c45850",
// fill: false
// }
]
};
const options= {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false,
},
scales: {
yAxes: [{
gridLines: {
drawBorder: false,
},
ticks: {
display: false
}
}],
xAxes: [{
type: 'time',
// time: {
// unit: 'second'
// },
//PNOTE - How to enable distribution == linear?
// distribution: 'linear',
//'linear': data are spread according to their time (distances can vary)
// From https://www.chartjs.org/docs/latest/axes/cartesian/time.html
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
}],
},
};
if(props.chartType === 'line')
{
return (
<GenVisualizationWrapper>
<ChartJSLine data={data} options={options} />
</GenVisualizationWrapper>
);
} else if (props.chartType === 'bar')
{
return (
<GenVisualizationWrapper>
<Bar data={data} options={options} />
</GenVisualizationWrapper>
);
}
else
return null;
}
export default GenericVisualizations;

View File

@ -0,0 +1,357 @@
import React from 'react';
import { Line as ChartJSLine } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js';
import { withRouter } from "react-router";
import { RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import { metricItem } from '../../actions/metrics'
const ChartPopUpUnique = styled.div<{ ycoordinate: number, xcoordinate: number }>`
background-color:white;
border:1px solid rgba(219,112,147,0.5);
zIndex:10;
position:absolute;
top:${props => props.ycoordinate}px;
left:${props => props.xcoordinate}px;
font-size:10px;
border-radius:2px;
`;
const PopUpElements = styled.p`
color:black;
margin-bottom:0px;
padding-left:4px;
padding-right:4px;
&:hover {
cursor:pointer;
}
`;
// const data_charts = {
// labels: ['1', '2', '3', '4', '5', '6'],
// datasets: [
// {
// // label: '# of Votes',
// data: [12, 19, 3, 5, 2, 3],
// fill: false,
// backgroundColor: 'rgb(255, 99, 132)',
// borderColor: 'rgba(255, 99, 132, 0.2)',
// },
// ],
// }
// const onClickHandler = (e) => {
// console.log("ChartJS chart clicked");
// console.log(e);
// };
// export interface ApiData {
// 0: number, // has epoch timestamp
// 1: string, // has value of the metric as a string
// }
const theme = 'dark';
// PNOTE - accessing history object in typescript - https://stackoverflow.com/questions/49342390/typescript-how-to-add-type-check-for-history-object-in-react
// withRouter is used to pass on history object as prop - https://stackoverflow.com/questions/43107912/how-to-access-history-object-in-new-react-router-v4
interface LatencyLineChartProps extends RouteComponentProps<any> {
data : metricItem[],
// chartRef: any,
// chartReference :ChartJSLineChart,
// data passed to ChartJSLineChart component is an array if json objects
}
interface LatencyLineChart {
chartRef: any;
}
class LatencyLineChart extends React.Component<LatencyLineChartProps>{
constructor(props: LatencyLineChartProps) {
super(props);
console.log('React CreatRef', React.createRef());
this.chartRef = React.createRef();
}
state = {
// data: props.data,
xcoordinate:0,
ycoordinate:0,
showpopUp:false,
// graphInfo:{}
}
onClickhandler = async(e:any,event:any) => {
console.log('e graph', e)
// console.log('event graph',event)
//PNOTE - e has all key values from mouse event, event has only reference to handler functions
// PNOTE - https://github.com/emn178/angular2-chartjs/issues/29 - for listening only to element points
// var firstPoin = this.chart.current.getElementAtEvent(e)
console.log('chartref',this.chartRef.current.chartInstance);
var firstPoint;
if(this.chartRef){
firstPoint = this.chartRef.current.chartInstance.getElementAtEvent(e)[0];
}
console.log('firstPoint', firstPoint);
// if (firstPoint) {
// var label = myChart.data.labels[firstPoint._index];
// var value = myChart.data.datasets[firstPoint._datasetIndex].data[firstPoint._index];
// }
if (firstPoint)
{// PNOTE - TODO - Is await needed in this expression?
await this.setState({
xcoordinate:e.offsetX+20,
ycoordinate:e.offsetY,
showpopUp:true,
// graphInfo:{...event}
})
}
// console.log(this.state.graphInfo.payload.timestamp)
//this.props.applicationTimeStamp(e.payload.x)
// this.props.history.push('/traces?timestamp=' + e.payload.timestamp + '&service=' + this.props.service.name)
}
gotoTracesHandler=()=>{
console.log('in gotoTraces handler')
this.props.history.push('/traces')
// this.props.history.push('/traces?timestamp=' + this.state.graphInfo.payload.timestamp + '&service=' + this.props.service.name)
}
gotoAlertsHandler=()=>{
console.log('in gotoAlerts handler')
this.props.history.push('/service-map')
// PNOTE - Keeping service map for now, will replace with alerts when alert page is made
}
options_charts: ChartOptions = {
// onClick: function(evt, element) {
// // console.log(evt);
// },
//- PNOTE - TO DO -- element is of type ChartElement, how to define its type
// https://gitlab.com/signoz-frontend/sample-project/-/blob/darkthemechanges/src/Components/Application/Graphs/SimpleLineChart.js
// Code for popup
// onClick: function(evt, element :any[]) {
// if (element.length > 0) {
// var ind = element[0]._index;
// console.log(element)
// alert(ind);
// }
// },
onClick: this.onClickhandler,
maintainAspectRatio: true,
responsive: true,
title: {
display: true,
text: 'Application Latency in ms',
fontSize: 20,
position:'top',
padding: 8,
fontFamily: 'Arial',
fontStyle: 'regular',
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
},
legend: {
display: true,
position: 'bottom',
align: 'center',
labels: {
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
fontSize: 10,
boxWidth : 10,
usePointStyle : true,
}
},
tooltips: {
mode: 'label',
bodyFontSize: 10,
titleFontSize: 10,
callbacks: {
label: function(tooltipItem, data) {
if (typeof(tooltipItem.yLabel) === 'number')
{
return data.datasets![tooltipItem.datasetIndex!].label +' : '+ tooltipItem.yLabel.toFixed(3);
}
else
{
return '';
}
// return data.datasets![tooltipItem.datasetIndex!].label +' : '+ tooltipItem.yLabel!.Fixed(3);
// not able to do toFixed(3) in typescript as string|number type is not working with toFixed(3) function for
// as toFixed() function only works with numbers
// using type of check gives issues in 'label' variable name
//!That's the non-null assertion operator. It is a way to tell the compiler "this expression cannot be null or undefined here, so don't complain about the possibility of it being null or undefined." Sometimes the type checker is unable to make that determination itself.
},
},
},
scales: {
yAxes: [
{
stacked: false,
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 6,
},
// scaleLabel: {
// display: true,
// labelString: 'latency in ms',
// fontSize: 6,
// padding: 4,
// },
gridLines: {
// You can change the color, the dash effect, the main axe color, etc.
borderDash: [1, 4],
color: "#D3D3D3",
lineWidth: 0.25,
}
},
],
xAxes: [{
type: 'time',
// time: {
// unit: 'second'
// },
distribution: 'linear',
//'linear': data are spread according to their time (distances can vary)
// From https://www.chartjs.org/docs/latest/axes/cartesian/time.html
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
}]
},
}
GraphTracePopUp = () => {
console.log('state in GraphTracePopPup',this.state);
if (this.state.showpopUp){
return(
// <div className='applicationpopup' style={{top:`${this.state.ycoordinate}px`,zIndex:10,position:'absolute',left:`${this.state.xcoordinate}px`,backgroundColor:'white',border:'1px solid grey'}}>
// <p style={{color:'black'}} onClick={this.gotoTracesHandler}>View Traces</p>
// <p style={{color:'black'}}> Set Alerts</p>
// </div>
// <ChartPopUpUnique>
// <p style={{color:'black'}} onClick={this.gotoTracesHandler}>View Traces</p>
// <p style={{color:'black'}}> Set Alerts</p>
// </ChartPopUpUnique>
<ChartPopUpUnique xcoordinate={this.state.xcoordinate} ycoordinate={this.state.ycoordinate}>
<PopUpElements onClick={this.gotoTracesHandler}>View Traces</PopUpElements>
<PopUpElements onClick={this.gotoAlertsHandler}>Set Alerts</PopUpElements>
</ChartPopUpUnique>
)
}
else
return null;
}
render(){
const ndata = this.props.data;
// console.log("in chartJS line render function")
// console.log(ndata);
// const data_charts = data.map( s => ({label:s.ts, value:parseFloat(s.val)}) );
// if(this.chartRef.ctx)
// {
// var gradient = this.chartRef.ctx.createLinearGradient(0, 0, 0, 400);
// gradient.addColorStop(0, 'rgba(250,174,50,1)');
// gradient.addColorStop(1, 'rgba(250,174,50,0)');
// }
const data_chartJS = (canvas:any) => {
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(0, 'rgba(250,174,50,1)');
gradient.addColorStop(1, 'rgba(250,174,50,1)');
return{ labels: ndata.map(s => new Date(s.timestamp/1000000)),
datasets: [{
label: 'p99 Latency',
data: ndata.map(s => s.p99/1000000), //converting latency from nano sec to ms
// backgroundColor:'#000000',
// fill: true,
// backgroundColor: gradient,
pointRadius: 0.5,
borderColor: 'rgba(250,174,50,1)', // Can also add transparency in border color
borderWidth: 2,
},
{
label: 'p90 Latency',
data: ndata.map(s => s.p90/1000000), //converting latency from nano sec to ms
// backgroundColor:'#dd0000',
// fill: true,
// backgroundColor: 'rgba(227, 74, 51, 1.0)',
pointRadius: 0.5,
borderColor: 'rgba(227, 74, 51, 1.0)',
borderWidth: 2,
},
{
label: 'p50 Latency',
data: ndata.map(s => s.p50/1000000), //converting latency from nano sec to ms
// backgroundColor:'#dd0000',
// fill: true,
// backgroundColor: 'rgba(227, 74, 51, 1.0)',
pointRadius: 0.5,
borderColor: 'rgba(57, 255, 20, 1.0)',
borderWidth: 2,
},
]}
};
return(
<div>
{this.GraphTracePopUp()}
<ChartJSLine ref={this.chartRef} data={data_chartJS} options={this.options_charts} />
</div>
);
}
}
export default withRouter(LatencyLineChart);

View File

@ -0,0 +1,336 @@
import React from 'react';
import { Line as ChartJSLine } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js';
import { withRouter } from "react-router";
import { RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import { metricItem } from '../../actions/metrics'
const ChartPopUpUnique = styled.div<{ ycoordinate: number, xcoordinate: number }>`
background-color:white;
border:1px solid rgba(219,112,147,0.5);
zIndex:10;
position:absolute;
top:${props => props.ycoordinate}px;
left:${props => props.xcoordinate}px;
font-size:10px;
border-radius:2px;
`;
const PopUpElements = styled.p`
color:black;
margin-bottom:0px;
padding-left:4px;
padding-right:4px;
&:hover {
cursor:pointer;
}
`;
// const data_charts = {
// labels: ['1', '2', '3', '4', '5', '6'],
// datasets: [
// {
// // label: '# of Votes',
// data: [12, 19, 3, 5, 2, 3],
// fill: false,
// backgroundColor: 'rgb(255, 99, 132)',
// borderColor: 'rgba(255, 99, 132, 0.2)',
// },
// ],
// }
// const onClickHandler = (e) => {
// console.log("ChartJS chart clicked");
// console.log(e);
// };
// export interface ApiData {
// 0: number, // has epoch timestamp
// 1: string, // has value of the metric as a string
// }
const theme = 'dark';
// PNOTE - accessing history object in typescript - https://stackoverflow.com/questions/49342390/typescript-how-to-add-type-check-for-history-object-in-react
// withRouter is used to pass on history object as prop - https://stackoverflow.com/questions/43107912/how-to-access-history-object-in-new-react-router-v4
interface RequestRateChartProps extends RouteComponentProps<any> {
data : metricItem[],
// chartRef: any,
// chartReference :ChartJSLineChart,
// data passed to ChartJSLineChart component is an array if json objects
}
interface RequestRateChart {
chartRef: any;
}
class RequestRateChart extends React.Component<RequestRateChartProps>{
constructor(props: RequestRateChartProps) {
super(props);
console.log('React CreatRef', React.createRef());
this.chartRef = React.createRef();
}
state = {
// data: props.data,
xcoordinate:0,
ycoordinate:0,
showpopUp:false,
// graphInfo:{}
}
onClickhandler = async(e:any,event:any) => {
console.log('e graph', e)
// console.log('event graph',event)
//PNOTE - e has all key values from mouse event, event has only reference to handler functions
// PNOTE - https://github.com/emn178/angular2-chartjs/issues/29 - for listening only to element points
// var firstPoin = this.chart.current.getElementAtEvent(e)
console.log('chartref',this.chartRef.current.chartInstance);
var firstPoint;
if(this.chartRef){
firstPoint = this.chartRef.current.chartInstance.getElementAtEvent(e)[0];
}
console.log('firstPoint', firstPoint);
// if (firstPoint) {
// var label = myChart.data.labels[firstPoint._index];
// var value = myChart.data.datasets[firstPoint._datasetIndex].data[firstPoint._index];
// }
if (firstPoint)
{// PNOTE - TODO - Is await needed in this expression?
await this.setState({
xcoordinate:e.offsetX+20,
ycoordinate:e.offsetY,
showpopUp:true,
// graphInfo:{...event}
})
}
// console.log(this.state.graphInfo.payload.timestamp)
//this.props.applicationTimeStamp(e.payload.x)
// this.props.history.push('/traces?timestamp=' + e.payload.timestamp + '&service=' + this.props.service.name)
}
gotoTracesHandler=()=>{
console.log('in gotoTraces handler')
this.props.history.push('/traces')
// this.props.history.push('/traces?timestamp=' + this.state.graphInfo.payload.timestamp + '&service=' + this.props.service.name)
}
gotoAlertsHandler=()=>{
console.log('in gotoAlerts handler')
this.props.history.push('/service-map')
// PNOTE - Keeping service map for now, will replace with alerts when alert page is made
}
options_charts: ChartOptions = {
// onClick: function(evt, element) {
// // console.log(evt);
// },
//- PNOTE - TO DO -- element is of type ChartElement, how to define its type
// https://gitlab.com/signoz-frontend/sample-project/-/blob/darkthemechanges/src/Components/Application/Graphs/SimpleLineChart.js
// Code for popup
// onClick: function(evt, element :any[]) {
// if (element.length > 0) {
// var ind = element[0]._index;
// console.log(element)
// alert(ind);
// }
// },
onClick: this.onClickhandler,
maintainAspectRatio: true,
responsive: true,
title: {
display: true,
text: 'Request per sec',
fontSize: 20,
position:'top',
padding: 2,
fontFamily: 'Arial',
fontStyle: 'regular',
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
},
legend: {
display: true,
position: 'bottom',
align: 'center',
labels: {
fontColor:theme === 'dark'? 'rgb(200, 200, 200)':'rgb(20, 20, 20)' ,
fontSize: 10,
boxWidth : 10,
usePointStyle : true,
}
},
tooltips: {
mode: 'label',
bodyFontSize: 10,
titleFontSize: 10,
callbacks: {
label: function(tooltipItem, data) {
if (typeof(tooltipItem.yLabel) === 'number')
{
return data.datasets![tooltipItem.datasetIndex!].label +' : '+ tooltipItem.yLabel.toFixed(3);
}
else
{
return '';
}
// return data.datasets![tooltipItem.datasetIndex!].label +' : '+ tooltipItem.yLabel!.Fixed(3);
// not able to do toFixed(3) in typescript as string|number type is not working with toFixed(3) function for
// as toFixed() function only works with numbers
// using type of check gives issues in 'label' variable name
//!That's the non-null assertion operator. It is a way to tell the compiler "this expression cannot be null or undefined here, so don't complain about the possibility of it being null or undefined." Sometimes the type checker is unable to make that determination itself.
},
},
},
scales: {
yAxes: [
{
stacked: false,
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 6,
},
// scaleLabel: {
// display: true,
// labelString: 'latency in ms',
// fontSize: 6,
// padding: 4,
// },
gridLines: {
// You can change the color, the dash effect, the main axe color, etc.
borderDash: [1, 4],
color: "#D3D3D3",
lineWidth: 0.25,
}
},
],
xAxes: [{
type: 'time',
// time: {
// unit: 'second'
// },
distribution:'linear',
ticks: {
beginAtZero: false,
fontSize: 10,
autoSkip: true,
maxTicksLimit: 10,
},
// gridLines: false, --> not a valid option
}]
},
}
GraphTracePopUp = () => {
console.log('state in GraphTracePopPup',this.state);
if (this.state.showpopUp){
return(
// <div className='applicationpopup' style={{top:`${this.state.ycoordinate}px`,zIndex:10,position:'absolute',left:`${this.state.xcoordinate}px`,backgroundColor:'white',border:'1px solid grey'}}>
// <p style={{color:'black'}} onClick={this.gotoTracesHandler}>View Traces</p>
// <p style={{color:'black'}}> Set Alerts</p>
// </div>
// <ChartPopUpUnique>
// <p style={{color:'black'}} onClick={this.gotoTracesHandler}>View Traces</p>
// <p style={{color:'black'}}> Set Alerts</p>
// </ChartPopUpUnique>
<ChartPopUpUnique xcoordinate={this.state.xcoordinate} ycoordinate={this.state.ycoordinate}>
<PopUpElements onClick={this.gotoTracesHandler}>View Traces</PopUpElements>
<PopUpElements onClick={this.gotoAlertsHandler}>Set Alerts</PopUpElements>
</ChartPopUpUnique>
)
}
else
return null;
}
render(){
const ndata = this.props.data;
// console.log("in chartJS line render function")
// console.log(ndata);
// const data_charts = data.map( s => ({label:s.ts, value:parseFloat(s.val)}) );
// if(this.chartRef.ctx)
// {
// var gradient = this.chartRef.ctx.createLinearGradient(0, 0, 0, 400);
// gradient.addColorStop(0, 'rgba(250,174,50,1)');
// gradient.addColorStop(1, 'rgba(250,174,50,0)');
// }
const data_chartJS = (canvas:any) => {
const ctx = canvas.getContext("2d");
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(0, 'rgba(250,174,50,1)');
gradient.addColorStop(1, 'rgba(250,174,50,1)');
return{labels: ndata.map(s => new Date(s.timestamp/1000000)),
datasets: [{
label: 'Request per sec',
data: ndata.map(s => s.callRate),
// backgroundColor:'#000000',
// fill: true,
// backgroundColor: gradient,
pointRadius: 0.5,
borderColor: 'rgba(250,174,50,1)', // Can also add transparency in border color
borderWidth: 2,
},
]}
};
return(
<div>
{this.GraphTracePopUp()}
<ChartJSLine ref={this.chartRef} data={data_chartJS} options={this.options_charts} />
</div>
);
}
}
export default withRouter(RequestRateChart);

View File

@ -0,0 +1,98 @@
import React,{useEffect} from 'react';
import { Tabs, Card, Row, Col} from 'antd';
import { connect } from 'react-redux';
import { useParams } from "react-router-dom";
import { getServicesMetrics, metricItem, getTopEndpoints, topEndpointListItem, GlobalTime } from '../../actions';
import { StoreState } from '../../reducers'
import LatencyLineChart from "./LatencyLineChart"
import RequestRateChart from './RequestRateChart'
import ErrorRateChart from './ErrorRateChart'
import TopEndpointsTable from './TopEndpointsTable';
const { TabPane } = Tabs;
interface ServicesMetricsProps {
serviceMetrics: metricItem[],
getServicesMetrics: Function,
topEndpointsList: topEndpointListItem[],
getTopEndpoints: Function,
globalTime: GlobalTime,
}
const _ServiceMetrics = (props: ServicesMetricsProps) => {
const params = useParams<{ servicename?: string; }>();
console.log('service name',params.servicename);
useEffect( () => {
props.getServicesMetrics(params.servicename,props.globalTime);
props.getTopEndpoints(params.servicename,props.globalTime);
}, [props.globalTime,params.servicename]);
return (
<Tabs defaultActiveKey="1">
<TabPane tab="Application Metrics" key="1">
<Row gutter={32} style={{ margin: 20 }}>
<Col span={12} >
<Card bodyStyle={{padding:10}}>
<LatencyLineChart data={props.serviceMetrics} />
</Card>
</Col>
<Col span={12}>
<Card bodyStyle={{padding:10}}>
<RequestRateChart data={props.serviceMetrics} />
</Card>
</Col>
</Row>
<Row gutter={32} style={{ margin: 20 }}>
<Col span={12}>
<Card bodyStyle={{padding:10}}>
<ErrorRateChart data={props.serviceMetrics} />
</Card>
</Col>
<Col span={12}>
<Card bodyStyle={{padding:10}}>
<TopEndpointsTable data={props.topEndpointsList} />
</Card>
</Col>
</Row>
</TabPane>
<TabPane tab="External Calls" key="2">
<div style={{ margin: 20 }}> Coming Soon.. </div>
<div className="container" style={{ display: 'flex', flexFlow: 'column wrap' }}>
<div className='row'>
<div className='col-md-6 col-sm-12 col-12'>
{/* <ChartJSLineChart data={this.state.graphData} /> */}
</div>
<div className='col-md-6 col-sm-12 col-12'>
{/* <ChartJSLineChart data={this.state.graphData} /> */}
</div>
</div>
</div>
</TabPane>
</Tabs>
);
}
const mapStateToProps = (state: StoreState): { serviceMetrics: metricItem[], topEndpointsList: topEndpointListItem[],globalTime: GlobalTime} => {
return { serviceMetrics : state.serviceMetrics, topEndpointsList: state.topEndpointsList, globalTime:state.globalTime};
};
// the name mapStateToProps is only a convention
// take state and map it to props which are accessible inside this component
export const ServiceMetrics = connect(mapStateToProps, {
getServicesMetrics: getServicesMetrics,
getTopEndpoints: getTopEndpoints,
})(_ServiceMetrics);

View File

@ -0,0 +1 @@
export { ServiceMetrics as default } from './ServiceMetrics';

View File

@ -0,0 +1,99 @@
import React, {useEffect} from 'react';
import { useLocation } from "react-router-dom";
import { NavLink } from 'react-router-dom'
import { Table } from 'antd';
import styled from 'styled-components';
import { connect } from 'react-redux';
import { getServicesList, GlobalTime, servicesListItem } from '../../actions';
import { StoreState } from '../../reducers'
interface ServicesTableProps {
servicesList: servicesListItem[],
getServicesList: Function,
globalTime: GlobalTime,
}
const Wrapper = styled.div`
padding-top:40px;
padding-bottom:40px;
padding-left:40px;
padding-right:40px;
.ant-table table { font-size: 12px; };
.ant-table tfoot>tr>td, .ant-table tfoot>tr>th, .ant-table-tbody>tr>td, .ant-table-thead>tr>th { padding: 10px; };
`;
//styling antd with styled components - https://codesandbox.io/s/8x1r670rxj
const columns = [
{
title: 'Application',
dataIndex: 'serviceName',
key: 'serviceName',
render: (text :string) => <NavLink style={{textTransform:'capitalize'}} to={'/application/' + text}><strong>{text}</strong></NavLink>,
},
{
title: 'P99 latency (in ms)',
dataIndex: 'p99',
key: 'p99',
sorter: (a:any, b:any) => a.p99 - b.p99,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'Error Rate (in %)',
dataIndex: 'errorRate',
key: 'errorRate',
sorter: (a:any, b:any) => a.errorRate - b.errorRate,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value*100).toFixed(2),
},
{
title: 'Requests Per Second',
dataIndex: 'callRate',
key: 'callRate',
sorter: (a:any, b:any) => a.callRate - b.callRate,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => value.toFixed(2),
},
];
const _ServicesTable = (props: ServicesTableProps) => {
const search = useLocation().search;
const time_interval = new URLSearchParams(search).get('time');
console.log(time_interval)
useEffect( () => {
props.getServicesList(props.globalTime);
}, [props.globalTime]);
return(
<Wrapper>
{console.log(props.servicesList)}
<Table dataSource={props.servicesList} columns={columns} pagination={false} />
</Wrapper>
);
}
const mapStateToProps = (state: StoreState): { servicesList: servicesListItem[], globalTime: GlobalTime } => {
// console.log(state);
return { servicesList : state.servicesList, globalTime:state.globalTime};
};
// the name mapStateToProps is only a convention
// take state and map it to props which are accessible inside this component
export const ServicesTable = connect(mapStateToProps, {
getServicesList: getServicesList,
})(_ServicesTable);

View File

@ -0,0 +1 @@
export { ServicesTable as default } from './ServicesTable';

View File

@ -0,0 +1,81 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Table } from 'antd'
import styled from 'styled-components';
import { topEndpointListItem } from '../../actions/metrics';
const Wrapper = styled.div`
padding-top:10px;
padding-bottom:10px;
padding-left:20px;
padding-right:20px;
.ant-table table { font-size: 12px; };
.ant-table tfoot>tr>td, .ant-table tfoot>tr>th, .ant-table-tbody>tr>td, .ant-table-thead>tr>th { padding: 10px; };
`;
interface TopEndpointsTableProps {
data : topEndpointListItem[],
}
const TopEndpointsTable = (props: TopEndpointsTableProps) => {
const columns: any = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
// sorter: (a:any, b:any) => a.startTime - b.startTime,
// sortDirections: ['descend', 'ascend'],
//PNOTE - TO DO - Change this to API link if available
render: (text :string) => <NavLink to={'/' + text}>{text}</NavLink>,
},
{
title: 'P50 (in ms)',
dataIndex: 'p50',
key: 'p50',
sorter: (a:any, b:any) => a.p50 - b.p50,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'P90 (in ms)',
dataIndex: 'p90',
key: 'p90',
sorter: (a:any, b:any) => a.p90 - b.p90,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'P99 (in ms)',
dataIndex: 'p99',
key: 'p99',
sorter: (a:any, b:any) => a.p99 - b.p99,
// sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'Number of Calls',
dataIndex: 'numCalls',
key: 'numCalls',
sorter: (a:any, b:any) => a.numCalls - b.numCalls,
// sortDirections: ['descend', 'ascend'],
// render: (value: number) => value.toFixed(2),
},
];
return(
<Wrapper>
<h6> Top Endpoints</h6>
<Table dataSource={props.data} columns={columns} pagination={false} />
</Wrapper>
)
}
export default TopEndpointsTable;

View File

@ -0,0 +1,67 @@
import React from "react";
// import {useState} from "react";
import Graph from "react-graph-vis";
// import { graphEvents } from "react-graph-vis";
//PNOTE - types of react-graph-vis defined in typings folder.
//How is it imported directly?
// type definition for service graph - https://github.com/crubier/react-graph-vis/issues/80
// Set shapes - https://visjs.github.io/vis-network/docs/network/nodes.html#
// https://github.com/crubier/react-graph-vis/issues/93
const graph = {
nodes: [
{ id: 1, label: "Catalogue", shape: "box", color: "green",border: "black",size: 100 },
{ id: 2, label: "Users", shape: "box", color: "#FFFF00" },
{ id: 3, label: "Payment App", shape: "box", color: "#FB7E81" },
{ id: 4, label: "My Sql", shape: "box", size: 10, color: "#7BE141" },
{ id: 5, label: "Redis-db", shape: "box", color: "#6E6EFD" },
],
edges: [
{from:1,to:2,color: { color: "red" },size:{size:20}},
{from:2,to:3,color: { color: "red" }},
{from:1,to:3,color: { color: "red" }},
{from:3,to:4,color: { color: "red" }},
{from:3,to:5,color: { color: "red" }},
]
};
const options = {
layout: {
hierarchical: true
},
edges: {
color: "#000000"
},
height: "500px"
};
// const events = {
// select: function(event:any) { //PNOTE - TO DO - Get rid of any type
// var { nodes, edges } = event;
// }
// };
const ServiceGraph = () => {
// const [network, setNetwork] = useState(null);
return (
<React.Fragment>
<div> Service Graph Component</div>
<Graph
graph={graph}
options={options}
// events={events}
// getNetwork={network => {
// // if you want access to vis.js network api you can set the state in a parent component using this property
// }}
/>
</React.Fragment>
);
}
export default ServiceGraph;

View File

@ -0,0 +1,19 @@
import React from 'react';
import ServiceGraph from './ServiceGraph'
const ServiceMap = () => {
return (
<div>
{/* <div> Service Map </div> */}
<ServiceGraph />
</div>
);
}
export default ServiceMap;

View File

@ -0,0 +1,80 @@
import React from 'react';
import { Card, Tag } from 'antd';
import { connect } from 'react-redux';
import { StoreState } from '../../reducers'
import { TagItem, TraceFilters, updateTraceFilters } from '../../actions';
interface FilterStateDisplayProps {
traceFilters: TraceFilters,
updateTraceFilters: Function,
}
const _FilterStateDisplay = (props: FilterStateDisplayProps) => {
function handleCloseTag(value:string) {
console.log('on close tag', value)
if (value==='service')
props.updateTraceFilters({...props.traceFilters,service:''})
if (value==='operation')
props.updateTraceFilters({...props.traceFilters,operation:''})
if (value==='maxLatency')
props.updateTraceFilters({...props.traceFilters,latency:{'max':'','min':props.traceFilters.latency?.min}})
if (value==='minLatency')
props.updateTraceFilters({...props.traceFilters,latency:{'min':'','max':props.traceFilters.latency?.max}})
}
function handleCloseTagElement(item:TagItem){
console.log('tag item closed in handle closeTagElement', item)
props.updateTraceFilters({...props.traceFilters,tags:props.traceFilters.tags?.filter(elem => elem !== item)})
}
return(
<Card style={{padding: 6, marginTop: 10, marginBottom: 10}} bodyStyle={{padding: 6}}>
{(props.traceFilters.service===''||props.traceFilters.operation===undefined)? null:
<Tag style={{fontSize:14, padding: 8}} closable
onClose={e => {handleCloseTag('service');}}>
service:{props.traceFilters.service}
</Tag> }
{(props.traceFilters.operation===''||props.traceFilters.operation===undefined)? null:
<Tag style={{fontSize:14, padding: 8}} closable
onClose={e => {handleCloseTag('operation');}}>
operation:{props.traceFilters.operation}
</Tag> }
{props.traceFilters.latency===undefined||props.traceFilters.latency?.min===''? null:
<Tag style={{fontSize:14, padding: 8}} closable
onClose={e => {handleCloseTag('minLatency');}}>
minLatency:{(parseInt(props.traceFilters.latency!.min)/1000000).toString()}ms
</Tag> }
{props.traceFilters.latency===undefined||props.traceFilters.latency?.max===''? null:
<Tag style={{fontSize:14, padding: 8}} closable
onClose={e => {handleCloseTag('maxLatency');}}>
maxLatency:{(parseInt(props.traceFilters.latency!.max)/1000000).toString()}ms
</Tag> }
{console.log('tagfilters before showing on card',props.traceFilters.tags)}
{props.traceFilters.tags === undefined? null: props.traceFilters.tags.map( item => (
<Tag style={{fontSize:14, padding: 8}} closable
onClose={e => {handleCloseTagElement(item);}}>
{item.key} {item.operator} {item.value}
</Tag>))}
</Card>
);
}
const mapStateToProps = (state: StoreState): { traceFilters: TraceFilters } => {
return { traceFilters : state.traceFilters };
};
export const FilterStateDisplay = connect(mapStateToProps,
{
updateTraceFilters: updateTraceFilters,
})(_FilterStateDisplay);

View File

@ -0,0 +1,72 @@
import React from 'react';
import { Modal, Form, InputNumber, Col, Row} from 'antd';
import { Store } from 'antd/lib/form/interface';
// interface Values {
// title: string;
// description: string;
// modifier: string;
// }
interface LatencyModalFormProps {
visible: boolean;
onCreate: (values: Store) => void; //Store is defined in antd forms library
onCancel: () => void;
}
const LatencyModalForm: React.FC<LatencyModalFormProps> = ({
visible,
onCreate,
onCancel,
}) => {
const [form] = Form.useForm();
return (
<Modal
visible={visible}
title="Chose min and max values of Latency"
okText="Apply"
cancelText="Cancel"
onCancel={onCancel}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
// onCreate({title:"hello",description:'good',modifier:'public'});
onCreate(values); // giving error for values
})
.catch(info => {
console.log('Validate Failed:', info);
});
}}
>
<Form
form={form}
layout="horizontal"
name="form_in_modal"
initialValues={{ min: '100', max:'500' }}
>
<Row>
{/* <Input.Group compact> */}
<Col span={12}>
<Form.Item
name="min"
label="Min (in ms)"
// rules={[{ required: true, message: 'Please input the title of collection!' }]}
>
<InputNumber />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="max" label="Max (in ms)">
<InputNumber />
</Form.Item>
</Col>
</Row>
{/* </Input.Group> */}
</Form>
</Modal>
);
};
export default LatencyModalForm;

View File

@ -0,0 +1,47 @@
import React from 'react';
import {Card,Tabs} from 'antd';
const { TabPane } = Tabs;
interface spanTagItem {
key:string;
type:string;
value:string;
}
interface SelectedSpanDetailsProps {
clickedSpanTags: spanTagItem[]
}
const SelectedSpanDetails = (props: SelectedSpanDetailsProps) => {
const callback = (key:any) => {
console.log(key);
}
return (
<Card style={{ height: 320 }} >
<Tabs defaultActiveKey="1" onChange={callback}>
<TabPane tab="Tags" key="1">
<strong> Details for selected Span </strong>
{props.clickedSpanTags.map((tags,index)=><li key={index} style={{color:'grey',fontSize:'13px',listStyle:'none'}}><span className='mr-1'>{tags.key}</span>:<span className='ml-1'>{tags.key==='error'?"true":tags.value}</span></li>)} </TabPane>
<TabPane tab="Errors" key="2">
{props.clickedSpanTags.filter(tags=>tags.key==='error').map(
error => <div className='ml-5'>
<p style={{color:'grey',fontSize:'10px'}}>
<span className='mr-1'>{error.key}</span>:
<span className='ml-1'>true</span></p>
</div>)}
</TabPane>
</Tabs>
</Card>
);
}
export default SelectedSpanDetails;

View File

@ -0,0 +1,252 @@
import React, {useState,useEffect} from 'react';
import GenericVisualizations from '../metrics/GenericVisualization'
import {Select, Card, Space, Form} from 'antd';
import { connect } from 'react-redux';
import { StoreState } from '../../reducers'
import {customMetricsItem, getFilteredTraceMetrics, GlobalTime, TraceFilters} from '../../actions';
const { Option } = Select;
const entity = [
{
title: 'Calls',
key:'calls',
dataindex:'calls'
},
{
title: 'Duration',
key:'duration',
dataindex:'duration'
},
{
title: 'Error',
key:'error',
dataindex:'error'
},
{
title: 'Status Code',
key:'status_code',
dataindex:'status_code'
},
];
const aggregation_options = [
{
linked_entity: 'calls',
default_selected:{title:'Count', dataindex:'count'},
options_available: [ {title:'Count', dataindex:'count'}, {title:'Rate (per sec)', dataindex:'rate_per_sec'}]
},
{
linked_entity: 'duration',
default_selected:{title:'p99', dataindex:'p99'},
// options_available: [ {title:'Avg', dataindex:'avg'}, {title:'Max', dataindex:'max'},{title:'Min', dataindex:'min'}, {title:'p50', dataindex:'p50'},{title:'p90', dataindex:'p90'}, {title:'p95', dataindex:'p95'}]
options_available: [ {title:'p50', dataindex:'p50'},{title:'p90', dataindex:'p90'}, {title:'p99', dataindex:'p99'}]
},
{
linked_entity: 'error',
default_selected:{title:'Count', dataindex:'count'},
options_available: [ {title:'Count', dataindex:'count'}, {title:'Rate (per sec)', dataindex:'rate_per_sec'}]
},
{
linked_entity: 'status_code',
default_selected: {title:'Count', dataindex:'count'},
options_available: [ {title:'Count', dataindex:'count'}]
},
];
interface TraceCustomVisualizationsProps {
filteredTraceMetrics: customMetricsItem[],
globalTime: GlobalTime,
getFilteredTraceMetrics: Function,
traceFilters: TraceFilters,
}
const _TraceCustomVisualizations = (props: TraceCustomVisualizationsProps) => {
const [selectedEntity, setSelectedEntity] = useState('calls');
// const [defaultOption, setDefaultOption]=useState('count');
const [selectedAggOption, setSelectedAggOption] = useState('count');
const [selectedStep, setSelectedStep] = useState('60');
// Step should be multiples of 60, 60 -> 1 min
useEffect( () => {
let request_string= 'service='+props.traceFilters.service+
'&operation='+props.traceFilters.operation+
'&maxDuration='+props.traceFilters.latency?.max+
'&minDuration='+props.traceFilters.latency?.min
if(props.traceFilters.tags)
request_string=request_string+'&tags='+encodeURIComponent(JSON.stringify(props.traceFilters.tags));
if(selectedEntity)
request_string=request_string+'&dimension='+selectedEntity.toLowerCase();
if(selectedAggOption)
request_string=request_string+'&aggregation_option='+selectedAggOption.toLowerCase();
if(selectedStep)
request_string=request_string+'&step='+selectedStep;
props.getFilteredTraceMetrics(request_string, props.globalTime)
}, [selectedEntity,selectedAggOption,props.traceFilters, props.globalTime ]);
//Custom metrics API called if time, tracefilters, selected entity or agg option changes
const [form] = Form.useForm();
function handleChange(value:string) {
console.log(value);
}
// function handleChangeEntity(value:string) {
// // console.log(value);
// // setSelectedEntity(value);
// setDefaultOption(aggregation_options.filter((item) => item.linked_entity === selectedEntity)[0].default_selected)
// }
function handleFinish(value:string) {
console.log(value);
}
// PNOTE - Can also use 'coordinate' option in antd Select for implementing this - https://ant.design/components/select/
const handleFormValuesChange = (changedValues:any) => {
const formFieldName = Object.keys(changedValues)[0];
// console.log('Object keys',Object.keys(changedValues) );
if (formFieldName === 'entity') {
// const a = selectedEntity; // why is selected entity not set instantly??
// setDefaultOption(aggregation_options.filter((item) => item.linked_entity === selectedEntity)[0].default_selected)
const temp_entity = aggregation_options.filter((item) => item.linked_entity === changedValues[formFieldName])[0];
form.setFieldsValue( {
// agg_options : aggregation_options.filter((item) => item.linked_entity === selectedEntity)[0].default_selected,
agg_options : temp_entity.default_selected.title,
// PNOTE - TO DO Check if this has the same behaviour as selecting an option?
})
let temp = form.getFieldsValue(['agg_options','entity']);
console.log('custom metric field values',temp,temp.entity,temp.agg_options);
setSelectedEntity(temp.entity);
setSelectedAggOption(temp.agg_options);
//form.setFieldsValue({ agg_options: aggregation_options.filter( item => item.linked_entity === selectedEntity )[0] }); //reset product selection
// PNOTE - https://stackoverflow.com/questions/64377293/update-select-option-list-based-on-other-select-field-selection-ant-design
}
if (formFieldName === 'agg_options') {
setSelectedAggOption(changedValues[formFieldName]);
}
// Make api calls here and display data
// PNOTE - TO DO - API CALL location - may be through action creators and set states in redux store?
// PNOTE - Change this
// API call via useEffects after monitoring traces
}
return (
<Card>
{/* <Space direction="vertical"> */}
<div>Custom Visualizations</div>
<Form
form={form}
onFinish={handleFinish}
onValuesChange={handleFormValuesChange}
// initialValues={ agg_options: 'Count'}
initialValues={{ agg_options: 'Count', chart_style:'line', interval:'5m', group_by:'none' }}
>
<Space>
<Form.Item name="entity">
{/* <Select defaultValue={selectedEntity} style={{ width: 120 }} onChange={handleChangeEntity} allowClear> */}
<Select defaultValue={selectedEntity} style={{ width: 120 }} allowClear>
{entity.map((item) => (
<Option key={item.key} value={item.dataindex}>
{item.title}
</Option>
)
)
}
</Select>
</Form.Item>
<Form.Item name="agg_options">
<Select style={{ width: 120 }} allowClear>
{ aggregation_options.filter((item) => item.linked_entity === selectedEntity)[0].options_available
.map((item) => (
<Option key={item.dataindex} value={item.dataindex}>
{item.title}
</Option>
))
}
</Select>
</Form.Item>
{/* <Select defaultValue="count" style={{ width: 120 }} onChange={handleChange} allowClear>
<Option value="count">Count</Option>
<Option value="sume">Sum</Option>
<Option value="rate">Rate</Option>
</Select> */}
<Form.Item name="chart_style">
<Select style={{ width: 120 }} onChange={handleChange} allowClear>
<Option value="line">Line Chart</Option>
<Option value="bar">Bar Chart</Option>
<Option value="area">Area Chart</Option>
</Select>
</Form.Item>
<Form.Item name="interval">
<Select style={{ width: 120 }} onChange={handleChange} allowClear>
<Option value="1m">1 min</Option>
<Option value="5m">5 min</Option>
<Option value="30m">30 min</Option>
</Select>
</Form.Item>
{/* Need heading for each option */}
<Form.Item name="group_by">
<Select style={{ width: 120 }} onChange={handleChange} allowClear>
<Option value="none">Group By</Option>
<Option value="status">Status Code</Option>
<Option value="protocol">Protocol</Option>
</Select>
</Form.Item>
</Space>
</Form>
<GenericVisualizations chartType='line' data={props.filteredTraceMetrics}/>
{/* This component should take bar or line as an input */}
</Card>
);
}
const mapStateToProps = (state: StoreState): { filteredTraceMetrics: customMetricsItem[] , globalTime: GlobalTime, traceFilters: TraceFilters} => {
// console.log(state);
return { filteredTraceMetrics : state.filteredTraceMetrics, globalTime: state.globalTime,traceFilters:state.traceFilters };
};
// the name mapStateToProps is only a convention
// take state and map it to props which are accessible inside this component
export const TraceCustomVisualizations = connect(mapStateToProps, {
getFilteredTraceMetrics: getFilteredTraceMetrics,
})(_TraceCustomVisualizations);
// export default TraceCustomVisualizations;

View File

@ -0,0 +1,25 @@
import React from 'react';
import {TraceCustomVisualizations} from './TraceCustomVisualizations';
import { TraceFilter } from './TraceFilter';
import { TraceList } from './TraceList';
const TraceDetail = () => {
// const [serviceName, setServiceName] = useState('Frontend'); //default value of service name
return (
<div>
{/* <div>Tracing Detail Page</div> */}
<TraceFilter />
{/* <TraceFilter servicename={serviceName} /> */}
<TraceCustomVisualizations />
<TraceList />
</div>
);
}
export default TraceDetail;

View File

@ -0,0 +1,339 @@
import React,{useEffect, useState} from 'react';
import { Select, Button, Input, Tag, Card, Form, AutoComplete} from 'antd';
import { connect } from 'react-redux';
import { Store } from 'antd/lib/form/interface';
import styled from 'styled-components';
import { updateTraceFilters, fetchTraces, TraceFilters, GlobalTime } from '../../actions';
import { StoreState } from '../../reducers';
import LatencyModalForm from './LatencyModalForm';
import {FilterStateDisplay} from './FilterStateDisplay';
import FormItem from 'antd/lib/form/FormItem';
import metricsAPI from '../../api/metricsAPI';
const { Option } = Select;
const InfoWrapper = styled.div`
padding-top:10px;
font-style:italic;
font-size: 12px;
`;
interface TraceFilterProps {
traceFilters: TraceFilters,
globalTime: GlobalTime,
updateTraceFilters: Function,
fetchTraces: Function,
}
interface TagKeyOptionItem {
"tagKeys": string;
"tagCount": number;
}
const _TraceFilter = (props: TraceFilterProps) => {
const [serviceList, setServiceList] = useState<string[]>([]);
const [operationList, setOperationsList] = useState<string[]>([]);
const [tagKeyOptions, setTagKeyOptions] = useState<TagKeyOptionItem[]>([]);
useEffect( () => {
metricsAPI.get<string[]>('services/list').then(response => {
// console.log(response.data);
setServiceList( response.data );
});
}, []);
useEffect( () => {
let request_string='service='+props.traceFilters.service+
'&operation='+props.traceFilters.operation+
'&maxDuration='+props.traceFilters.latency?.max+
'&minDuration='+props.traceFilters.latency?.min
if(props.traceFilters.tags)
request_string=request_string+'&tags='+encodeURIComponent(JSON.stringify(props.traceFilters.tags));
props.fetchTraces(props.globalTime, request_string)
console.log('stringified traceFilters redux state',encodeURIComponent(JSON.stringify(props.traceFilters.tags)));
}, [props.traceFilters,props.globalTime]);
// useEffect( () => {
// tagKeyOptions.map(s => options.push({'value':s.tagKeys}));
// console.log('tagoptions USeEffect',options)
// }, [tagKeyOptions]);
// Use effects run in the order they are specified
useEffect ( () => {
let latencyButtonText = 'Latency';
if (props.traceFilters.latency?.min === '' && props.traceFilters.latency?.max !== '')
latencyButtonText = 'Latency<'+(parseInt(props.traceFilters.latency?.max)/1000000).toString()+'ms';
else if (props.traceFilters.latency?.min !== '' && props.traceFilters.latency?.max === '')
latencyButtonText = 'Latency>'+(parseInt(props.traceFilters.latency?.min)/1000000).toString()+'ms';
else if ( props.traceFilters.latency !== undefined && props.traceFilters.latency?.min !== '' && props.traceFilters.latency?.max !== '')
latencyButtonText = (parseInt(props.traceFilters.latency.min)/1000000).toString()+'ms <Latency<'+(parseInt(props.traceFilters.latency.max)/1000000).toString()+'ms';
form_basefilter.setFieldsValue({latency:latencyButtonText ,})
}, [props.traceFilters.latency])
useEffect ( () => {
form_basefilter.setFieldsValue({service: props.traceFilters.service,})
}, [props.traceFilters.service])
useEffect ( () => {
form_basefilter.setFieldsValue({operation: props.traceFilters.operation,})
}, [props.traceFilters.operation])
const [modalVisible, setModalVisible] = useState(false);
const [loading] = useState(false);
const [tagKeyValueApplied, setTagKeyValueApplied]=useState(['']);
const [latencyFilterValues, setLatencyFilterValues]=useState({min:'',max:''})
const [form] = Form.useForm();
const [form_basefilter] = Form.useForm();
function handleChange(value:string) {
console.log(value);
}
function handleChangeOperation(value:string) {
props.updateTraceFilters({...props.traceFilters,operation:value})
}
function handleChangeService(value:string) {
console.log(value);
let service_request='/service/'+value+'/operations';
metricsAPI.get<string[]>(service_request).then(response => {
console.log('operations',response.data);
// form_basefilter.resetFields(['operation',])
setOperationsList( response.data );
});
let tagkeyoptions_request='tags?service='+value;
metricsAPI.get<TagKeyOptionItem[]>(tagkeyoptions_request).then(response => {
console.log('tag key options',response.data);
setTagKeyOptions( response.data );
});
props.updateTraceFilters({...props.traceFilters,service:value})
}
const onLatencyButtonClick = () => {
setModalVisible(true);
}
const onLatencyModalApply = (values: Store) => {
setModalVisible(false);
console.log('Received values of form: ', values);
//Latency modal form returns null if the value entered is not a number or string etc or empty
props.updateTraceFilters({...props.traceFilters,latency:{min:values.min?(parseInt(values.min)*1000000).toString():"", max:values.max?(parseInt(values.max)*1000000).toString():""}})
// setLatencyFilterValues()
}
const onTagFormSubmit = (values:any) => {
console.log(values);
// setTagKeyValueApplied(tagKeyValueApplied => [...tagKeyValueApplied, values.tag_key+' '+values.operator+' '+values.tag_value]);
// making API calls with only tags data for testing, for last tagform submit
// ideally all tags including service, operations, tags, latency selected should be in one state and used
// to make API call and update trace list
let request_tags= 'service=frontend&tags='+encodeURIComponent(JSON.stringify([{"key":values.tag_key,"value":values.tag_value,"operator":values.operator}]))
// props.fetchTraces(request_tags)
// form field names are tag_key & tag_value
// data structure has key, value & operator
if (props.traceFilters.tags){ // If there are existing tag filters present
props.updateTraceFilters(
{
service:props.traceFilters.service,
operation:props.traceFilters.operation,
latency:props.traceFilters.latency,
tags:[...props.traceFilters.tags, {'key':values.tag_key,'value':values.tag_value,'operator':values.operator}]
});
}
else
{
props.updateTraceFilters(
{
service:props.traceFilters.service,
operation:props.traceFilters.operation,
latency:props.traceFilters.latency,
tags:[ {'key':values.tag_key,'value':values.tag_value,'operator':values.operator}]
});
}
form.resetFields();
}
const onTagClose = (value:string) => {
console.log(value);
// setJoinList(joinList.filter((e)=>(e !== name)))
// removing closed tag from the tagKeyValueApplied array
setTagKeyValueApplied(tagKeyValueApplied.filter( e => (e !== value)));
}
// For autocomplete
//Setting value when autocomplete field is changed
const onChangeTagKey = (data: string) => {
form.setFieldsValue({ tag_key: data });
};
const dataSource = ['status:200'];
const children = [];
for (let i = 0; i < dataSource.length; i++) {
children.push(<Option value={dataSource[i]} key={dataSource[i]}>{dataSource[i]}</Option>);
}
// PNOTE - Remove any
const handleApplyFilterForm = (values:any) => {
console.log('values are', values);
console.log(typeof(values.service))
console.log(typeof(values.operation))
let request_params: string ='';
if (typeof values.service !== undefined && typeof(values.operation) !== undefined)
{
request_params = 'service='+values.service+'&operation='+values.operation;
}
else if (typeof values.service === undefined && typeof values.operation !== undefined)
{
request_params = 'operation='+values.operation;
}
else if (typeof values.service !== undefined && typeof values.operation === undefined)
{
request_params = 'service='+values.service;
}
request_params=request_params+'&minDuration='+latencyFilterValues.min+'&maxDuration='+latencyFilterValues.max;
console.log(request_params);
// props.fetchTraces(request_params)
// console.log(props.inputTag)
// props.updateTagFilters([{key:props.inputTag, value: props.inputTag }]);
setTagKeyValueApplied(tagKeyValueApplied => [...tagKeyValueApplied, 'service eq'+values.service, 'operation eq '+values.operation, 'maxduration eq '+ (parseInt(latencyFilterValues.max)/1000000).toString(), 'minduration eq '+(parseInt(latencyFilterValues.min)/1000000).toString()]);
props.updateTraceFilters({'service':values.service,'operation':values.operation,'latency':latencyFilterValues})
}
return (
<div>
<div>Filter Traces</div>
<div>{JSON.stringify(props.traceFilters)}</div>
<Form form={form_basefilter} layout='inline' onFinish={handleApplyFilterForm} initialValues={{ service:'', operation:'',latency:'Latency',}} style={{marginTop: 10, marginBottom:10}}>
<FormItem rules={[{ required: true }]} name='service'>
<Select showSearch style={{ width: 180 }} onChange={handleChangeService} placeholder='Select Service' allowClear>
{serviceList.map( s => <Option value={s}>{s}</Option>)}
</Select>
</FormItem>
<FormItem name='operation'>
<Select showSearch style={{ width: 180 }} onChange={handleChangeOperation} placeholder='Select Operation' allowClear>
{operationList.map( item => <Option value={item}>{item}</Option>)}
{/* We need to URL encode before making API call on form submission */}
{/* <Option value="HTTP%20GET">HTTP GET</Option>
<Option value="HTTP%20GET%20%2Fdispatch">HTTP GET /dispatch</Option>
<Option value="HTTP%20GET%3A%20%2Froute">HTTP GET: /route</Option>
<Option value="%2Fdriver.DriverService%2FFindNearest">/driver.DriverService/FindNearest</Option>
<Option value="HTTP%20GET%20%2Fcustomer">HTTP GET /customer</Option> */}
</Select>
</FormItem>
<FormItem name='latency'>
<Input style={{ width: 200 }} type='button' onClick={onLatencyButtonClick}/>
</FormItem>
{/* <FormItem>
<Button type="primary" htmlType="submit">Apply Filters</Button>
</FormItem> */}
</Form>
<FilterStateDisplay />
{/* <Card style={{padding: 6, marginTop: 10, marginBottom: 10}} bodyStyle={{padding: 6}}>
<Tag style={{fontSize:14, padding: 8}} closable> status:200 </Tag><Tag style={{fontSize:14, padding: 8}} closable> customerid:123 </Tag>
{tagKeyValueApplied.map( item => <Tag key={item} style={{fontSize:14, padding: 8}} onClose={() => onTagClose(item)} closable> {item} </Tag>)}
</Card> */}
{/* // What will be the empty state of card when there is no Tag , it should show something */}
<InfoWrapper>Select Service to get Tag suggestions </InfoWrapper>
<Form form={form} layout='inline' onFinish={onTagFormSubmit} initialValues={{operator:'equals'}} style={{marginTop: 10, marginBottom:10}}>
<FormItem rules={[{ required: true }]} name='tag_key'>
{/* <Input style={{ width: 160, textAlign: 'center' }} placeholder="Tag Key" /> */}
{/* Not using tag count data to show in options */}
<AutoComplete
options={tagKeyOptions.map((s) => { return ({'value' : s.tagKeys}) })}
style={{ width: 200, textAlign: 'center' }}
// onSelect={onSelect}
// onSearch={onSearch}
onChange={onChangeTagKey}
filterOption={(inputValue, option) =>
option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
placeholder="Tag Key"
/>
{/* // ! means that we are saying object can't be undefined */}
</FormItem>
<FormItem name='operator'>
<Select style={{ width: 120, textAlign: 'center' }}>
<Option value="equals">EQUAL</Option>
<Option value="contains">CONTAINS</Option>
{/* <Option value="not-in">NOT IN</Option> */}
</Select>
</FormItem>
<FormItem rules={[{ required: true }]} name='tag_value'>
<Input style={{ width: 160, textAlign: 'center',}} placeholder="Tag Value" />
</FormItem>
<FormItem>
<Button type="primary" htmlType="submit"> Apply Tag Filter </Button>
</FormItem>
</Form>
<LatencyModalForm
visible={modalVisible}
onCreate={onLatencyModalApply}
onCancel={() => {
setModalVisible(false);
}}
/>
</div>
);
}
const mapStateToProps = (state: StoreState): { traceFilters: TraceFilters, globalTime: GlobalTime } => {
// console.log(state);
return { traceFilters: state.traceFilters, globalTime: state.globalTime };
};
export const TraceFilter = connect(mapStateToProps, {
updateTraceFilters: updateTraceFilters,
fetchTraces: fetchTraces,
})(_TraceFilter);

View File

@ -0,0 +1,49 @@
.d3-tip {
line-height: 1;
padding: 2px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
font-size: 6px;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 6px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
/* Style northward tooltips differently */
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
}
/* SVG element */
/* Way to add borders in SVG - https://stackoverflow.com/questions/18330344/how-to-add-border-outline-stroke-to-svg-elements-in-css */
.frame {
fill: none;
stroke: rgba(255, 255, 255, 0.25);
stroke-width: 1;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Transparency simulates sub pixel border https://stackoverflow.com/questions/13891177/css-border-less-than-1px */
.d3-flame-graph-label:hover {
border: 1px dotted;
border-color: rgba(255, 255, 255, 0.75);
}
/*
.d3-flame-graph-label:hover {
border: 1px solid;
border-color: rgba(255, 255, 255, 0.75);
} */

View File

@ -0,0 +1,147 @@
import React, { useEffect, useState } from 'react'
import { useParams } from "react-router-dom";
import { flamegraph } from 'd3-flame-graph'
import { connect } from 'react-redux';
import { Card, Button, Row, Col, Space } from 'antd';
import * as d3 from 'd3';
import * as d3Tip from 'd3-tip';
//import * as d3Tip from 'd3-tip';
// PNOTE - uninstall @types/d3-tip. issues with importing d3-tip https://github.com/Caged/d3-tip/issues/181
// import styled from 'styled-components';
import './TraceGraph.css'
import { spanToTreeUtil } from '../../utils/spanToTree'
import { fetchTraceItem , spansWSameTraceIDResponse } from '../../actions';
import { StoreState } from '../../reducers'
import { TraceGraphColumn } from './TraceGraphColumn'
import SelectedSpanDetails from './SelectedSpanDetails'
interface TraceGraphProps {
traceItem: spansWSameTraceIDResponse ,
fetchTraceItem: Function,
}
const _TraceGraph = (props: TraceGraphProps) => {
const params = useParams<{ id?: string; }>();
const [clickedSpanTags,setClickedSpanTags]=useState([])
useEffect( () => {
console.log('inside initial fetch trace for flamegraph')
props.fetchTraceItem(params.id);
}, []);
useEffect( () => {
if (props.traceItem[0].events.length>0)
{
const tree = spanToTreeUtil(props.traceItem[0].events);
d3.select("#chart").datum(tree).call(chart);
}
},[props.traceItem]);
// if this monitoring of props.traceItem.data is removed then zoom on click doesn't work
// Doesn't work if only do initial check, works if monitor an element - as it may get updated in sometime
// PNOTE - Is this being called multiple times?
//PNOTE - Do we fetch trace data again based on id or do we call again using rest calls
// d3-flame-graph repository -- https://github.com/spiermar/d3-flame-graph
// const tip = d3.tip().attr('class', 'd3-tip').html(function(d) { return d; });
// const tip = d3Tip.default().attr('class', 'd3-flame-graph-tip').html(function(d:any) { console.log(d);return 'Name -> '+d.data.name+'<BR>Duration -> '+d.data.value});
//https://stackoverflow.com/questions/5934928/javascript-return-value-for-tooltip -> How to display tooltip
//const tip = d3Tip.default().attr('class', 'd3-tip').html(function(d:any) { console.log(d);return <FlamegraphTooltip>{d.data.name}</FlamegraphTooltip>});
// var tip = flamegraph.tooltip.defaultFlamegraphTooltip().html(function(d) { return "name: " + d.data.name + ", value: " + d.data.value; });
const tip = d3Tip.default().attr('class', 'd3-tip').html(function(d:any) { return d.data.name+'<br>duration: '+d.data.value});
// PNOTE - Used this example for tooltip styling
const onClick = (z:any) => {
// props.tagsInfo(d.data.tags)
// let new_tags = z.data.tags;
// let new_tags = [{
// key: 'Ankit',
// type: 'testin',
// value: 'Nothing',
// }]
// setClickedSpanTags(new_tags);
// setNum(9);
setClickedSpanTags(z.data.tags);
console.log(`Clicked on ${z.data.name}, id: "${z.id}"`);
}
const chart = flamegraph()
.width(640)
.cellHeight(18)
.transitionDuration(500)
.minFrameSize(5)
.sort(true)
.inverted(true)
.tooltip(tip)
.elided(false)
.onClick(onClick)
// .title("Trace Flame graph")
.differential(false)
.selfValue(true); //sets span width based on value - which is mapped to duration
//PNOTE
// used inverted() instead of passing a function in Sort
// .sort(function(a :pushDStree,b :pushDStree) :number{
// if(a.startTime < b.startTime) return -1;
// else return 1;
// })
//removed transition ease - easeCubic as it was giving type error. d3.easeCubic is the default transition easing function
//Example to sort in reverse order
//.sort(function(a,b){ return d3.descending(a.name, b.name);})
// PNOTE - filter based on traceid - trace should already be in redux-state, will redux-state become very big if all trace reponses are stored in it
//if tree
// d3.select("#chart").datum(tree).call(chart)
const resetZoom = () => {
chart.resetZoom();
}
return (
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
<Col md={8} sm={24} >
<TraceGraphColumn />
</Col>
<Col md={16} sm={24} >
{/* <Card style={{ width: 640 }}> */}
<Space direction="vertical" size='middle' >
<Card bodyStyle={{padding: 80, }} style={{ height: 320, }}>
<div>Trace Graph component ID is {params.id} </div>
<Button type="primary" onClick={resetZoom}>Reset Zoom</Button>
<div id="chart" style={{ fontSize: 12 }}></div>
</Card>
<SelectedSpanDetails clickedSpanTags={clickedSpanTags}/>
</Space>
</Col>
</Row>
);
}
const mapStateToProps = (state: StoreState): { traceItem: spansWSameTraceIDResponse } => {
// console.log(state);
return { traceItem: state.traceItem };
};
// the name mapStateToProps is only a convention
// take state and map it to props which are accessible inside this component
export const TraceGraph = connect(mapStateToProps, {
fetchTraceItem: fetchTraceItem,
})(_TraceGraph);

View File

@ -0,0 +1,83 @@
import React from 'react';
import { connect } from 'react-redux';
import { Table } from 'antd'
import { traceResponseNew, pushDStree } from '../../actions';
import { StoreState } from '../../reducers'
interface TraceGraphColumnProps {
traces: traceResponseNew,
}
interface TableDataSourceItem {
key: string;
operationName: string;
startTime: number;
duration: number;
}
const _TraceGraphColumn = (props: TraceGraphColumnProps) => {
const columns: any = [
{
title: 'Start Time (UTC Time)',
dataIndex: 'startTime',
key: 'startTime',
sorter: (a:any, b:any) => a.startTime - b.startTime,
sortDirections: ['descend', 'ascend'],
render: (value: number) => (new Date(Math.round(value/1000))).toUTCString()
// render: (value: number) => (new Date(Math.round(value/1000))).toLocaleDateString()+' '+(new Date(Math.round(value/1000))).toLocaleTimeString()
},
{
title: 'Duration (in ms)',
dataIndex: 'duration',
key: 'duration',
sorter: (a:any, b:any) => a.duration - b.duration,
sortDirections: ['descend', 'ascend'],
render: (value: number) => (value/1000000).toFixed(2),
},
{
title: 'Operation',
dataIndex: 'operationName',
key: 'operationName',
},
];
let dataSource :TableDataSourceItem[] = [];
// PNOTE - Define new array
// if (props.traces.data.length > 0)
// {
// props.traces.data[0].spans.map((item: spanItem, index ) => dataSource.push({startTime: item.startTime, operationName: item.operationName , duration: item.duration, key:index.toString()}) );
// }
if (props.traces[0].events.length > 0) {
props.traces[0].events.map((item: (number|string|string[]|pushDStree[])[], index ) => {
if (typeof item[0] === 'number' && typeof item[4] === 'string' && typeof item[6] === 'string' && typeof item[1] === 'string' && typeof item[2] === 'string' )
dataSource.push({startTime: item[0], operationName: item[4] , duration:parseInt(item[6]), key:index.toString()});
});
}
return (
<div>
{/* <div>Tracing Graph Column Page</div> */}
<Table dataSource={dataSource} columns={columns} size="middle"/>;
</div>
);
}
const mapStateToProps = (state: StoreState): { traces: traceResponseNew } => {
// console.log(state);
return { traces : state.traces };
};
// the name mapStateToProps is only a convention
// take state and map it to props which are accessible inside this component
export const TraceGraphColumn = connect(mapStateToProps)(_TraceGraphColumn);

View File

@ -0,0 +1,5 @@
export { TraceGraph as default } from './TraceGraph';
// PNOTE
// Because react.lazy doesn't work on named components
// https://reactjs.org/docs/code-splitting.html#:~:text=Named%20Exports,t%20pull%20in%20unused%20components

Some files were not shown because too many files have changed in this diff Show More