Merge pull request #1532 from SigNoz/release/v0.11.0

Release/v0.11.0
This commit is contained in:
Ankit Nayan 2022-08-24 20:11:36 +05:30 committed by GitHub
commit d170515d4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
126 changed files with 7763 additions and 1959 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ package.json
deploy/docker/environment_tiny/common_test
frontend/node_modules
frontend/.pnp
frontend/i18n-translations-hash.json
*.pnp.js
# testing

View File

@ -13,6 +13,8 @@ FRONTEND_DIRECTORY ?= frontend
QUERY_SERVICE_DIRECTORY ?= pkg/query-service
STANDALONE_DIRECTORY ?= deploy/docker/clickhouse-setup
SWARM_DIRECTORY ?= deploy/docker-swarm/clickhouse-setup
LOCAL_GOOS ?= $(shell go env GOOS)
LOCAL_GOARCH ?= $(shell go env GOARCH)
REPONAME ?= signoz
DOCKER_TAG ?= latest
@ -79,11 +81,25 @@ dev-setup:
@echo "--> Local Setup completed"
@echo "------------------"
run-local:
@LOCAL_GOOS=$(LOCAL_GOOS) LOCAL_GOARCH=$(LOCAL_GOARCH) docker-compose -f \
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-local.yaml \
up --build -d
down-local:
@docker-compose -f \
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-local.yaml \
down -v
run-x86:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml up -d
@docker-compose -f \
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-prod.yaml \
up --build -d
down-x86:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml down -v
@docker-compose -f \
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-prod.yaml \
down -v
clear-standalone-data:
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \

View File

@ -40,7 +40,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.10.2
image: signoz/query-service:0.11.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@ -51,10 +51,12 @@ services:
- ./data/signoz/:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
- STORAGE=clickhouse
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-swarm
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
@ -68,7 +70,7 @@ services:
- clickhouse
frontend:
image: signoz/frontend:0.10.2
image: signoz/frontend:0.11.0
deploy:
restart_policy:
condition: on-failure
@ -81,10 +83,14 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.45.1-1.3
image: signoz-otel-collector:0.55.0
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
@ -97,21 +103,15 @@ services:
# - "14268:14268" # Jaeger thrift HTTP
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zPages extension
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
deploy:
mode: replicated
replicas: 3
mode: global
restart_policy:
condition: on-failure
resources:
limits:
memory: 2000m
depends_on:
- clickhouse
otel-collector-metrics:
image: signoz/otelcontribcol:0.45.1-1.3
image: signoz-otel-collector:0.55.0
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -1,4 +1,29 @@
receivers:
filelog/dockercontainers:
include: [ "/var/lib/docker/containers/*/*.log" ]
start_at: end
include_file_path: true
include_file_name: false
operators:
- type: json_parser
id: parser-docker
output: extract_metadata_from_filepath
timestamp:
parse_from: attributes.time
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
- type: regex_parser
id: extract_metadata_from_filepath
regex: '^.*containers/(?P<container_id>[^_]+)/.*log$'
parse_from: attributes["log.file.path"]
output: parse_body
- type: move
id: parse_body
from: attributes.log
to: body
output: time
- type: remove
id: time
field: attributes.time
opencensus:
endpoint: 0.0.0.0:55678
otlp/spanmetrics:
@ -76,6 +101,16 @@ exporters:
prometheus:
endpoint: 0.0.0.0:8889
# logging: {}
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/
timeout: 5s
sending_queue:
queue_size: 100
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
extensions:
health_check:
@ -106,3 +141,7 @@ service:
metrics/spanmetrics:
receivers: [otlp/spanmetrics]
exporters: [prometheus]
logs:
receivers: [otlp, filelog/dockercontainers]
processors: [batch]
exporters: [clickhouselogsexporter]

View File

@ -0,0 +1,108 @@
version: "2.4"
services:
clickhouse:
image: clickhouse/clickhouse-server:22.4.5-alpine
container_name: clickhouse
# ports:
# - "9000:9000"
# - "8123:8123"
tty: true
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/
restart: on-failure
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
interval: 30s
timeout: 5s
retries: 3
alertmanager:
container_name: alertmanager
image: signoz/alertmanager:0.23.0-0.2
volumes:
- ./data/alertmanager:/data
depends_on:
query-service:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8085
- --storage.path=/data
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: otel-collector
image: signoz/signoz-otel-collector:0.55.0
command: ["--config=/etc/otel-collector-config.yaml"]
# user: root # required for reading docker container logs
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8888:8888" # OtelCollector internal metrics
# - "8889:8889" # signoz spanmetrics exposed by the agent
# - "9411:9411" # Zipkin port
# - "13133:13133" # health check extension
# - "14250:14250" # Jaeger gRPC
# - "14268:14268" # Jaeger thrift HTTP
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zPages extension
restart: on-failure
depends_on:
clickhouse:
condition: service_healthy
otel-collector-metrics:
container_name: otel-collector-metrics
image: signoz/signoz-otel-collector:0.55.0
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
# ports:
# - "1777:1777" # pprof extension
# - "8888:8888" # OtelCollector internal metrics
# - "13133:13133" # Health check extension
# - "55679:55679" # zPages extension
restart: on-failure
depends_on:
clickhouse:
condition: service_healthy
hotrod:
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
container_name: load-hotrod
hostname: load-hotrod
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../common/locust-scripts:/locust

View File

@ -0,0 +1,55 @@
version: "2.4"
services:
query-service:
hostname: query-service
build:
context: "../../../pkg/query-service"
dockerfile: "./Dockerfile"
args:
LDFLAGS: ""
TARGETPLATFORM: "${LOCAL_GOOS}/${LOCAL_GOARCH}"
container_name: query-service
environment:
- ClickHouseUrl=tcp://clickhouse:9000
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
- STORAGE=clickhouse
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
volumes:
- ./prometheus.yml:/root/config/prometheus.yml
- ../dashboards:/root/config/dashboards
- ./data/signoz/:/var/lib/signoz/
command: ["-config=/root/config/prometheus.yml"]
ports:
- "6060:6060"
- "8080:8080"
restart: on-failure
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
clickhouse:
condition: service_healthy
frontend:
build:
context: "../../../frontend"
dockerfile: "./Dockerfile"
args:
TARGETOS: "${LOCAL_GOOS}"
TARGETPLATFORM: "${LOCAL_GOARCH}"
container_name: frontend
environment:
- FRONTEND_API_ENDPOINT=http://query-service:8080
restart: on-failure
depends_on:
- alertmanager
- query-service
ports:
- "3301:3301"
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf

View File

@ -0,0 +1,43 @@
version: "2.4"
services:
query-service:
image: signoz/query-service:0.11.0
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
# - "8080:8080" # query-service port
volumes:
- ./prometheus.yml:/root/config/prometheus.yml
- ../dashboards:/root/config/dashboards
- ./data/signoz/:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
- STORAGE=clickhouse
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-amd
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
restart: on-failure
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
clickhouse:
condition: service_healthy
frontend:
image: signoz/frontend:0.11.0
container_name: frontend
restart: on-failure
depends_on:
- alertmanager
- query-service
ports:
- "3301:3301"
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf

View File

@ -39,7 +39,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:0.10.2
image: signoz/query-service:0.11.0
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@ -51,6 +51,8 @@ services:
- ./data/signoz/:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
- STORAGE=clickhouse
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
@ -66,7 +68,7 @@ services:
condition: service_healthy
frontend:
image: signoz/frontend:0.10.2
image: signoz/frontend:0.11.0
container_name: frontend
restart: on-failure
depends_on:
@ -78,10 +80,12 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.45.1-1.3
image: signoz/signoz-otel-collector:0.55.0
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
ports:
@ -96,14 +100,13 @@ services:
# - "14268:14268" # Jaeger thrift HTTP
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zPages extension
mem_limit: 2000m
restart: on-failure
depends_on:
clickhouse:
condition: service_healthy
otel-collector-metrics:
image: signoz/otelcontribcol:0.45.1-1.3
image: signoz/signoz-otel-collector:0.55.0
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -1,4 +1,29 @@
receivers:
filelog/dockercontainers:
include: [ "/var/lib/docker/containers/*/*.log" ]
start_at: end
include_file_path: true
include_file_name: false
operators:
- type: json_parser
id: parser-docker
output: extract_metadata_from_filepath
timestamp:
parse_from: attributes.time
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
- type: regex_parser
id: extract_metadata_from_filepath
regex: '^.*containers/(?P<container_id>[^_]+)/.*log$'
parse_from: attributes["log.file.path"]
output: parse_body
- type: move
id: parse_body
from: attributes.log
to: body
output: time
- type: remove
id: time
field: attributes.time
opencensus:
endpoint: 0.0.0.0:55678
otlp/spanmetrics:
@ -85,6 +110,17 @@ exporters:
endpoint: 0.0.0.0:8889
# logging: {}
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/
timeout: 5s
sending_queue:
queue_size: 100
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
service:
telemetry:
metrics:
@ -109,3 +145,7 @@ service:
metrics/spanmetrics:
receivers: [otlp/spanmetrics]
exporters: [prometheus]
logs:
receivers: [otlp, filelog/dockercontainers]
processors: [batch]
exporters: [clickhouselogsexporter]

View File

@ -1,3 +1,4 @@
node_modules
build
*.typegen.ts
i18-generate-hash.js

View File

@ -0,0 +1,24 @@
/* eslint-disable */
// @ts-ignore
// @ts-nocheck
const crypto = require('crypto');
const fs = require('fs');
const glob = require('glob');
function generateChecksum(str, algorithm, encoding) {
return crypto
.createHash(algorithm || 'md5')
.update(str, 'utf8')
.digest(encoding || 'hex');
}
const result = {};
glob.sync(`public/locales/**/*.json`).forEach(path => {
const [_, lang] = path.split('public/locales');
const content = fs.readFileSync(path, { encoding: 'utf-8' });
result[lang.replace('.json', '')] = generateChecksum(content);
});
fs.writeFileSync('./i18n-translations-hash.json', JSON.stringify(result));

View File

@ -4,19 +4,20 @@
"description": "",
"main": "webpack.config.js",
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve --progress",
"build": "webpack --config=webpack.config.prod.js --progress",
"i18n:generate-hash": "node ./i18-generate-hash.js",
"dev": "npm run i18n:generate-hash && cross-env NODE_ENV=development webpack serve --progress",
"build": "npm run i18n:generate-hash && webpack --config=webpack.config.prod.js --progress",
"prettify": "prettier --write .",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"lint": "npm run i18n:generate-hash && eslint ./src",
"lint:fix": "npm run i18n:generate-hash && eslint ./src --fix",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "is-ci || yarn husky:configure",
"playwright": "NODE_ENV=testing playwright test --config=./playwright.config.ts",
"playwright": "npm run i18n:generate-hash && NODE_ENV=testing playwright test --config=./playwright.config.ts",
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
"playwright:codegen:local":"playwright codegen http://localhost:3301",
"playwright:codegen:local:auth":"yarn playwright:codegen:local --load-storage=tests/auth.json",
"playwright:codegen:local": "playwright codegen http://localhost:3301",
"playwright:codegen:local:auth": "yarn playwright:codegen:local --load-storage=tests/auth.json",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1"
},
@ -55,7 +56,9 @@
"d3-tip": "^0.9.1",
"dayjs": "^1.10.7",
"dotenv": "8.2.0",
"event-source-polyfill": "1.0.31",
"file-loader": "6.1.1",
"flat": "^5.0.2",
"history": "4.10.1",
"html-webpack-plugin": "5.1.0",
"i18next": "^21.6.12",
@ -123,6 +126,8 @@
"@types/copy-webpack-plugin": "^8.0.1",
"@types/d3": "^6.2.0",
"@types/d3-tip": "^3.5.5",
"@types/event-source-polyfill": "^1.0.0",
"@types/flat": "^5.0.2",
"@types/jest": "^27.5.1",
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",

View File

@ -100,6 +100,10 @@ export const MySettings = Loadable(
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
);
export const Logs = Loadable(
() => import(/* webpackChunkName: "Logs" */ 'pages/Logs'),
);
export const Login = Loadable(
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
);

View File

@ -14,6 +14,7 @@ import {
InstrumentationPage,
ListAllALertsPage,
Login,
Logs,
MySettings,
NewDashboardPage,
OrganizationSettings,
@ -193,6 +194,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'MY_SETTINGS',
},
{
path: ROUTES.LOGS,
exact: true,
component: Logs,
key: 'LOGS',
isPrivate: true,
},
{
path: ROUTES.LOGIN,
exact: true,

View File

@ -3,6 +3,8 @@ import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
import cacheBursting from '../../i18n-translations-hash.json';
i18n
// load translation using http -> see /public/locales
.use(Backend)
@ -17,7 +19,14 @@ i18n
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
backend: {
loadPath: (language, namespace) => {
const ns = namespace[0];
const pathkey = `/${language}/${ns}`;
const hash = cacheBursting[pathkey as keyof typeof cacheBursting] || '';
return `/locales/${language}/${namespace}.json?h=${hash}`;
},
},
react: {
useSuspense: false,
},

View File

@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/addToSelectedFields';
const AddToSelectedFields = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.post(`/logs/fields`, props);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default AddToSelectedFields;

View File

@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/getLogs';
const GetLogs = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.get(`/logs`, {
params: props,
});
return {
statusCode: 200,
error: null,
message: '',
payload: data.data.results,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetLogs;

View File

@ -0,0 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/getLogsAggregate';
const GetLogsAggregate = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.get(`/logs/aggregate`, {
params: props,
});
return {
statusCode: 200,
error: null,
message: '',
payload: data.data.items,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetLogsAggregate;

View File

@ -0,0 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/logs/getSearchFields';
const GetSearchFields = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const data = await axios.get(`/logs/fields`);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetSearchFields;

View File

@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/addToSelectedFields';
const RemoveSelectedField = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.post(`/logs/fields`, props);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default RemoveSelectedField;

View File

@ -0,0 +1,17 @@
import apiV1 from 'api/apiV1';
import getLocalStorageKey from 'api/browser/localstorage/get';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import { EventSourcePolyfill } from 'event-source-polyfill';
export const LiveTail = (queryParams: string): EventSourcePolyfill => {
const dict = {
headers: {
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
},
};
return new EventSourcePolyfill(
`${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`,
dict,
);
};

View File

@ -65,6 +65,7 @@ function Graph({
yAxisUnit = 'short',
forceReRender,
staticLine,
containerHeight,
}: GraphProps): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const chartRef = useRef<HTMLCanvasElement>(null);
@ -241,7 +242,7 @@ function Graph({
}, [buildChart, forceReRender]);
return (
<div style={{ height: '85%' }}>
<div style={{ height: containerHeight }}>
<canvas ref={chartRef} />
<LegendsContainer id={name} />
</div>
@ -259,6 +260,7 @@ interface GraphProps {
yAxisUnit?: string;
forceReRender?: boolean | null | number;
staticLine?: StaticLineProps | undefined;
containerHeight?: string | number;
}
export interface StaticLineProps {
@ -285,5 +287,6 @@ Graph.defaultProps = {
yAxisUnit: undefined,
forceReRender: undefined,
staticLine: undefined,
containerHeight: '85%',
};
export default Graph;

View File

@ -0,0 +1,147 @@
import { Button, Popover } from 'antd';
import getStep from 'lib/getStep';
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
import React, { memo, useCallback, useMemo } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { SET_SEARCH_QUERY_STRING, TOGGLE_LIVE_TAIL } from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
interface AddToQueryHOCProps {
fieldKey: string;
fieldValue: string;
children: React.ReactNode;
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => ReturnType<typeof getLogsAggregate>;
}
function AddToQueryHOC({
fieldKey,
fieldValue,
children,
getLogs,
getLogsAggregate,
}: AddToQueryHOCProps): JSX.Element {
const {
searchFilter: { queryString },
logLinesPerPage,
idStart,
idEnd,
liveTail,
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const dispatch = useDispatch();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const generatedQuery = useMemo(
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
[fieldKey, fieldValue],
);
const handleQueryAdd = useCallback(() => {
let updatedQueryString = queryString || '';
if (updatedQueryString.length === 0) {
updatedQueryString += `${generatedQuery}`;
} else {
updatedQueryString += ` AND ${generatedQuery}`;
}
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: updatedQueryString,
});
if (liveTail === 'STOPPED') {
getLogs({
q: updatedQueryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order: 'desc',
timestampStart: minTime,
timestampEnd: maxTime,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: updatedQueryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
} else if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'PAUSED',
});
setTimeout(
() =>
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
}),
0,
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
dispatch,
generatedQuery,
getLogs,
idEnd,
idStart,
logLinesPerPage,
maxTime,
minTime,
queryString,
]);
const popOverContent = (
<span style={{ fontSize: '0.9rem' }}>Add to query: {fieldKey}</span>
);
return (
<Button
size="small"
type="text"
style={{
margin: 0,
padding: 0,
}}
onClick={handleQueryAdd}
>
<Popover placement="top" content={popOverContent}>
{children}
</Popover>
</Button>
);
}
interface DispatchProps {
getLogs: (
props: Parameters<typeof getLogs>[0],
) => (dispatch: Dispatch<AppActions>) => void;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(AddToQueryHOC));

View File

@ -0,0 +1,12 @@
import React from 'react';
import { CategoryHeadingText } from './styles';
interface ICategoryHeadingProps {
children: React.ReactNode;
}
function CategoryHeading({ children }: ICategoryHeadingProps): JSX.Element {
return <CategoryHeadingText type="secondary">{children}</CategoryHeadingText>;
}
export default CategoryHeading;

View File

@ -0,0 +1,6 @@
import { Typography } from 'antd';
import styled from 'styled-components';
export const CategoryHeadingText = styled(Typography.Text)`
font-size: 0.8rem;
`;

View File

@ -0,0 +1,37 @@
import { Popover } from 'antd';
import React from 'react';
import { useCopyToClipboard } from 'react-use';
interface CopyClipboardHOCProps {
textToCopy: string;
children: React.ReactNode;
}
function CopyClipboardHOC({
textToCopy,
children,
}: CopyClipboardHOCProps): JSX.Element {
const [, setCopy] = useCopyToClipboard();
return (
<span
style={{
margin: 0,
padding: 0,
cursor: 'pointer',
}}
onClick={(): void => setCopy(textToCopy)}
onKeyDown={(): void => setCopy(textToCopy)}
role="button"
tabIndex={0}
>
<Popover
placement="top"
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
>
{children}
</Popover>
</span>
);
}
export default CopyClipboardHOC;

View File

@ -0,0 +1,157 @@
import { blue, grey, orange } from '@ant-design/colors';
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
import { Button, Divider, Row, Typography } from 'antd';
import { map } from 'd3';
import dayjs from 'dayjs';
import { FlatLogData } from 'lib/logs/flatLogData';
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log';
import { ILogsReducer } from 'types/reducer/logs';
import AddToQueryHOC from '../AddToQueryHOC';
import CopyClipboardHOC from '../CopyClipboardHOC';
import { Container } from './styles';
import { isValidLogField } from './util';
interface LogFieldProps {
fieldKey: string;
fieldValue: string;
}
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
return (
<div
style={{
display: 'flex',
overflow: 'hidden',
width: '100%',
}}
>
<Typography.Text type="secondary">{fieldKey}</Typography.Text>
<CopyClipboardHOC textToCopy={fieldValue}>
<Typography.Text ellipsis>
{': '}
{fieldValue}
</Typography.Text>
</CopyClipboardHOC>
</div>
);
}
function LogSelectedField({
fieldKey = '',
fieldValue = '',
}: LogFieldProps): JSX.Element {
return (
<div
style={{
display: 'flex',
overflow: 'hidden',
width: '100%',
}}
>
<AddToQueryHOC fieldKey={fieldKey} fieldValue={fieldValue}>
<Typography.Text>
{`"`}
<span style={{ color: blue[4] }}>{fieldKey}</span>
{`"`}
</Typography.Text>
</AddToQueryHOC>
<CopyClipboardHOC textToCopy={fieldValue}>
<Typography.Text ellipsis>
<span>
{': '}
{typeof fieldValue === 'string' && `"`}
</span>
<span style={{ color: orange[6] }}>{fieldValue}</span>
{typeof fieldValue === 'string' && `"`}
</Typography.Text>
</CopyClipboardHOC>
</div>
);
}
interface LogItemProps {
logData: ILog;
}
function LogItem({ logData }: LogItemProps): JSX.Element {
const {
fields: { selected },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const dispatch = useDispatch();
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
const [, setCopy] = useCopyToClipboard();
const handleDetailedView = useCallback(() => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: logData,
});
}, [dispatch, logData]);
const handleCopyJSON = (): void => {
setCopy(JSON.stringify(logData, null, 2));
};
return (
<Container>
<div style={{ maxWidth: '100%' }}>
<div>
{'{'}
<div style={{ marginLeft: '0.5rem' }}>
<LogGeneralField
fieldKey="log"
fieldValue={flattenLogData.body as never}
/>
{flattenLogData.stream && (
<LogGeneralField
fieldKey="stream"
fieldValue={flattenLogData.stream as never}
/>
)}
<LogGeneralField
fieldKey="timestamp"
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
/>
</div>
{'}'}
</div>
<div>
{map(selected, (field) => {
return isValidLogField(flattenLogData[field.name] as never) ? (
<LogSelectedField
key={field.name}
fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never}
/>
) : null;
})}
</div>
</div>
<Divider style={{ padding: 0, margin: '0.4rem 0', opacity: 0.5 }} />
<Row>
<Button
size="small"
type="text"
onClick={handleDetailedView}
style={{ color: blue[5], padding: 0, margin: 0 }}
>
{' '}
<ExpandAltOutlined /> View Details
</Button>
<Button
size="small"
type="text"
onClick={handleCopyJSON}
style={{ padding: 0, margin: 0, color: grey[1] }}
>
{' '}
<CopyFilled /> Copy JSON
</Button>
</Row>
</Container>
);
}
export default LogItem;

View File

@ -0,0 +1,18 @@
import { Card } from 'antd';
import styled, { keyframes } from 'styled-components';
const fadeInAnimation = keyframes`
0% { opacity: 0; }
100% { opacity: 1;}
`;
export const Container = styled(Card)`
width: 100% !important;
margin-bottom: 0.3rem;
.ant-card-body {
padding: 0.3rem 0.6rem;
}
animation-name: ${fadeInAnimation};
animation-duration: 0.2s;
animation-timing-function: ease-in;
`;

View File

@ -0,0 +1 @@
export const isValidLogField = (value: never): boolean => value !== undefined;

View File

@ -30,4 +30,5 @@ export const QueryBuilderQueryTemplate = {
export const QueryBuilderFormulaTemplate = {
expression: '',
disabled: false,
legend: '',
};

View File

@ -26,6 +26,7 @@ const ROUTES = {
SOMETHING_WENT_WRONG: '/something-went-wrong',
UN_AUTHORIZED: '/un-authorized',
NOT_FOUND: '/not-found',
LOGS: '/logs',
HOME_PAGE: '/',
PASSWORD_RESET: '/password-reset',
};

View File

@ -68,15 +68,20 @@ function QuerySection({
const handleFormulaChange = ({
formulaIndex,
expression,
legend,
toggleDisable,
toggleDelete,
}: IQueryBuilderFormulaHandleChange): void => {
const allFormulas = formulaQueries;
const current = allFormulas[formulaIndex];
if (expression) {
if (expression !== undefined) {
current.expression = expression;
}
if (legend !== undefined) {
current.legend = legend;
}
if (toggleDisable) {
current.disabled = !current.disabled;
}
@ -179,6 +184,7 @@ function QuerySection({
formulaOnly: true,
expression: 'A',
disabled: false,
legend: '',
};
setFormulaQueries({ ...formulas });

View File

@ -1,6 +1,8 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
Card,
Col,
Divider,
Modal,
@ -26,6 +28,7 @@ import {
} from 'types/api/disks/getDisks';
import { TTTLType } from 'types/api/settings/common';
import {
PayloadPropsLogs as GetRetentionPeriodLogsPayload,
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
} from 'types/api/settings/getRetention';
@ -40,19 +43,25 @@ type NumberOrNull = number | null;
function GeneralSettings({
metricsTtlValuesPayload,
tracesTtlValuesPayload,
logsTtlValuesPayload,
getAvailableDiskPayload,
metricsTtlValuesRefetch,
tracesTtlValuesRefetch,
logsTtlValuesRefetch,
}: GeneralSettingsProps): JSX.Element {
const { t } = useTranslation(['generalSettings']);
const [modalMetrics, setModalMetrics] = useState<boolean>(false);
const [modalTraces, setModalTraces] = useState<boolean>(false);
const [modalLogs, setModalLogs] = useState<boolean>(false);
const [postApiLoadingMetrics, setPostApiLoadingMetrics] = useState<boolean>(
false,
);
const [postApiLoadingTraces, setPostApiLoadingTraces] = useState<boolean>(
false,
);
const [modalTraces, setModalTraces] = useState<boolean>(false);
const [postApiLoadingLogs, setPostApiLoadingLogs] = useState<boolean>(false);
const [availableDisks] = useState<IDiskType[]>(getAvailableDiskPayload);
const [metricsCurrentTTLValues, setMetricsCurrentTTLValues] = useState(
@ -62,6 +71,10 @@ function GeneralSettings({
tracesTtlValuesPayload,
);
const [logsCurrentTTLValues, setLogsCurrentTTLValues] = useState(
logsTtlValuesPayload,
);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [setRetentionPermission] = useComponentPermission(
@ -86,6 +99,15 @@ function GeneralSettings({
setTracesS3RetentionPeriod,
] = useState<NumberOrNull>(null);
const [
logsTotalRetentionPeriod,
setLogsTotalRetentionPeriod,
] = useState<NumberOrNull>(null);
const [
logsS3RetentionPeriod,
setLogsS3RetentionPeriod,
] = useState<NumberOrNull>(null);
useEffect(() => {
if (metricsCurrentTTLValues) {
setMetricsTotalRetentionPeriod(
@ -112,6 +134,17 @@ function GeneralSettings({
}
}, [tracesCurrentTTLValues]);
useEffect(() => {
if (logsCurrentTTLValues) {
setLogsTotalRetentionPeriod(logsCurrentTTLValues.logs_ttl_duration_hrs);
setLogsS3RetentionPeriod(
logsCurrentTTLValues.logs_move_ttl_duration_hrs
? logsCurrentTTLValues.logs_move_ttl_duration_hrs
: null,
);
}
}, [logsCurrentTTLValues]);
useInterval(
async (): Promise<void> => {
if (metricsTtlValuesPayload.status === 'pending') {
@ -130,13 +163,24 @@ function GeneralSettings({
tracesTtlValuesPayload.status === 'pending' ? 1000 : null,
);
useInterval(
async (): Promise<void> => {
if (logsTtlValuesPayload.status === 'pending') {
logsTtlValuesRefetch();
}
},
logsTtlValuesPayload.status === 'pending' ? 1000 : null,
);
const onModalToggleHandler = (type: TTTLType): void => {
if (type === 'metrics') setModalMetrics((modal) => !modal);
if (type === 'traces') setModalTraces((modal) => !modal);
if (type === 'logs') setModalLogs((modal) => !modal);
};
const onPostApiLoadingHandler = (type: TTTLType): void => {
if (type === 'metrics') setPostApiLoadingMetrics((modal) => !modal);
if (type === 'traces') setPostApiLoadingTraces((modal) => !modal);
if (type === 'logs') setPostApiLoadingLogs((modal) => !modal);
};
const onClickSaveHandler = useCallback(
@ -157,7 +201,13 @@ function GeneralSettings({
[availableDisks],
);
const [isMetricsSaveDisabled, isTracesSaveDisabled, errorText] = useMemo((): [
const [
isMetricsSaveDisabled,
isTracesSaveDisabled,
isLogsSaveDisabled,
errorText,
] = useMemo((): [
boolean,
boolean,
boolean,
string,
@ -174,6 +224,7 @@ function GeneralSettings({
// Defaults to button not disabled and empty error message text.
let isMetricsSaveDisabled = false;
let isTracesSaveDisabled = false;
let isLogsSaveDisabled = false;
let errorText = '';
if (s3Enabled) {
@ -189,18 +240,35 @@ function GeneralSettings({
) {
isTracesSaveDisabled = true;
errorText = messages.compareError('traces');
} else if (
(logsTotalRetentionPeriod || logsS3RetentionPeriod) &&
Number(logsTotalRetentionPeriod) <= Number(logsS3RetentionPeriod)
) {
isLogsSaveDisabled = true;
errorText = messages.compareError('logs');
}
}
if (!metricsTotalRetentionPeriod || !tracesTotalRetentionPeriod) {
if (
!metricsTotalRetentionPeriod ||
!tracesTotalRetentionPeriod ||
!logsTotalRetentionPeriod
) {
isMetricsSaveDisabled = true;
isTracesSaveDisabled = true;
if (!metricsTotalRetentionPeriod && !tracesTotalRetentionPeriod) {
errorText = messages.nullValueError('metrics and traces');
isLogsSaveDisabled = true;
if (
!metricsTotalRetentionPeriod &&
!tracesTotalRetentionPeriod &&
!logsTotalRetentionPeriod
) {
errorText = messages.nullValueError('metrics, traces and logs');
} else if (!metricsTotalRetentionPeriod) {
errorText = messages.nullValueError('metrics');
} else if (!tracesTotalRetentionPeriod) {
errorText = messages.nullValueError('traces');
} else if (!logsTotalRetentionPeriod) {
errorText = messages.nullValueError('logs');
}
}
if (
@ -219,8 +287,23 @@ function GeneralSettings({
)
isTracesSaveDisabled = true;
return [isMetricsSaveDisabled, isTracesSaveDisabled, errorText];
if (
logsCurrentTTLValues.logs_ttl_duration_hrs === logsTotalRetentionPeriod &&
logsCurrentTTLValues.logs_move_ttl_duration_hrs === logsS3RetentionPeriod
)
isLogsSaveDisabled = true;
return [
isMetricsSaveDisabled,
isTracesSaveDisabled,
isLogsSaveDisabled,
errorText,
];
}, [
logsCurrentTTLValues.logs_move_ttl_duration_hrs,
logsCurrentTTLValues.logs_ttl_duration_hrs,
logsS3RetentionPeriod,
logsTotalRetentionPeriod,
metricsCurrentTTLValues.metrics_move_ttl_duration_hrs,
metricsCurrentTTLValues?.metrics_ttl_duration_hrs,
metricsS3RetentionPeriod,
@ -235,21 +318,36 @@ function GeneralSettings({
// eslint-disable-next-line sonarjs/cognitive-complexity
const onOkHandler = async (type: TTTLType): Promise<void> => {
let apiCallTotalRetention;
let apiCallS3Retention;
switch (type) {
case 'metrics': {
apiCallTotalRetention = metricsTotalRetentionPeriod;
apiCallS3Retention = metricsS3RetentionPeriod;
break;
}
case 'traces': {
apiCallTotalRetention = tracesTotalRetentionPeriod;
apiCallS3Retention = tracesS3RetentionPeriod;
break;
}
case 'logs': {
apiCallTotalRetention = logsTotalRetentionPeriod;
apiCallS3Retention = logsS3RetentionPeriod;
break;
}
default: {
break;
}
}
try {
onPostApiLoadingHandler(type);
const setTTLResponse = await setRetentionApi({
type,
totalDuration: `${
(type === 'metrics'
? metricsTotalRetentionPeriod
: tracesTotalRetentionPeriod) || -1
}h`,
totalDuration: `${apiCallTotalRetention || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${
(type === 'metrics'
? metricsS3RetentionPeriod
: tracesS3RetentionPeriod) || -1
}h`,
toColdDuration: `${apiCallS3Retention || -1}h`,
});
let hasSetTTLFailed = false;
if (setTTLResponse.statusCode === 409) {
@ -281,6 +379,15 @@ function GeneralSettings({
traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1,
status: '',
});
} else if (type === 'logs') {
logsTtlValuesRefetch();
if (!hasSetTTLFailed)
// Updates the currentTTL Values in order to avoid pushing the same values.
setLogsCurrentTTLValues({
logs_ttl_duration_hrs: logsTotalRetentionPeriod || -1,
logs_move_ttl_duration_hrs: logsS3RetentionPeriod || -1,
status: '',
});
}
} catch (error) {
notification.error({
@ -375,16 +482,64 @@ function GeneralSettings({
/>
),
},
].map((category, idx, renderArr): JSX.Element | null => {
{
name: 'Logs',
retentionFields: [
{
name: t('total_retention_period'),
value: logsTotalRetentionPeriod,
setValue: setLogsTotalRetentionPeriod,
},
{
name: t('move_to_s3'),
value: logsS3RetentionPeriod,
setValue: setLogsS3RetentionPeriod,
hide: !s3Enabled,
},
],
save: {
modal: modalLogs,
modalOpen: (): void => onClickSaveHandler('logs'),
apiLoading: postApiLoadingLogs,
saveButtonText:
logsTtlValuesPayload.status === 'pending' ? (
<span>
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
{t('retention_save_button.pending', { name: 'logs' })}
</span>
) : (
<span>{t('retention_save_button.success')}</span>
),
isDisabled: logsTtlValuesPayload.status === 'pending' || isLogsSaveDisabled,
},
statusComponent: (
<StatusMessage
total_retention={logsTtlValuesPayload.expected_logs_ttl_duration_hrs}
status={logsTtlValuesPayload.status}
s3_retention={logsTtlValuesPayload.expected_logs_move_ttl_duration_hrs}
/>
),
},
].map((category): JSX.Element | null => {
if (
Array.isArray(category.retentionFields) &&
category.retentionFields.length > 0
) {
return (
<React.Fragment key={category.name}>
<Col xs={22} xl={11} key={category.name}>
<Typography.Title level={3}>{category.name}</Typography.Title>
<Col xs={22} xl={11} key={category.name} style={{ margin: '0.5rem' }}>
<Card style={{ height: '100%', minHeight: 300 }}>
<Typography.Title style={{ margin: 0 }} level={3}>
{category.name}
</Typography.Title>
<Divider
style={{
margin: '0.5rem 0',
padding: 0,
opacity: 0.5,
marginBottom: '1rem',
}}
/>
{category.retentionFields.map((retentionField) => (
<Retention
key={retentionField.name}
@ -426,12 +581,8 @@ function GeneralSettings({
})}
</Typography>
</Modal>
</Card>
</Col>
{idx < renderArr.length && (
<Col xs={0} xl={1} style={{ textAlign: 'center' }}>
<Divider type="vertical" dashed style={{ height: '100%' }} />
</Col>
)}
</React.Fragment>
);
}
@ -451,7 +602,7 @@ function GeneralSettings({
{errorText && <ErrorText>{errorText}</ErrorText>}
</ErrorTextContainer>
<Row justify="space-around">{renderConfig}</Row>
<Row justify="start">{renderConfig}</Row>
</Col>
);
}
@ -460,12 +611,16 @@ interface GeneralSettingsProps {
getAvailableDiskPayload: GetDisksPayload;
metricsTtlValuesPayload: GetRetentionPeriodMetricsPayload;
tracesTtlValuesPayload: GetRetentionPeriodTracesPayload;
logsTtlValuesPayload: GetRetentionPeriodLogsPayload;
metricsTtlValuesRefetch: UseQueryResult<
ErrorResponse | SuccessResponse<GetRetentionPeriodMetricsPayload>
>['refetch'];
tracesTtlValuesRefetch: UseQueryResult<
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
>['refetch'];
logsTtlValuesRefetch: UseQueryResult<
ErrorResponse | SuccessResponse<GetRetentionPeriodLogsPayload>
>['refetch'];
}
export default GeneralSettings;

View File

@ -84,10 +84,10 @@ function Retention({
return (
<RetentionContainer>
<Row justify="space-between">
<Col flex={1} style={{ display: 'flex' }}>
<Col span={12} style={{ display: 'flex' }}>
<RetentionFieldLabel>{text}</RetentionFieldLabel>
</Col>
<Col flex="150px">
<Row justify="end">
<RetentionFieldInputContainer>
<Input
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
@ -102,7 +102,7 @@ function Retention({
{menuItems}
</Select>
</RetentionFieldInputContainer>
</Col>
</Row>
</Row>
</RetentionContainer>
);

View File

@ -21,6 +21,7 @@ function GeneralSettings(): JSX.Element {
const [
getRetentionPeriodMetricsApiResponse,
getRetentionPeriodTracesApiResponse,
getRetentionPeriodLogsApiResponse,
getDisksResponse,
] = useQueries([
{
@ -33,6 +34,10 @@ function GeneralSettings(): JSX.Element {
getRetentionPeriodApi('traces'),
queryKey: 'getRetentionPeriodApiTraces',
},
{
queryFn: (): TRetentionAPIReturn<'logs'> => getRetentionPeriodApi('logs'),
queryKey: 'getRetentionPeriodApiLogs',
},
{
queryFn: getDisks,
queryKey: 'getDisks',
@ -60,17 +65,27 @@ function GeneralSettings(): JSX.Element {
</Typography>
);
}
// Error State - When RetentionPeriodLogsApi or getDiskApi gets errored out.
if (getRetentionPeriodLogsApiResponse.isError || getDisksResponse.isError) {
return (
<Typography>
{getRetentionPeriodLogsApiResponse.data?.error ||
getDisksResponse.data?.error ||
t('something_went_wrong')}
</Typography>
);
}
// Loading State - When Metrics, Traces and Disk API are in progress and the promise has not been resolved/reject.
if (
getRetentionPeriodMetricsApiResponse.isLoading ||
getDisksResponse.isLoading ||
!getDisksResponse.data?.payload ||
getRetentionPeriodMetricsApiResponse.isLoading ||
!getRetentionPeriodMetricsApiResponse.data?.payload ||
getRetentionPeriodTracesApiResponse.isLoading ||
getDisksResponse.isLoading ||
!getDisksResponse.data?.payload ||
!getRetentionPeriodTracesApiResponse.data?.payload
!getRetentionPeriodTracesApiResponse.data?.payload ||
getRetentionPeriodLogsApiResponse.isLoading ||
!getRetentionPeriodLogsApiResponse.data?.payload
) {
return <Spinner tip="Loading.." height="70vh" />;
}
@ -83,6 +98,8 @@ function GeneralSettings(): JSX.Element {
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.payload,
logsTtlValuesRefetch: getRetentionPeriodLogsApiResponse.refetch,
}}
/>
);

View File

@ -0,0 +1,121 @@
import {
FastBackwardOutlined,
LeftOutlined,
RightOutlined,
} from '@ant-design/icons';
import { Button, Divider, Select } from 'antd';
import React, { memo } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
GET_NEXT_LOG_LINES,
GET_PREVIOUS_LOG_LINES,
RESET_ID_START_AND_END,
SET_LOG_LINES_PER_PAGE,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { Container } from './styles';
const { Option } = Select;
const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];
interface LogControlsProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
}
function LogControls({ getLogs }: LogControlsProps): JSX.Element | null {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const {
logLinesPerPage,
idStart,
idEnd,
liveTail,
searchFilter: { queryString },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const dispatch = useDispatch();
const handleLogLinesPerPageChange = (e: number): void => {
dispatch({
type: SET_LOG_LINES_PER_PAGE,
payload: e,
});
};
const handleGoToLatest = (): void => {
dispatch({
type: RESET_ID_START_AND_END,
});
if (liveTail === 'STOPPED')
getLogs({
q: queryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order: 'desc',
timestampStart: minTime,
timestampEnd: maxTime,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
};
const handleNavigatePrevious = (): void => {
dispatch({
type: GET_PREVIOUS_LOG_LINES,
});
};
const handleNavigateNext = (): void => {
dispatch({
type: GET_NEXT_LOG_LINES,
});
};
if (liveTail !== 'STOPPED') {
return null;
}
return (
<Container>
<Button size="small" type="link" onClick={handleGoToLatest}>
<FastBackwardOutlined /> Go to latest
</Button>
<Divider type="vertical" />
<Button size="small" type="link" onClick={handleNavigatePrevious}>
<LeftOutlined /> Previous
</Button>
<Button size="small" type="link" onClick={handleNavigateNext}>
Next <RightOutlined />
</Button>
<Select
style={{ width: 120 }}
value={logLinesPerPage}
onChange={handleLogLinesPerPageChange}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => {
return <Option key={count} value={count}>{`${count} / page`}</Option>;
})}
</Select>
</Container>
);
}
interface DispatchProps {
getLogs: (
props: Parameters<typeof getLogs>[0],
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(LogControls));

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
margin-bottom: 0.5rem;
`;

View File

@ -0,0 +1,167 @@
import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
import { Button, Col, Popover } from 'antd';
import getStep from 'lib/getStep';
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
import React, { memo, useMemo } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { SET_SEARCH_QUERY_STRING, TOGGLE_LIVE_TAIL } from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
const removeJSONStringifyQuotes = (s: string): string => {
if (!s || !s.length) {
return s;
}
if (s[0] === '"' && s[s.length - 1] === '"') {
return s.slice(1, s.length - 1);
}
return s;
};
interface ActionItemProps {
fieldKey: string;
fieldValue: string;
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => ReturnType<typeof getLogsAggregate>;
}
function ActionItem({
fieldKey,
fieldValue,
getLogs,
getLogsAggregate,
}: ActionItemProps): JSX.Element | unknown {
const {
searchFilter: { queryString },
logLinesPerPage,
idStart,
liveTail,
idEnd,
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const dispatch = useDispatch();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const handleQueryAdd = (newQueryString: string): void => {
let updatedQueryString = queryString || '';
if (updatedQueryString.length === 0) {
updatedQueryString += `${newQueryString}`;
} else {
updatedQueryString += ` AND ${newQueryString}`;
}
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: updatedQueryString,
});
if (liveTail === 'STOPPED') {
getLogs({
q: updatedQueryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order: 'desc',
timestampStart: minTime,
timestampEnd: maxTime,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: updatedQueryString,
});
} else if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'PAUSED',
});
setTimeout(
() =>
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
}),
0,
);
}
};
const validatedFieldValue = removeJSONStringifyQuotes(fieldValue);
const PopOverMenuContent = useMemo(
() => (
<Col>
<Button
type="text"
size="small"
onClick={(): void =>
handleQueryAdd(
generateFilterQuery({
fieldKey,
fieldValue: validatedFieldValue,
type: 'IN',
}),
)
}
>
<PlusCircleOutlined /> Filter for value
</Button>
<br />
<Button
type="text"
size="small"
onClick={(): void =>
handleQueryAdd(
generateFilterQuery({
fieldKey,
fieldValue: validatedFieldValue,
type: 'NIN',
}),
)
}
>
<MinusCircleOutlined /> Filter out value
</Button>
</Col>
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[fieldKey, validatedFieldValue],
);
return (
<Popover placement="bottomLeft" content={PopOverMenuContent} trigger="click">
<Button type="text" size="small">
...
</Button>
</Popover>
);
}
interface DispatchProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => (dispatch: never) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default connect(null, mapDispatchToProps)(memo(ActionItem as any));

View File

@ -0,0 +1,44 @@
import { blue } from '@ant-design/colors';
import { CopyFilled } from '@ant-design/icons';
import { Button, Row } from 'antd';
import Editor from 'components/Editor';
import React, { useMemo } from 'react';
import { useCopyToClipboard } from 'react-use';
import { ILog } from 'types/api/logs/log';
interface JSONViewProps {
logData: ILog;
}
function JSONView({ logData }: JSONViewProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const LogJsonData = useMemo(() => JSON.stringify(logData, null, 2), [logData]);
return (
<div>
<Row
style={{
justifyContent: 'flex-end',
margin: '0.5rem 0',
}}
>
<Button
size="small"
type="text"
onClick={(): void => copyToClipboard(LogJsonData)}
>
<CopyFilled /> <span style={{ color: blue[5] }}>Copy to Clipboard</span>
</Button>
</Row>
<div style={{ marginTop: '0.5rem' }}>
<Editor
value={LogJsonData}
language="json"
height="70vh"
readOnly
onChange={(): void => {}}
/>
</div>
</div>
);
}
export default JSONView;

View File

@ -0,0 +1,109 @@
import { blue, orange } from '@ant-design/colors';
import { Input, Table } from 'antd';
import AddToQueryHOC from 'components/Logs/AddToQueryHOC';
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
import flatten from 'flat';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import React, { useMemo, useState } from 'react';
import { ILog } from 'types/api/logs/log';
import ActionItem from './ActionItem';
// Fields which should be restricted from adding it to query
const RESTRICTED_FIELDS = ['timestamp'];
interface TableViewProps {
logData: ILog;
}
function TableView({ logData }: TableViewProps): JSX.Element | null {
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
const flattenLogData: Record<string, never> | null = useMemo(
() => (logData ? flatten(logData) : null),
[logData],
);
if (logData === null) {
return null;
}
const dataSource =
flattenLogData !== null &&
Object.keys(flattenLogData)
.filter((field) => fieldSearchFilter(field, fieldSearchInput))
.map((key) => {
return {
key,
field: key,
value: JSON.stringify(flattenLogData[key]),
};
});
if (!dataSource) {
return null;
}
const columns = [
{
title: 'Action',
width: 75,
render: (fieldData: Record<string, string>): JSX.Element | null => {
const fieldKey = fieldData.field.split('.').slice(-1);
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
return <ActionItem fieldKey={fieldKey} fieldValue={fieldData.value} />;
}
return null;
},
},
{
title: 'Field',
dataIndex: 'field',
key: 'field',
width: '35%',
render: (field: string): JSX.Element => {
const fieldKey = field.split('.').slice(-1);
const renderedField = <span style={{ color: blue[4] }}>{field}</span>;
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
return (
<AddToQueryHOC fieldKey={fieldKey[0]} fieldValue={flattenLogData[field]}>
{' '}
{renderedField}
</AddToQueryHOC>
);
}
return renderedField;
},
},
{
title: 'Value',
dataIndex: 'value',
key: 'value',
ellipsis: false,
render: (field: never): JSX.Element => (
<CopyClipboardHOC textToCopy={field}>
<span style={{ color: orange[6] }}>{field}</span>
</CopyClipboardHOC>
),
width: '60%',
},
];
return (
<div style={{ position: 'relative' }}>
<Input
placeholder="Search field names"
size="large"
value={fieldSearchInput}
onChange={(e): void => setFieldSearchInput(e.target.value)}
/>
<Table
// scroll={{ x: true }}
tableLayout="fixed"
dataSource={dataSource}
columns={columns as never}
pagination={false}
/>
</div>
);
}
export default TableView;

View File

@ -0,0 +1,53 @@
import { Drawer, Tabs } from 'antd';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs';
import JSONView from './JsonView';
import TableView from './TableView';
const { TabPane } = Tabs;
function LogDetailedView(): JSX.Element {
const { detailedLog } = useSelector<AppState, ILogsReducer>(
(state) => state.logs,
);
const dispatch = useDispatch();
const onDrawerClose = (): void => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: null,
});
};
return (
<div style={{}}>
<Drawer
width="60%"
title="Log Details"
placement="right"
closable
mask={false}
onClose={onDrawerClose}
visible={detailedLog !== null}
getContainer={false}
style={{ overscrollBehavior: 'contain' }}
>
{detailedLog && (
<Tabs defaultActiveKey="1">
<TabPane tab="Table" key="1">
<TableView logData={detailedLog} />
</TabPane>
<TabPane tab="JSON" key="2">
<JSONView logData={detailedLog} />
</TabPane>
</Tabs>
)}
</Drawer>
</div>
);
}
export default LogDetailedView;

View File

@ -0,0 +1,26 @@
import React from 'react';
interface OptionIconProps {
isDarkMode: boolean;
}
function OptionIcon({ isDarkMode }: OptionIconProps): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="1rem"
height="1rem"
viewBox="0 0 52 52"
enableBackground="new 0 0 52 52"
fill={isDarkMode ? '#eee' : '#222'}
>
<path
d="M20,44c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6S20,47.3,20,44z M20,26c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6
S20,29.3,20,26z M20,8c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6S20,11.3,20,8z"
/>
</svg>
);
}
export default OptionIcon;

View File

@ -0,0 +1,222 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { green } from '@ant-design/colors';
import { PauseOutlined, PlayCircleOutlined } from '@ant-design/icons';
import { Button, Popover, Row, Select } from 'antd';
import { LiveTail } from 'api/logs/livetail';
import dayjs from 'dayjs';
import { throttle } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import {
FLUSH_LOGS,
PUSH_LIVE_TAIL_EVENT,
SET_LIVE_TAIL_START_TIME,
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
import AppReducer from 'types/reducer/app';
import { ILogsReducer } from 'types/reducer/logs';
import OptionIcon from './OptionIcon';
import { TimePickerCard, TimePickerSelect } from './styles';
const { Option } = Select;
const TIME_PICKER_OPTIONS = [
{
value: 5,
label: '5m',
},
{
value: 15,
label: '15m',
},
{
value: 30,
label: '30m',
},
{
value: 60,
label: '1hr',
},
{
value: 360,
label: '6hrs',
},
{
value: 720,
label: '12hrs',
},
];
function LogLiveTail(): JSX.Element {
const {
liveTail,
searchFilter: { queryString },
liveTailStartRange,
logs,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const dispatch = useDispatch();
const handleLiveTail = (toggleState: TLogsLiveTailState): void => {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: toggleState,
});
};
const batchedEventsRef = useRef<Record<string, unknown>[]>([]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const pushLiveLog = useCallback(
throttle(() => {
dispatch({
type: PUSH_LIVE_TAIL_EVENT,
payload: batchedEventsRef.current.reverse(),
});
// console.log('DISPATCH', batchedEventsRef.current.length);
batchedEventsRef.current = [];
}, 1500),
[],
);
const batchLiveLog = (e: { data: string }): void => {
// console.log('EVENT BATCHED');
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
pushLiveLog();
};
// This ref depicts thats whether the live tail is played from paused state or not.
const liveTailSourceRef = useRef<EventSource | null>(null);
useEffect(() => {
if (liveTail === 'PLAYING') {
// console.log('Starting Live Tail', logs.length);
const timeStamp = dayjs().subtract(liveTailStartRange, 'minute').valueOf();
const queryParams = new URLSearchParams({
...(queryString ? { q: queryString } : {}),
timestampStart: (timeStamp * 1e6) as never,
...(liveTailSourceRef.current && logs.length > 0
? {
idGt: logs[0].id,
}
: {}),
});
const source = LiveTail(queryParams.toString());
liveTailSourceRef.current = source;
source.onmessage = function connectionMessage(e): void {
batchLiveLog(e);
};
// source.onopen = function connectionOpen(): void { };
source.onerror = function connectionError(event: unknown): void {
console.error(event);
source.close();
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: false,
});
};
} else if (liveTailSourceRef.current && liveTailSourceRef.current.close) {
liveTailSourceRef.current?.close();
}
if (liveTail === 'STOPPED') {
liveTailSourceRef.current = null;
}
}, [liveTail]);
const handleLiveTailStart = (): void => {
handleLiveTail('PLAYING');
if (!liveTailSourceRef.current) {
dispatch({
type: FLUSH_LOGS,
});
}
};
const OptionsPopOverContent = useMemo(
() => (
<TimePickerSelect
disabled={liveTail === 'PLAYING'}
value={liveTailStartRange}
onChange={(value): void => {
dispatch({
type: SET_LIVE_TAIL_START_TIME,
payload: value,
});
}}
>
{TIME_PICKER_OPTIONS.map((optionData) => (
<Option key={optionData.label} value={optionData.value}>
Last {optionData.label}
</Option>
))}
</TimePickerSelect>
),
[dispatch, liveTail, liveTailStartRange],
);
return (
<TimePickerCard>
<Row
style={{ gap: '0.5rem', alignItems: 'center', justifyContent: 'center' }}
>
<div>
{liveTail === 'PLAYING' ? (
<Button
type="primary"
onClick={(): void => handleLiveTail('PAUSED')}
title="Pause live tail"
style={{ background: green[6] }}
>
Pause <PauseOutlined />
</Button>
) : (
<Button
type="primary"
onClick={handleLiveTailStart}
title="Start live tail"
>
Go Live <PlayCircleOutlined />
</Button>
)}
{liveTail !== 'STOPPED' && (
<Button
type="dashed"
onClick={(): void => handleLiveTail('STOPPED')}
title="Exit live tail"
>
<div
style={{
height: '0.8rem',
width: '0.8rem',
background: isDarkMode ? '#eee' : '#222',
borderRadius: '0.1rem',
}}
/>
</Button>
)}
</div>
<Popover
placement="bottomRight"
title="Select Live Tail Timing"
trigger="click"
content={OptionsPopOverContent}
>
<span
style={{
padding: '0.3rem 0.4rem 0.3rem 0',
display: 'flex',
justifyContent: 'center',
alignContent: 'center',
}}
>
<OptionIcon isDarkMode={isDarkMode} />
</span>
</Popover>
</Row>
</TimePickerCard>
);
}
export default LogLiveTail;

View File

@ -0,0 +1,12 @@
import { Card, Select } from 'antd';
import styled from 'styled-components';
export const TimePickerCard = styled(Card)`
.ant-card-body {
padding: 0;
}
`;
export const TimePickerSelect = styled(Select)`
min-width: 100px;
`;

View File

@ -0,0 +1,71 @@
import { Divider, Row } from 'antd';
import LogControls from 'container/LogControls';
import LogDetailedView from 'container/LogDetailedView';
import LogLiveTail from 'container/LogLiveTail';
import LogsAggregate from 'container/LogsAggregate';
import LogsFilters from 'container/LogsFilters';
import SearchFilter from 'container/LogsSearchFilter';
import LogsTable from 'container/LogsTable';
import React, { memo, useEffect, useMemo } from 'react';
import { connect, useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetLogsFields } from 'store/actions/logs/getFields';
import AppActions from 'types/actions';
import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs';
interface LogsProps {
getLogsFields: VoidFunction;
}
function Logs({ getLogsFields }: LogsProps): JSX.Element {
const { search } = useLocation();
const urlQuery = useMemo(() => {
return new URLSearchParams(search);
}, [search]);
const dispatch = useDispatch();
useEffect(() => {
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: urlQuery.get('q'),
});
}, [dispatch, urlQuery]);
useEffect(() => {
getLogsFields();
}, [getLogsFields]);
return (
<div style={{ position: 'relative' }}>
<Row style={{ justifyContent: 'center', alignItems: 'center' }}>
<SearchFilter />
<Divider type="vertical" style={{ height: '2rem' }} />
<LogLiveTail />
</Row>
<LogsAggregate />
<LogControls />
<Divider style={{ margin: 0 }} />
<Row gutter={20} style={{ flexWrap: 'nowrap' }}>
<LogsFilters />
<Divider type="vertical" style={{ height: '100%', margin: 0 }} />
<LogsTable />
</Row>
<LogDetailedView />
</div>
);
}
interface DispatchProps {
getLogsFields: () => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(Logs));

View File

@ -0,0 +1,133 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { blue } from '@ant-design/colors';
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import dayjs from 'dayjs';
import getStep from 'lib/getStep';
import React, { memo, useEffect, useRef } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { Container } from './styles';
interface LogsAggregateProps {
getLogsAggregate: (arg0: Parameters<typeof getLogsAggregate>[0]) => void;
}
function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
const {
searchFilter: { queryString },
idEnd,
idStart,
isLoadingAggregate,
logsAggregate,
liveTail,
liveTailStartRange,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const reFetchIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
switch (liveTail) {
case 'STOPPED': {
if (reFetchIntervalRef.current) {
clearInterval(reFetchIntervalRef.current);
}
reFetchIntervalRef.current = null;
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: queryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
break;
}
case 'PLAYING': {
const aggregateCall = (): void => {
const startTime =
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
const endTime = dayjs().valueOf() * 1e6;
getLogsAggregate({
timestampStart: startTime,
timestampEnd: endTime,
step: getStep({
start: startTime,
end: endTime,
inputFormat: 'ns',
}),
q: queryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
};
aggregateCall();
reFetchIntervalRef.current = setInterval(aggregateCall, 60000);
break;
}
case 'PAUSED': {
if (reFetchIntervalRef.current) {
clearInterval(reFetchIntervalRef.current);
}
break;
}
default: {
break;
}
}
}, [getLogsAggregate, maxTime, minTime, liveTail]);
const data = {
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
data: logsAggregate.map((s) => s.value),
backgroundColor: blue[4],
},
],
};
return (
<Container>
{isLoadingAggregate ? (
<Spinner size="default" height="100%" />
) : (
<Graph
name="usage"
data={data}
type="bar"
containerHeight="100%"
animate={false}
/>
)}
</Container>
);
}
interface DispatchProps {
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(LogsAggregate));

View File

@ -0,0 +1,11 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`
position: relative;
margin: 0.5rem 0;
.ant-card-body {
height: 20vh;
min-height: 200px;
}
`;

View File

@ -0,0 +1,57 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Popover, Spin } from 'antd';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { Field } from './styles';
interface FieldItemProps {
name: string;
buttonIcon: React.ReactNode;
buttonOnClick: (arg0: Record<string, unknown>) => void;
fieldData: Record<string, never>;
fieldIndex: number;
isLoading: boolean;
iconHoverText: string;
}
export function FieldItem({
name,
buttonIcon,
buttonOnClick,
fieldData,
fieldIndex,
isLoading,
iconHoverText,
}: FieldItemProps): JSX.Element {
const [isHovered, setIsHovered] = useState(false);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
return (
<Field
onMouseEnter={(): void => {
setIsHovered(true);
}}
onMouseLeave={(): void => setIsHovered(false)}
isDarkMode={isDarkMode}
>
<span>{name}</span>
{isLoading ? (
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />
) : (
isHovered &&
buttonOnClick && (
<Popover content={<span>{iconHoverText}</span>}>
<Button
type="text"
size="small"
icon={buttonIcon}
onClick={(): void => buttonOnClick({ fieldData, fieldIndex })}
style={{ color: 'inherit', padding: 0, height: '1rem', width: '1rem' }}
/>
</Popover>
)
)}
</Field>
);
}

View File

@ -0,0 +1,153 @@
/* eslint-disable react/no-array-index-key */
import { red } from '@ant-design/colors';
import { CloseOutlined, PlusCircleFilled } from '@ant-design/icons';
import { Input } from 'antd';
import AddToSelectedFields from 'api/logs/AddToSelectedField';
import RemoveSelectedField from 'api/logs/RemoveFromSelectedField';
import CategoryHeading from 'components/Logs/CategoryHeading';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import React, { memo, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetLogsFields } from 'store/actions/logs/getFields';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { IInterestingFields, ISelectedFields } from 'types/api/logs/fields';
import { ILogsReducer } from 'types/reducer/logs';
import { FieldItem } from './FieldItem';
import { CategoryContainer, Container, FieldContainer } from './styles';
const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
interface LogsFiltersProps {
getLogsFields: () => void;
}
function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element {
const {
fields: { interesting, selected },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const [selectedFieldLoading, setSelectedFieldLoading] = useState<number[]>([]);
const [interestingFieldLoading, setInterestingFieldLoading] = useState<
number[]
>([]);
const [filterValuesInput, setFilterValuesInput] = useState('');
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>): void => {
setFilterValuesInput((e.target as HTMLInputElement).value);
};
const handleAddInterestingToSelected = async ({
fieldData,
fieldIndex,
}: {
fieldData: IInterestingFields;
fieldIndex: number;
}): Promise<void> => {
setInterestingFieldLoading((prevState: number[]) => {
prevState.push(fieldIndex);
return [...prevState];
});
await AddToSelectedFields({
...fieldData,
selected: true,
});
getLogsFields();
setInterestingFieldLoading(
interestingFieldLoading.filter((e) => e !== fieldIndex),
);
};
const handleRemoveSelectedField = async ({
fieldData,
fieldIndex,
}: {
fieldData: ISelectedFields;
fieldIndex: number;
}): Promise<void> => {
setSelectedFieldLoading((prevState) => {
prevState.push(fieldIndex);
return [...prevState];
});
await RemoveSelectedField({
...fieldData,
selected: false,
});
getLogsFields();
setSelectedFieldLoading(
interestingFieldLoading.filter((e) => e !== fieldIndex),
);
};
return (
<Container flex="450px">
<Input
placeholder="Filter Values"
onInput={handleSearch}
style={{ width: '100%' }}
value={filterValuesInput}
onChange={handleSearch}
/>
<CategoryContainer>
<CategoryHeading>SELECTED FIELDS</CategoryHeading>
<FieldContainer>
{selected
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
.map((field, idx) => (
<FieldItem
key={`${JSON.stringify(field)}-${idx}`}
name={field.name}
fieldData={field as never}
fieldIndex={idx}
buttonIcon={<CloseOutlined style={{ color: red[5] }} />}
buttonOnClick={
(!RESTRICTED_SELECTED_FIELDS.includes(field.name) &&
handleRemoveSelectedField) as never
}
isLoading={selectedFieldLoading.includes(idx)}
iconHoverText="Remove from Selected Fields"
/>
))}
</FieldContainer>
</CategoryContainer>
<CategoryContainer>
<CategoryHeading>INTERESTING FIELDS</CategoryHeading>
<FieldContainer>
{interesting
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
.map((field, idx) => (
<FieldItem
key={`${JSON.stringify(field)}-${idx}`}
name={field.name}
fieldData={field as never}
fieldIndex={idx}
buttonIcon={<PlusCircleFilled />}
buttonOnClick={handleAddInterestingToSelected as never}
isLoading={interestingFieldLoading.includes(idx)}
iconHoverText="Add to Selected Fields"
/>
))}
</FieldContainer>
</CategoryContainer>
{/* <ExtractField>Extract Fields</ExtractField> */}
</Container>
);
}
interface DispatchProps {
getLogsFields: () => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(LogsFilters));

View File

@ -0,0 +1,36 @@
import { blue, grey } from '@ant-design/colors';
import { Col, Typography } from 'antd';
import styled from 'styled-components';
export const Container = styled(Col)`
padding-top: 0.3rem;
min-width: 250px;
max-width: 350px;
`;
export const CategoryContainer = styled.div`
margin: 1rem 0;
padding-left: 0.2rem;
`;
export const FieldContainer = styled(Typography.Text)`
margin: 0.2rem 0;
color: ${blue[4]};
`;
export const Field = styled.div<{ isDarkMode: boolean }>`
border-radius: 0.5rem;
padding: 0.3rem 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
&:hover {
background: ${({ isDarkMode }): string => {
return isDarkMode ? grey[7] : '#ddd';
}};
}
`;
export const ExtractField = styled(Typography.Text)`
color: ${blue[4]};
`;

View File

@ -0,0 +1,20 @@
import { Typography } from 'antd';
import React from 'react';
interface FieldKeyProps {
name: string;
type: string;
}
function FieldKey({ name, type }: FieldKeyProps): JSX.Element {
return (
<span style={{ margin: '0.25rem 0', display: 'flex', gap: '0.5rem' }}>
<Typography.Text>{name}</Typography.Text>
<Typography.Text type="secondary" italic>
{type}
</Typography.Text>
</span>
);
}
export default FieldKey;

View File

@ -0,0 +1,263 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-bitwise */
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable no-param-reassign */
/* eslint-disable react/no-array-index-key */
/* eslint-disable react-hooks/exhaustive-deps */
import { CloseOutlined } from '@ant-design/icons';
import { Button, Input, Select } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import {
ConditionalOperators,
QueryOperatorsMultiVal,
QueryOperatorsSingleVal,
} from 'lib/logql/tokens';
import { flatten } from 'lodash-es';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
import { v4 } from 'uuid';
import FieldKey from '../FieldKey';
import { QueryConditionContainer, QueryFieldContainer } from '../styles';
import { createParsedQueryStructure } from '../utils';
const { Option } = Select;
interface QueryFieldProps {
query: { value: string | string[]; type: string }[];
queryIndex: number;
onUpdate: (query: unknown, queryIndex: number) => void;
onDelete: (queryIndex: number) => void;
}
function QueryField({
query,
queryIndex,
onUpdate,
onDelete,
}: QueryFieldProps): JSX.Element | null {
const {
fields: { selected },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const getFieldType = (inputKey: string): string => {
// eslint-disable-next-line no-restricted-syntax
for (const selectedField of selected) {
if (inputKey === selectedField.name) {
return selectedField.type;
}
}
return '';
};
const fieldType = useMemo(() => getFieldType(query[0].value as string), [
query,
]);
const handleChange = (qIdx: number, value: string): void => {
query[qIdx].value = value || '';
if (qIdx === 1) {
if (Object.values(QueryOperatorsMultiVal).includes(value)) {
if (!Array.isArray(query[2].value)) {
query[2].value = [];
}
} else if (
Object.values(QueryOperatorsSingleVal).includes(value) &&
Array.isArray(query[2].value)
) {
query[2].value = '';
}
}
onUpdate(query, queryIndex);
};
const handleClear = (): void => {
onDelete(queryIndex);
};
if (!Array.isArray(query)) {
return null;
}
return (
<QueryFieldContainer
style={{ ...(queryIndex === 0 && { gridColumnStart: 2 }) }}
>
<div style={{ flex: 1, minWidth: 100 }}>
<FieldKey name={(query[0] && query[0].value) as string} type={fieldType} />
</div>
<Select
defaultActiveFirstOption={false}
placeholder="Select Operator"
defaultValue={
query[1] && query[1].value
? (query[1].value as string).toUpperCase()
: null
}
onChange={(e): void => handleChange(1, e)}
style={{ minWidth: 150 }}
>
{Object.values({
...QueryOperatorsMultiVal,
...QueryOperatorsSingleVal,
}).map((cond) => (
<Option key={cond} value={cond} label={cond}>
{cond}
</Option>
))}
</Select>
<div style={{ flex: 2 }}>
{Array.isArray(query[2].value) ||
Object.values(QueryOperatorsMultiVal).some(
(op) => op.toUpperCase() === (query[1].value as string)?.toUpperCase(),
) ? (
<Select
mode="tags"
style={{ width: '100%' }}
onChange={(e): void => handleChange(2, e as never)}
defaultValue={(query[2] && query[2].value) || []}
notFoundContent={null}
/>
) : (
<Input
onChange={(e): void => handleChange(2, e.target.value)}
style={{ width: '100%' }}
defaultValue={query[2] && query[2].value}
/>
)}
</div>
<Button
icon={<CloseOutlined />}
type="text"
size="small"
onClick={handleClear}
/>
</QueryFieldContainer>
);
}
interface QueryConditionFieldProps {
query: { value: string | string[]; type: string }[];
queryIndex: number;
onUpdate: (arg0: unknown, arg1: number) => void;
}
function QueryConditionField({
query,
queryIndex,
onUpdate,
}: QueryConditionFieldProps): JSX.Element {
return (
<QueryConditionContainer>
<Select
defaultValue={
(query as any).value &&
(((query as any)?.value as any) as string).toUpperCase()
}
onChange={(e): void => {
onUpdate({ ...query, value: e }, queryIndex);
}}
style={{ width: '100%' }}
>
{Object.values(ConditionalOperators).map((cond) => (
<Option key={cond} value={cond} label={cond}>
{cond}
</Option>
))}
</Select>
</QueryConditionContainer>
);
}
const hashCode = (s: string): string => {
if (!s) {
return '0';
}
return `${Math.abs(
s.split('').reduce((a, b) => {
a = (a << 5) - a + b.charCodeAt(0);
return a & a;
}, 0),
)}`;
};
function QueryBuilder({
updateParsedQuery,
}: {
updateParsedQuery: (arg0: unknown) => void;
}): JSX.Element {
const {
searchFilter: { parsedQuery },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const keyPrefixRef = useRef(hashCode(JSON.stringify(parsedQuery)));
const [keyPrefix, setKeyPrefix] = useState(keyPrefixRef.current);
const generatedQueryStructure = createParsedQueryStructure(
parsedQuery as never[],
);
useEffect(() => {
const incomingHashCode = hashCode(JSON.stringify(parsedQuery));
if (incomingHashCode !== keyPrefixRef.current) {
keyPrefixRef.current = incomingHashCode;
setKeyPrefix(incomingHashCode);
}
}, [parsedQuery]);
const handleUpdate = (
query: { value: string | string[]; type: string }[],
queryIndex: number,
): void => {
const updatedParsedQuery = generatedQueryStructure;
updatedParsedQuery[queryIndex] = query as never;
const flatParsedQuery = flatten(updatedParsedQuery).filter((q) => q.value);
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
updateParsedQuery(flatParsedQuery);
};
const handleDelete = (queryIndex: number): void => {
const updatedParsedQuery = generatedQueryStructure;
updatedParsedQuery.splice(queryIndex - 1, 2);
const flatParsedQuery = flatten(updatedParsedQuery).filter((q) => q.value);
keyPrefixRef.current = v4();
updateParsedQuery(flatParsedQuery);
};
const QueryUI = (): JSX.Element | JSX.Element[] =>
generatedQueryStructure.map((query, idx) => {
if (Array.isArray(query))
return (
<QueryField
key={keyPrefix + idx}
query={query as never}
queryIndex={idx}
onUpdate={handleUpdate as never}
onDelete={handleDelete}
/>
);
return (
<QueryConditionField
key={keyPrefix + idx}
query={query}
queryIndex={idx}
onUpdate={handleUpdate as never}
/>
);
});
return (
<div>
<CategoryHeading>LOG QUERY BUILDER</CategoryHeading>
<div
style={{
display: 'grid',
gridTemplateColumns: '80px 1fr',
margin: '0.5rem 0',
}}
>
{QueryUI()}
</div>
</div>
);
}
export default QueryBuilder;

View File

@ -0,0 +1,57 @@
import { Button } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import { map } from 'lodash-es';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ADD_SEARCH_FIELD_QUERY_STRING } from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs';
import FieldKey from './FieldKey';
interface SuggestedItemProps {
name: string;
type: string;
}
function SuggestedItem({ name, type }: SuggestedItemProps): JSX.Element {
const dispatch = useDispatch();
const addSuggestedField = (): void => {
dispatch({
type: ADD_SEARCH_FIELD_QUERY_STRING,
payload: name,
});
};
return (
<Button
type="text"
style={{ display: 'block', padding: '0.2rem' }}
onClick={addSuggestedField}
>
<FieldKey name={name} type={type} />
</Button>
);
}
function Suggestions(): JSX.Element {
const {
fields: { selected },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
return (
<div>
<CategoryHeading>SUGGESTIONS</CategoryHeading>
<div>
{map(selected, (field) => (
<SuggestedItem
key={JSON.stringify(field)}
name={field.name}
type={field.type}
/>
))}
</div>
</div>
);
}
export default Suggestions;

View File

@ -0,0 +1,17 @@
import React from 'react';
import QueryBuilder from './QueryBuilder/QueryBuilder';
import Suggestions from './Suggestions';
interface SearchFieldsProps {
updateParsedQuery: () => void;
}
function SearchFields({ updateParsedQuery }: SearchFieldsProps): JSX.Element {
return (
<>
<QueryBuilder updateParsedQuery={updateParsedQuery} />
<Suggestions />
</>
);
}
export default SearchFields;

View File

@ -0,0 +1,20 @@
import { blue } from '@ant-design/colors';
import styled from 'styled-components';
export const QueryFieldContainer = styled.div`
padding: 0.25rem 0.5rem;
margin: 0.1rem 0.5rem 0;
display: flex;
flex-direction: row;
align-items: center;
border-radius: 0.25rem;
gap: 1rem;
&:hover {
background: ${blue[6]};
}
`;
export const QueryConditionContainer = styled.div`
padding: 0.25rem 0rem;
margin: 0.1rem 0;
`;

View File

@ -0,0 +1,61 @@
/* eslint-disable */
// @ts-ignore
// @ts-nocheck
import { QueryTypes } from 'lib/logql/tokens';
export const queryKOVPair = () => [
{
type: QueryTypes.QUERY_KEY,
value: null,
},
{
type: QueryTypes.QUERY_OPERATOR,
value: null,
},
{
type: QueryTypes.QUERY_VALUE,
value: null,
},
];
export const createParsedQueryStructure = (parsedQuery = []) => {
if (!parsedQuery.length) {
return parsedQuery;
}
const structuredArray = [queryKOVPair()];
let cond;
let qCtr = -1;
parsedQuery.forEach((query, idx) => {
if (cond) {
structuredArray.push(cond);
structuredArray.push(queryKOVPair());
cond = null;
qCtr = -1;
}
const stagingArr = structuredArray[structuredArray.length - 1];
const prevQuery =
Array.isArray(stagingArr) && qCtr >= 0 ? stagingArr[qCtr] : null;
if (query.type === QueryTypes.QUERY_KEY) {
stagingArr[qCtr + 1] = query;
} else if (
query.type === QueryTypes.QUERY_OPERATOR &&
prevQuery &&
prevQuery.type === QueryTypes.QUERY_KEY
) {
stagingArr[qCtr + 1] = query;
} else if (
query.type === QueryTypes.QUERY_VALUE &&
prevQuery &&
prevQuery.type === QueryTypes.QUERY_OPERATOR
) {
stagingArr[qCtr + 1] = query;
} else if (query.type === QueryTypes.CONDITIONAL_OPERATOR) {
cond = query;
}
qCtr++;
});
return structuredArray;
};

View File

@ -0,0 +1,178 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { CloseSquareOutlined } from '@ant-design/icons';
import { Button, Input } from 'antd';
import useClickOutside from 'hooks/useClickOutside';
import getStep from 'lib/getStep';
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-use';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { TOGGLE_LIVE_TAIL } from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import SearchFields from './SearchFields';
import { DropDownContainer } from './styles';
import { useSearchParser } from './useSearchParser';
const { Search } = Input;
interface SearchFilterProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => ReturnType<typeof getLogsAggregate>;
}
function SearchFilter({
getLogs,
getLogsAggregate,
}: SearchFilterProps): JSX.Element {
const {
queryString,
updateParsedQuery,
updateQueryString,
} = useSearchParser();
const [showDropDown, setShowDropDown] = useState(false);
const { logLinesPerPage, idEnd, idStart, liveTail } = useSelector<
AppState,
ILogsReducer
>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const searchComponentRef = useRef<HTMLDivElement>(null);
useClickOutside(searchComponentRef, (e: HTMLElement) => {
// using this hack as overlay span is voilating this condition
if (
e.nodeName === 'svg' ||
e.nodeName === 'path' ||
e.nodeName === 'span' ||
e.nodeName === 'button'
) {
return;
}
if (
e.nodeName === 'DIV' &&
![
'ant-empty-image',
'ant-select-item',
'ant-col',
'ant-select-item-option-content',
'ant-select-item-option-active',
].find((p) => p.indexOf(e.className) !== -1) &&
!(e.ariaSelected === 'true') &&
showDropDown
) {
setShowDropDown(false);
}
});
const { search } = useLocation();
const dispatch = useDispatch();
const handleSearch = (customQuery = ''): void => {
if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'PAUSED',
});
setTimeout(
() =>
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
}),
0,
);
} else {
getLogs({
q: customQuery || queryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order: 'desc',
timestampStart: minTime,
timestampEnd: maxTime,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: customQuery || queryString,
});
}
setShowDropDown(false);
};
const urlQuery = useMemo(() => {
return new URLSearchParams(search);
}, [search]);
useEffect(() => {
const urlQueryString = urlQuery.get('q');
if (urlQueryString !== null) handleSearch(urlQueryString);
}, []);
return (
<div ref={searchComponentRef} style={{ flex: 1 }}>
<Search
placeholder="Search Filter"
onFocus={(): void => setShowDropDown(true)}
value={queryString}
onChange={(e): void => {
updateQueryString(e.target.value);
}}
onSearch={handleSearch}
/>
<div style={{ position: 'relative' }}>
{showDropDown && (
<DropDownContainer>
<Button
type="text"
onClick={(): void => setShowDropDown(false)}
style={{
position: 'absolute',
top: 0,
right: 0,
}}
>
<CloseSquareOutlined />
</Button>
<SearchFields updateParsedQuery={updateParsedQuery as never} />
</DropDownContainer>
)}
</div>
</div>
);
}
interface DispatchProps {
getLogs: (
props: Parameters<typeof getLogs>[0],
) => (dispatch: Dispatch<AppActions>) => void;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(SearchFilter));

View File

@ -0,0 +1,12 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const DropDownContainer = styled(Card)`
top: 0.5rem;
position: absolute;
width: 100%;
z-index: 1;
.ant-card-body {
padding: 0.8rem;
}
`;

View File

@ -0,0 +1,76 @@
import history from 'lib/history';
import { parseQuery, reverseParser } from 'lib/logql';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import {
SET_SEARCH_QUERY_PARSED_PAYLOAD,
SET_SEARCH_QUERY_STRING,
} from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs';
export function useSearchParser(): {
queryString: string;
parsedQuery: unknown;
updateParsedQuery: (arg0: unknown) => void;
updateQueryString: (arg0: unknown) => void;
} {
const dispatch = useDispatch();
const {
searchFilter: { parsedQuery, queryString },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const updateQueryString = useCallback(
(updatedQueryString) => {
history.push({
pathname: history.location.pathname,
search: updatedQueryString ? `?q=${updatedQueryString}` : '',
});
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: updatedQueryString,
});
const parsedQueryFromString = parseQuery(updatedQueryString);
if (!isEqual(parsedQuery, parsedQueryFromString)) {
dispatch({
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
payload: parsedQueryFromString,
});
}
},
[dispatch, parsedQuery],
);
useEffect(() => {
if (queryString !== null) updateQueryString(queryString);
}, [queryString, updateQueryString]);
const updateParsedQuery = useCallback(
(updatedParsedPayload) => {
dispatch({
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
payload: updatedParsedPayload,
});
const reversedParsedQuery = reverseParser(updatedParsedPayload);
if (
!isEqual(queryString, reversedParsedQuery) ||
(queryString === '' && reversedParsedQuery === '')
) {
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: reversedParsedQuery,
});
}
},
[dispatch, queryString],
);
return {
queryString,
parsedQuery,
updateParsedQuery,
updateQueryString,
};
}

View File

@ -0,0 +1,89 @@
/* eslint-disable no-nested-ternary */
import { Typography } from 'antd';
import LogItem from 'components/Logs/LogItem';
import Spinner from 'components/Spinner';
import { map } from 'lodash-es';
import React, { memo, useEffect } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { Container, Heading } from './styles';
interface LogsTableProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
}
function LogsTable({ getLogs }: LogsTableProps): JSX.Element {
const {
searchFilter: { queryString },
logs,
logLinesPerPage,
idEnd,
idStart,
isLoading,
liveTail,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
useEffect(() => {
if (liveTail === 'STOPPED')
getLogs({
q: queryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order: 'desc',
timestampStart: minTime,
timestampEnd: maxTime,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getLogs, idEnd, idStart, logLinesPerPage, maxTime, minTime, liveTail]);
if (isLoading) {
return <Spinner height={20} tip="Getting Logs" />;
}
return (
<Container flex="auto">
<Heading>
<Typography.Text
style={{
fontSize: '1rem',
fontWeight: 400,
}}
>
Event
</Typography.Text>
</Heading>
{Array.isArray(logs) && logs.length > 0 ? (
map(logs, (log) => <LogItem key={log.id} logData={log} />)
) : liveTail === 'PLAYING' ? (
<span>Getting live logs...</span>
) : (
<span>No log lines found</span>
)}
</Container>
);
}
interface DispatchProps {
getLogs: (
props: Parameters<typeof getLogs>[0],
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(LogsTable));

View File

@ -0,0 +1,15 @@
import { Card, Col } from 'antd';
import styled from 'styled-components';
export const Container = styled(Col)`
overflow-x: hidden;
width: 100%;
margin-bottom: 1rem;
`;
export const Heading = styled(Card)`
margin-bottom: 0.1rem;
.ant-card-body {
padding: 0.3rem 0.5rem;
}
`;

View File

@ -1,9 +1,13 @@
import Table, { ColumnsType } from 'antd/lib/table';
import { blue } from '@ant-design/colors';
import { SearchOutlined } from '@ant-design/icons';
import { Button, Input, Space, Table } from 'antd';
import type { ColumnsType, ColumnType } from 'antd/es/table';
import type { FilterConfirmProps } from 'antd/es/table/interface';
import localStorageGet from 'api/browser/localstorage/get';
import localStorageSet from 'api/browser/localstorage/set';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import ROUTES from 'constants/routes';
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
@ -27,6 +31,57 @@ function Metrics(): JSX.Element {
localStorageSet(SKIP_ONBOARDING, 'true');
setSkipOnboarding(true);
};
const handleSearch = (confirm: (param?: FilterConfirmProps) => void): void => {
confirm();
};
const FilterIcon = useCallback(
({ filtered }) => (
<SearchOutlined
style={{
color: filtered ? blue[6] : undefined,
}}
/>
),
[],
);
const filterDropdown = useCallback(
({ setSelectedKeys, selectedKeys, confirm }) => (
<div
style={{
padding: 8,
}}
>
<Input
placeholder="Search by service"
value={selectedKeys[0]}
onChange={(e): void =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
allowClear
onPressEnter={(): void => handleSearch(confirm)}
style={{
marginBottom: 8,
}}
/>
<Space>
<Button
type="primary"
onClick={(): void => handleSearch(confirm)}
icon={<SearchOutlined />}
size="small"
style={{
width: 90,
}}
>
Search
</Button>
</Space>
</div>
),
[],
);
if (
services.length === 0 &&
@ -37,21 +92,37 @@ function Metrics(): JSX.Element {
return <SkipBoardModal onContinueClick={onContinueClick} />;
}
const columns: ColumnsType<DataProps> = [
{
title: 'Application',
dataIndex: 'serviceName',
key: 'serviceName',
type DataIndex = keyof ServicesList;
const getColumnSearchProps = (
dataIndex: DataIndex,
): ColumnType<DataProps> => ({
filterDropdown,
filterIcon: FilterIcon,
onFilter: (value: string | number | boolean, record: DataProps): boolean =>
record[dataIndex]
.toString()
.toLowerCase()
.includes(value.toString().toLowerCase()),
render: (text: string): JSX.Element => (
<Link to={`${ROUTES.APPLICATION}/${text}${search}`}>
<Name>{text}</Name>
</Link>
),
});
const columns: ColumnsType<DataProps> = [
{
title: 'Application',
dataIndex: 'serviceName',
key: 'serviceName',
...getColumnSearchProps('serviceName'),
},
{
title: 'P99 latency (in ms)',
dataIndex: 'p99',
key: 'p99',
defaultSortOrder: 'descend',
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
render: (value: number): string => (value / 1000000).toFixed(2),
},

View File

@ -37,6 +37,14 @@ function MetricsBuilderFormula({
style={{ marginBottom: '0.5rem' }}
rows={2}
/>
<Input
onChange={(event): void => {
handleFormulaChange({ formulaIndex, legend: event.target.value });
}}
size="middle"
defaultValue={formulaData.legend}
addonBefore="Legend Format"
/>
</QueryHeader>
);
}

View File

@ -85,6 +85,7 @@ function QueryBuilderQueryContainer({
const handleQueryBuilderFormulaChange = ({
formulaIndex,
expression,
legend,
toggleDisable,
toggleDelete,
}: IQueryBuilderFormulaHandleChange): void => {
@ -94,9 +95,12 @@ function QueryBuilderQueryContainer({
];
const currentIndexFormula = allFormulas[formulaIndex as number];
if (expression) {
if (expression !== undefined) {
currentIndexFormula.expression = expression;
}
if (legend !== undefined) {
currentIndexFormula.legend = legend;
}
if (toggleDisable) {
currentIndexFormula.disabled = !currentIndexFormula.disabled;

View File

@ -19,5 +19,6 @@ export interface IQueryBuilderFormulaHandleChange {
formulaIndex: number | string;
expression?: IMetricsBuilderFormula['expression'];
toggleDisable?: IMetricsBuilderFormula['disabled'];
legend?: IMetricsBuilderFormula['legend'];
toggleDelete?: boolean;
}

View File

@ -7,6 +7,7 @@ import {
DashboardFilled,
DeploymentUnitOutlined,
LineChartOutlined,
MenuOutlined,
SettingOutlined,
} from '@ant-design/icons';
import ROUTES from 'constants/routes';
@ -18,10 +19,16 @@ const menus: SidebarMenu[] = [
name: 'Services',
},
{
Icon: AlignLeftOutlined,
Icon: MenuOutlined,
to: ROUTES.TRACE,
name: 'Traces',
},
{
Icon: AlignLeftOutlined,
to: ROUTES.LOGS,
name: 'Logs',
tags: ['Beta'],
},
{
Icon: DashboardFilled,
to: ROUTES.ALL_DASHBOARD,
@ -41,7 +48,6 @@ const menus: SidebarMenu[] = [
to: ROUTES.SERVICE_MAP,
name: 'Service Map',
Icon: DeploymentUnitOutlined,
tags: ['Beta'],
},
{
Icon: LineChartOutlined,

View File

@ -18,6 +18,7 @@ const breadcrumbNameMap = {
[ROUTES.ERROR_DETAIL]: 'Errors',
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
[ROUTES.LOGS]: 'Logs',
};
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {

View File

@ -45,6 +45,10 @@ export const ServiceMapOptions: Option[] = [
{ value: '5min', label: 'Last 5 min' },
{ value: '15min', label: 'Last 15 min' },
{ value: '30min', label: 'Last 30 min' },
{ value: '1hr', label: 'Last 1 hour' },
{ value: '6hr', label: 'Last 6 hour' },
{ value: '1day', label: 'Last 1 day' },
{ value: '1week', label: 'Last 1 week' },
];
export const getDefaultOption = (route: string): Time => {

View File

@ -0,0 +1,209 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint no-useless-escape: 0 */
const logqlQueries = [
{
query: "OPERATION in ('bcd','xy\\'z') AND FULLTEXT contains 'helloxyz'", // Query with IN
splitterQuery: [
'OPERATION',
'in',
"('bcd','xy\\'z')",
'AND',
'FULLTEXT',
'contains',
"'helloxyz'",
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'OPERATION' },
{ type: 'QUERY_OPERATOR', value: 'in' },
{ type: 'QUERY_VALUE', value: ['bcd', "xy\\'z"] },
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
],
},
{
query: "OPERATION in ('bcd') and FULLTEXT contains 'helloxyz'", // Query with IN
splitterQuery: [
'OPERATION',
'in',
"('bcd')",
'and',
'FULLTEXT',
'contains',
"'helloxyz'",
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'OPERATION' },
{ type: 'QUERY_OPERATOR', value: 'in' },
{ type: 'QUERY_VALUE', value: ['bcd'] },
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
],
},
{
query: "OPERATION in ('bcd','xyz') AND FULLTEXT contains 'helloxyz'", // Query with IN
splitterQuery: [
'OPERATION',
'in',
"('bcd','xyz')",
'AND',
'FULLTEXT',
'contains',
"'helloxyz'",
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'OPERATION' },
{ type: 'QUERY_OPERATOR', value: 'in' },
{ type: 'QUERY_VALUE', value: ['bcd', 'xyz'] },
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
],
},
{
query: "status gte 200 AND FULLTEXT contains 'helloxyz'",
splitterQuery: [
'status',
'gte',
'200',
'AND',
'FULLTEXT',
'contains',
"'helloxyz'",
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'status' },
{ type: 'QUERY_OPERATOR', value: 'gte' },
{ type: 'QUERY_VALUE', value: '200' },
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
],
},
{
query: "service IN ('Hello (\\'World')", // Query with quotes and brackets
splitterQuery: ['service', 'IN', "('Hello (\\'World')"],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'service' },
{ type: 'QUERY_OPERATOR', value: 'IN' },
{ type: 'QUERY_VALUE', value: ["Hello (\\'World"] },
],
},
{
query: "service IN ('Hello (\\'World') AND FULLTEXT contains 'ola'", // Query with full text as key pair
splitterQuery: [
'service',
'IN',
"('Hello (\\'World')",
'AND',
'FULLTEXT',
'contains',
"'ola'",
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'service' },
{ type: 'QUERY_OPERATOR', value: 'IN' },
{ type: 'QUERY_VALUE', value: ["Hello (\\'World"] },
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: "'ola'" },
],
},
{
query: 'id lt 100 and id gt 50 and code lte 500 and code gte 400', // Query with numbers
splitterQuery: [
'id',
'lt',
'100',
'and',
'id',
'gt',
'50',
'and',
'code',
'lte',
'500',
'and',
'code',
'gte',
'400',
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'id' },
{ type: 'QUERY_OPERATOR', value: 'lt' },
{ type: 'QUERY_VALUE', value: '100' },
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
{ type: 'QUERY_KEY', value: 'id' },
{ type: 'QUERY_OPERATOR', value: 'gt' },
{ type: 'QUERY_VALUE', value: '50' },
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
{ type: 'QUERY_KEY', value: 'code' },
{ type: 'QUERY_OPERATOR', value: 'lte' },
{ type: 'QUERY_VALUE', value: '500' },
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
{ type: 'QUERY_KEY', value: 'code' },
{ type: 'QUERY_OPERATOR', value: 'gte' },
{ type: 'QUERY_VALUE', value: '400' },
],
},
{
query: 'FULLTEXT contains \'Hello, "World"\'', // Full text with Quotes
splitterQuery: ['FULLTEXT', 'contains', `'Hello, "World"'`],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: `'Hello, "World"'` },
],
},
{
query: "service NIN ('name > 100') AND length gt 100", // Characters inside string
splitterQuery: [
'service',
'NIN',
"('name > 100')",
'AND',
'length',
'gt',
'100',
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'service' },
{ type: 'QUERY_OPERATOR', value: 'NIN' },
{ type: 'QUERY_VALUE', value: ['name > 100'] },
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
{ type: 'QUERY_KEY', value: 'length' },
{ type: 'QUERY_OPERATOR', value: 'gt' },
{ type: 'QUERY_VALUE', value: '100' },
],
},
// Template
{
query: '',
splitterQuery: [],
parsedQuery: [],
},
];
const specialLogQLQueries = [
{
query: 'key IN 22', // Fulltext
splitterQuery: ['key', 'IN', '22'],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'CONTAINS' },
{ type: 'QUERY_VALUE', value: 'key IN 22' },
],
reverseParsed: "FULLTEXT CONTAINS 'key IN 22'",
},
];
export { logqlQueries, specialLogQLQueries };

View File

@ -0,0 +1,15 @@
import { logqlQueries } from 'lib/__fixtures__/logql';
import parser from 'lib/logql/parser';
describe('lib/logql/parser', () => {
test('parse valid queries', () => {
logqlQueries.forEach((queryObject) => {
try {
parser(queryObject.query);
} catch (e) {
console.log(e);
}
expect(parser(queryObject.query)).toEqual(queryObject.parsedQuery);
});
});
});

View File

@ -0,0 +1,14 @@
import { logqlQueries } from 'lib/__fixtures__/logql';
import { reverseParser } from 'lib/logql/reverseParser';
describe('lib/logql/reverseParser', () => {
test('reverse parse valid queries', () => {
logqlQueries.forEach((queryObject) => {
try {
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
} catch (e) {
console.log(e);
}
});
});
});

View File

@ -0,0 +1,10 @@
import { logqlQueries } from 'lib/__fixtures__/logql';
import { splitter } from 'lib/logql/splitter';
describe('lib/logql/splitter', () => {
test('splitter valid quereies', () => {
logqlQueries.forEach((queryObject) => {
expect(splitter(queryObject.query)).toEqual(queryObject.splitterQuery);
});
});
});

View File

@ -0,0 +1,7 @@
export class ErrorConvertToFullText extends Error {
constructor(message: string) {
super(message);
this.name = 'ErrorConvertToFullText';
Object.setPrototypeOf(this, ErrorConvertToFullText.prototype);
}
}

View File

@ -0,0 +1,7 @@
export class ErrorInvalidQueryPair extends Error {
constructor(message: string) {
super(message);
this.name = 'ErrorInvalidQueryPair';
Object.setPrototypeOf(this, ErrorInvalidQueryPair.prototype);
}
}

View File

@ -0,0 +1,2 @@
export * from './ConvertToFullText';
export * from './InvalidQueryPair';

View File

@ -0,0 +1,4 @@
export * from './parser';
export * from './reverseParser';
export * from './splitter';
export * from './tokens';

View File

@ -0,0 +1,152 @@
/* eslint-disable */
// @ts-ignore
// @ts-nocheck
import {
ErrorConvertToFullText,
ErrorInvalidQueryPair,
} from 'lib/logql/errors';
import splitter from 'lib/logql/splitter';
import {
ConditionalOperators,
QueryOperatorsMultiVal,
QueryOperatorsSingleVal,
QueryTypes,
} from 'lib/logql/tokens';
const validateMultiValue = (queryToken: string): boolean => {
const queryValues = [];
let start;
let isQuoteStart = false;
if (queryToken[0] === '(' && queryToken[queryToken.length - 1] === ')') {
for (let idx = 1; idx < queryToken.length - 1; idx += 1) {
if (queryToken[idx] === "'") {
if (queryToken[idx - 1] === '\\') {
// skip
} else if (isQuoteStart) {
isQuoteStart = false;
queryValues.push(queryToken.slice(start, idx));
} else {
isQuoteStart = true;
start = idx + 1;
}
}
}
} else {
return false;
}
return queryValues;
};
export const parseQuery = (queryString) => {
let parsedRaw = [];
const generateQuery = (queryToken) => {
const prevToken = parsedRaw[parsedRaw.length - 1];
// Is a QUERY_KEY
if (
prevToken === undefined ||
prevToken.type === QueryTypes.CONDITIONAL_OPERATOR
) {
parsedRaw.push({
type: QueryTypes.QUERY_KEY,
value: queryToken,
});
}
// Is a QUERY_OPERATOR
else if (prevToken && prevToken.type === QueryTypes.QUERY_KEY) {
if (
Object.values({
...QueryOperatorsMultiVal,
...QueryOperatorsSingleVal,
}).find((op) => op.toLowerCase() === queryToken.toLowerCase())
)
parsedRaw.push({
type: QueryTypes.QUERY_OPERATOR,
value: queryToken,
});
else {
throw new ErrorInvalidQueryPair(
'Expected conditional operator received',
queryToken,
);
}
}
// Is a QUERY_VALUE
else if (prevToken && prevToken.type === QueryTypes.QUERY_OPERATOR) {
// Check for multi value
let value = queryToken;
// if (
// typeof queryToken === 'string' &&
// queryToken.length >= 2 &&
// queryToken[0] === "'" &&
// queryToken[queryToken.length - 1] === "'"
// ) {
// value = queryToken.slice(1, queryToken.length - 1);
// }
if (
Object.values(QueryOperatorsMultiVal).some(
(operator) => operator.toLowerCase() === prevToken.value.toLowerCase(),
)
) {
value = validateMultiValue(queryToken);
if (value === false) {
throw new ErrorConvertToFullText();
}
}
parsedRaw.push({
type: QueryTypes.QUERY_VALUE,
value,
});
} else if (prevToken && prevToken.type === QueryTypes.QUERY_VALUE) {
if (
Object.values(ConditionalOperators).find(
(op) => op.toLowerCase() === queryToken.toLowerCase(),
)
)
parsedRaw.push({
type: QueryTypes.CONDITIONAL_OPERATOR,
value: queryToken,
});
else {
throw new ErrorInvalidQueryPair(
'Expected conditional operator received',
queryToken,
);
}
} else {
// Not a Key
}
};
try {
const spaceSplittedQUery = splitter(queryString);
spaceSplittedQUery.forEach((q) => {
generateQuery(q);
});
} catch (e: Error) {
if (e instanceof ErrorInvalidQueryPair) {
//
} else if (e instanceof ErrorConvertToFullText) {
parsedRaw = [
{
type: QueryTypes.QUERY_KEY,
value: 'FULLTEXT',
},
{
type: QueryTypes.QUERY_OPERATOR,
value: 'CONTAINS',
},
{
type: QueryTypes.QUERY_VALUE,
value: String.raw`${queryString}`,
},
];
}
}
// console.log(parsedRaw);
return parsedRaw;
};
export default parseQuery;

View File

@ -0,0 +1,25 @@
/* eslint-disable */
// @ts-ignore
// @ts-nocheck
export const reverseParser = (
parserQueryArr: { type: string; value: any }[] = [],
) => {
let queryString = '';
parserQueryArr.forEach((query) => {
if (queryString) {
queryString += ' ';
}
if (Array.isArray(query.value) && query.value.length > 0) {
queryString += `(${query.value.map((val) => `'${val}'`).join(',')})`;
} else {
queryString += query.value;
}
});
// console.log(queryString);
return queryString;
};
export default reverseParser;

View File

@ -0,0 +1,52 @@
/* eslint-disable */
// @ts-ignore
// @ts-nocheck
export const splitter = (queryString: string): string[] => {
const splittedParts: string[] = [];
let start = 0;
let isBracketStart = false;
let isQuoteStart = false;
const pushPart = (idx) => {
splittedParts.push(queryString.slice(start, idx));
start = idx + 1;
};
for (let idx = 0; idx < queryString.length; idx += 1) {
const currentChar = queryString[idx];
if (currentChar === ' ') {
if (!isBracketStart && !isQuoteStart) {
pushPart(idx);
}
} else if (currentChar === '(') {
isBracketStart = true;
} else if (currentChar === ')') {
if (queryString[idx - 1] !== '\\') {
pushPart(idx + 1);
isBracketStart = false;
}
if (isQuoteStart) {
isQuoteStart = false;
}
} else if (currentChar === "'") {
if (isQuoteStart) {
if (queryString[idx - 1] !== '\\' && !isBracketStart) {
pushPart(idx + 1);
isQuoteStart = false;
}
} else {
isQuoteStart = true;
}
}
}
// Process remaining part
if (start < queryString.length) {
pushPart(queryString.length);
}
return splittedParts.map((s) => String.raw`${s}`).filter(Boolean);
};
export default splitter;

View File

@ -0,0 +1,25 @@
export const QueryOperatorsSingleVal = {
GTE: 'GTE',
GT: 'GT',
LTE: 'LTE',
LT: 'LT',
CONTAINS: 'CONTAINS',
NCONTAINS: 'NCONTAINS',
};
export const QueryOperatorsMultiVal = {
IN: 'IN',
NIN: 'NIN',
};
export const ConditionalOperators = {
AND: 'AND',
OR: 'OR',
};
export const QueryTypes = {
QUERY_KEY: 'QUERY_KEY',
QUERY_OPERATOR: 'QUERY_OPERATOR',
QUERY_VALUE: 'QUERY_VALUE',
CONDITIONAL_OPERATOR: 'CONDITIONAL_OPERATOR',
};

View File

@ -0,0 +1,4 @@
export interface ILogQLParsedQueryItem {
type: string;
value: string | string[];
}

View File

@ -0,0 +1,9 @@
export const fieldSearchFilter = (
searchSpace = '',
currentValue = '',
): boolean => {
if (!currentValue || !searchSpace) {
return true;
}
return searchSpace.toLowerCase().indexOf(currentValue.toLowerCase()) !== -1;
};

View File

@ -0,0 +1,16 @@
import { ILog } from 'types/api/logs/log';
export function FlatLogData(log: ILog): Record<string, unknown> {
const flattenLogObject: Record<string, unknown> = {};
Object.keys(log).forEach((key: string): void => {
if (typeof log[key as never] !== 'object') {
flattenLogObject[key] = log[key as never];
} else {
Object.keys(log[key as never]).forEach((childKey) => {
flattenLogObject[childKey] = log[key as never][childKey];
});
}
});
return flattenLogObject;
}

View File

@ -0,0 +1,24 @@
import { QueryOperatorsMultiVal } from 'lib/logql/tokens';
type Keys = keyof typeof QueryOperatorsMultiVal;
type Values = typeof QueryOperatorsMultiVal[Keys];
interface GenerateFilterQueryParams {
fieldKey: string;
fieldValue: string;
type: Values;
}
export const generateFilterQuery = ({
fieldKey,
fieldValue,
type,
}: GenerateFilterQueryParams): string => {
let generatedQueryString = `${fieldKey} ${type} `;
if (typeof fieldValue === 'number') {
generatedQueryString += `(${fieldValue})`;
} else {
generatedQueryString += `('${fieldValue}')`;
}
return generatedQueryString;
};

View File

@ -0,0 +1,8 @@
import Logs from 'container/Logs';
import React from 'react';
function LogsHome(): JSX.Element {
return <Logs />;
}
export default LogsHome;

View File

@ -76,9 +76,11 @@ export async function GetMetricQueryRange({
queryData[WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME].map((formula) => {
const generatedFormulaPayload = {};
legendMap[formula.name] = formula.legend || formula.name;
generatedFormulaPayload.queryName = formula.name;
generatedFormulaPayload.expression = formula.expression;
generatedFormulaPayload.disabled = formula.disabled;
generatedFormulaPayload.legend = formula.legend;
builderQueries[formula.name] = generatedFormulaPayload;
});
QueryPayload.compositeMetricQuery.builderQueries = builderQueries;

View File

@ -0,0 +1,17 @@
import GetSearchFields from 'api/logs/GetSearchFields';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { SET_FIELDS } from 'types/actions/logs';
export const AddToSelectedField = (): ((
dispatch: Dispatch<AppActions>,
) => void) => {
return async (dispatch): Promise<void> => {
const response = await GetSearchFields();
if (response.payload)
dispatch({
type: SET_FIELDS,
payload: response.payload,
});
};
};

View File

@ -0,0 +1,23 @@
import GetSearchFields from 'api/logs/GetSearchFields';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { SET_FIELDS } from 'types/actions/logs';
const IGNORED_SELECTED_FIELDS = ['timestamp'];
export const GetLogsFields = (): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch): Promise<void> => {
const response = await GetSearchFields();
if (response.payload) {
dispatch({
type: SET_FIELDS,
payload: {
interesting: response.payload.interesting,
selected: response.payload.selected.filter(
(field) => !IGNORED_SELECTED_FIELDS.includes(field.name),
),
},
});
}
};
};

View File

@ -0,0 +1,34 @@
import GetLogs from 'api/logs/GetLogs';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { SET_LOADING, SET_LOGS } from 'types/actions/logs';
import { Props } from 'types/api/logs/getLogs';
export const getLogs = (
props: Props,
): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch): Promise<void> => {
dispatch({
type: SET_LOADING,
payload: true,
});
const response = await GetLogs(props);
if (response.payload)
dispatch({
type: SET_LOGS,
payload: response.payload,
});
else
dispatch({
type: SET_LOGS,
payload: [],
});
dispatch({
type: SET_LOADING,
payload: false,
});
};
};

View File

@ -0,0 +1,44 @@
import GetLogsAggregate from 'api/logs/GetLogsAggregate';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import {
SET_LOADING_AGGREGATE,
SET_LOGS_AGGREGATE_SERIES,
} from 'types/actions/logs';
import { Props } from 'types/api/logs/getLogsAggregate';
import { ILogsAggregate } from 'types/api/logs/logAggregate';
export const getLogsAggregate = (
props: Props,
): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch): Promise<void> => {
dispatch({
type: SET_LOADING_AGGREGATE,
payload: true,
});
const response = await GetLogsAggregate(props);
if (response.payload) {
const convertedArray: ILogsAggregate[] = Object.values(response.payload).map(
(data) => {
return { ...data, time: new Date(data.timestamp / 1e6) };
},
);
dispatch({
type: SET_LOGS_AGGREGATE_SERIES,
payload: convertedArray,
});
} else {
dispatch({
type: SET_LOGS_AGGREGATE_SERIES,
payload: [],
});
}
dispatch({
type: SET_LOADING_AGGREGATE,
payload: false,
});
};
};

View File

@ -3,6 +3,7 @@ import { combineReducers } from 'redux';
import appReducer from './app';
import dashboardReducer from './dashboard';
import globalTimeReducer from './global';
import { LogsReducer } from './logs';
import metricsReducers from './metric';
import { ServiceMapReducer } from './serviceMap';
import traceReducer from './trace';
@ -16,6 +17,7 @@ const reducers = combineReducers({
dashboards: dashboardReducer,
app: appReducer,
metrics: metricsReducers,
logs: LogsReducer,
});
export type AppState = ReturnType<typeof reducers>;

View File

@ -0,0 +1,213 @@
import { parseQuery } from 'lib/logql';
import {
ADD_SEARCH_FIELD_QUERY_STRING,
FLUSH_LOGS,
GET_FIELDS,
GET_NEXT_LOG_LINES,
GET_PREVIOUS_LOG_LINES,
LogsActions,
PUSH_LIVE_TAIL_EVENT,
RESET_ID_START_AND_END,
SET_DETAILED_LOG_DATA,
SET_FIELDS,
SET_LIVE_TAIL_START_TIME,
SET_LOADING,
SET_LOADING_AGGREGATE,
SET_LOG_LINES_PER_PAGE,
SET_LOGS,
SET_LOGS_AGGREGATE_SERIES,
SET_SEARCH_QUERY_PARSED_PAYLOAD,
SET_SEARCH_QUERY_STRING,
STOP_LIVE_TAIL,
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs';
const initialState: ILogsReducer = {
fields: {
interesting: [],
selected: [],
},
searchFilter: {
queryString: '',
parsedQuery: [],
},
logs: [],
logLinesPerPage: 25,
idEnd: '',
idStart: '',
isLoading: false,
isLoadingAggregate: false,
logsAggregate: [],
liveTail: 'STOPPED',
liveTailStartRange: 15,
selectedLogId: null,
detailedLog: null,
};
export const LogsReducer = (
state = initialState,
action: LogsActions,
): ILogsReducer => {
switch (action.type) {
case SET_LOADING: {
return {
...state,
isLoading: action.payload,
};
}
case SET_LOADING_AGGREGATE: {
return {
...state,
isLoadingAggregate: action.payload,
};
}
case GET_FIELDS:
return {
...state,
};
case SET_FIELDS: {
const newFields = action.payload;
return {
...state,
fields: newFields,
};
}
case SET_SEARCH_QUERY_STRING: {
return {
...state,
searchFilter: {
...state.searchFilter,
queryString: action.payload,
},
};
}
case SET_SEARCH_QUERY_PARSED_PAYLOAD: {
return {
...state,
searchFilter: {
...state.searchFilter,
parsedQuery: action.payload,
},
};
}
case ADD_SEARCH_FIELD_QUERY_STRING: {
const updatedQueryString =
state.searchFilter.queryString ||
`${
state.searchFilter.queryString && state.searchFilter.queryString.length > 0
? ' and '
: ''
}${action.payload}`;
const updatedParsedQuery = parseQuery(updatedQueryString);
return {
...state,
searchFilter: {
...state.searchFilter,
queryString: updatedQueryString,
parsedQuery: updatedParsedQuery,
},
};
}
case SET_LOGS: {
const logsData = action.payload;
return {
...state,
logs: logsData,
};
}
case SET_LOG_LINES_PER_PAGE: {
return {
...state,
logLinesPerPage: action.payload,
};
}
case GET_PREVIOUS_LOG_LINES: {
const idStart = state.logs.length > 0 ? state.logs[0].id : '';
return {
...state,
idStart,
idEnd: '',
};
}
case GET_NEXT_LOG_LINES: {
const idEnd =
state.logs.length > 0 ? state.logs[state.logs.length - 1].id : '';
return {
...state,
idStart: '',
idEnd,
};
}
case RESET_ID_START_AND_END: {
return {
...state,
idEnd: '',
idStart: '',
};
}
case SET_LOGS_AGGREGATE_SERIES: {
return {
...state,
logsAggregate: action.payload,
};
}
case SET_DETAILED_LOG_DATA: {
return {
...state,
detailedLog: action.payload,
};
}
case TOGGLE_LIVE_TAIL: {
return {
...state,
liveTail: action.payload,
};
}
case STOP_LIVE_TAIL: {
return {
...state,
logs: [],
liveTail: 'STOPPED',
};
}
case PUSH_LIVE_TAIL_EVENT: {
return {
...state,
logs: action.payload.concat(state.logs).slice(0, 100),
};
}
case SET_LIVE_TAIL_START_TIME: {
return {
...state,
liveTailStartRange: action.payload,
};
}
case FLUSH_LOGS: {
return {
...state,
logs: [],
};
}
default:
return state;
}
};
export default LogsReducer;

View File

@ -1,6 +1,7 @@
import { AppAction } from './app';
import { DashboardActions } from './dashboard';
import { GlobalTimeAction } from './globalTime';
import { LogsActions } from './logs';
import { MetricsActions } from './metrics';
import { TraceActions } from './trace';
@ -9,6 +10,7 @@ type AppActions =
| AppAction
| GlobalTimeAction
| MetricsActions
| TraceActions;
| TraceActions
| LogsActions;
export default AppActions;

View File

@ -0,0 +1,147 @@
// import { DBOverView } from 'types/api/metrics/getDBOverview';
// import { ExternalAverageDuration } from 'types/api/metrics/getExternalAverageDuration';
// import { ExternalError } from 'types/api/metrics/getExternalError';
// import { ExternalService } from 'types/api/metrics/getExternalService';
// import { IResourceAttributeQuery } from 'container/MetricsApplication/ResourceAttributesFilter/types';
// import { ServicesList } from 'types/api/metrics/getService';
// import { ServiceOverview } from 'types/api/metrics/getServiceOverview';
// import { TopEndPoints } from 'types/api/metrics/getTopEndPoints';
import { ILogQLParsedQueryItem } from 'lib/logql/types';
import { IFieldMoveToSelected, IFields } from 'types/api/logs/fields';
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
import { ILog } from 'types/api/logs/log';
import { ILogsAggregate } from 'types/api/logs/logAggregate';
// export const GET_SERVICE_LIST_SUCCESS = 'GET_SERVICE_LIST_SUCCESS';
// export const GET_SERVICE_LIST_LOADING_START = 'GET_SERVICE_LIST_LOADING_START';
// export const GET_SERVICE_LIST_ERROR = 'GET_SERVICE_LIST_ERROR';
// export const GET_INITIAL_APPLICATION_LOADING =
// 'GET_INITIAL_APPLICATION_LOADING';
// export const GET_INITIAL_APPLICATION_ERROR = 'GET_INITIAL_APPLICATION_ERROR';
// export const GET_INTIAL_APPLICATION_DATA = 'GET_INTIAL_APPLICATION_DATA';
// export const RESET_INITIAL_APPLICATION_DATA = 'RESET_INITIAL_APPLICATION_DATA';
export const GET_FIELDS = 'LOGS_GET_FIELDS';
export const SET_FIELDS = 'LOGS_SET_FIELDS';
export const SET_SEARCH_QUERY_STRING = 'LOGS_SET_SEARCH_QUERY_STRING';
export const SET_SEARCH_QUERY_PARSED_PAYLOAD =
'LOGS_SET_SEARCH_QUERY_PARSED_PAYLOAD';
export const ADD_SEARCH_FIELD_QUERY_STRING =
'LOGS_ADD_SEARCH_FIELD_QUERY_STRING';
export const ADD_TO_SELECTED_FIELD = 'LOGS_ADD_TO_SELECTED_FIELD';
export const SET_LOGS = 'LOGS_SET_LOGS';
export const SET_LOG_LINES_PER_PAGE = 'LOGS_SET_LOG_LINES_PER_PAGE';
export const GET_NEXT_LOG_LINES = 'LOGS_GET_NEXT_LOG_LINES';
export const GET_PREVIOUS_LOG_LINES = 'LOGS_GET_PREVIOUS_LOG_LINES';
export const RESET_ID_START_AND_END = 'LOGS_RESET_ID_START_AND_END';
export const SET_LOADING = 'LOGS_SET_LOADING';
export const SET_LOADING_AGGREGATE = 'LOGS_SET_LOADING_AGGREGATE';
export const SET_LOGS_AGGREGATE_SERIES = 'LOGS_SET_LOGS_AGGREGATE_SERIES';
export const SET_DETAILED_LOG_DATA = 'LOGS_SET_DETAILED_LOG_DATA';
export const TOGGLE_LIVE_TAIL = 'LOGS_TOGGLE_LIVE_TAIL';
export const PUSH_LIVE_TAIL_EVENT = 'LOGS_PUSH_LIVE_TAIL_EVENT';
export const STOP_LIVE_TAIL = 'LOGS_STOP_LIVE_TAIL';
export const FLUSH_LOGS = 'LOGS_FLUSH_LOGS';
export const SET_LIVE_TAIL_START_TIME = 'LOGS_SET_LIVE_TAIL_START_TIME';
export interface GetFields {
type: typeof GET_FIELDS;
}
export interface SetFields {
type: typeof SET_FIELDS;
payload: IFields;
}
export interface SetSearchQueryString {
type: typeof SET_SEARCH_QUERY_STRING;
payload: string;
}
export interface SetSearchQueryParsedPayload {
type: typeof SET_SEARCH_QUERY_PARSED_PAYLOAD;
payload: ILogQLParsedQueryItem[];
}
export interface AddSearchFieldQueryString {
type: typeof ADD_SEARCH_FIELD_QUERY_STRING;
payload: string;
}
export interface AddToSelectedField {
type: typeof ADD_TO_SELECTED_FIELD;
payload: IFieldMoveToSelected;
}
export interface UpdateLogs {
type: typeof SET_LOGS;
payload: ILog[];
}
export interface SetLogsLinesPerPage {
type: typeof SET_LOG_LINES_PER_PAGE;
payload: number;
}
export interface PreviousLogsLines {
type: typeof GET_PREVIOUS_LOG_LINES;
}
export interface NextLogsLines {
type: typeof GET_NEXT_LOG_LINES;
}
export interface ResetIdStartAndEnd {
type: typeof RESET_ID_START_AND_END;
}
export interface SetLoading {
type: typeof SET_LOADING;
payload: boolean;
}
export interface SetLoadingAggregate {
type: typeof SET_LOADING_AGGREGATE;
payload: boolean;
}
export interface SetLogsAggregateSeries {
type: typeof SET_LOGS_AGGREGATE_SERIES;
payload: ILogsAggregate[];
}
export interface SetDetailedLogData {
type: typeof SET_DETAILED_LOG_DATA;
payload: ILog;
}
export interface ToggleLiveTail {
type: typeof TOGGLE_LIVE_TAIL;
payload: TLogsLiveTailState;
}
export interface PushLiveTailEvent {
type: typeof PUSH_LIVE_TAIL_EVENT;
payload: ILog[];
}
export interface StopLiveTail {
type: typeof STOP_LIVE_TAIL;
}
export interface FlushLogs {
type: typeof FLUSH_LOGS;
}
export interface SetLiveTailStartTime {
type: typeof SET_LIVE_TAIL_START_TIME;
payload: number;
}
export type LogsActions =
| GetFields
| SetFields
| SetSearchQueryString
| SetSearchQueryParsedPayload
| AddSearchFieldQueryString
| AddToSelectedField
| UpdateLogs
| SetLogsLinesPerPage
| PreviousLogsLines
| NextLogsLines
| ResetIdStartAndEnd
| SetLoading
| SetLoadingAggregate
| SetLogsAggregateSeries
| SetDetailedLogData
| ToggleLiveTail
| PushLiveTailEvent
| StopLiveTail
| FlushLogs
| SetLiveTailStartTime;

View File

@ -30,8 +30,7 @@ export interface IBuilderQuery
extends Omit<
IMetricQuery,
'aggregateOperator' | 'legend' | 'metricName' | 'tagFilters'
>,
Omit<IFormulaQuery, 'expression'> {
> {
aggregateOperator: EAggregateOperator | undefined;
disabled: boolean;
name: string;

View File

@ -71,6 +71,7 @@ export interface IMetricsBuilderFormula {
expression: string;
disabled: boolean;
name: string;
legend: string;
}
export interface IMetricsBuilderQuery {
aggregateOperator: EAggregateOperator;

View File

@ -0,0 +1,4 @@
import { IFieldMoveToSelected } from './fields';
export type Props = IFieldMoveToSelected;
export type PayloadProps = IFieldMoveToSelected;

View File

@ -0,0 +1,16 @@
export interface IField {
name: string;
type: string;
dataType: string;
}
export interface IFields {
selected: ISelectedFields[];
interesting: IInterestingFields[];
}
export type ISelectedFields = IField;
export type IInterestingFields = IField;
export type IFieldMoveToSelected = IField & {
selected: boolean;
};

View File

@ -0,0 +1,13 @@
import { ILog } from './log';
export type PayloadProps = ILog[];
export type Props = {
q: string;
limit: number;
orderBy: string;
order: string;
idGt?: string;
idLt?: string;
timestampStart?: number;
timestampEnd?: number;
};

View File

@ -0,0 +1,15 @@
export type PayloadProps = Record<
string,
{
timestamp: number;
value: number;
}
>;
export type Props = {
timestampStart: number;
timestampEnd: number;
step: number;
q?: string;
idGt?: string;
idLt?: string;
};

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