Compare commits

...

163 Commits

Author SHA1 Message Date
SagarRajput-7
d6fd19b363 Merge branch 'variable-update-queue' into dashboard-variable-test-cases 2024-12-17 17:53:49 +05:30
SagarRajput-7
e50a773fa9 feat: added API limiting to reduce unnecessary api call for dashboard variables (#6609)
* feat: added API limiting to reduce unneccesary api call for dashboard variables

* feat: fixed dropdown open triggering the api calls for single-select and misc
2024-12-17 17:53:03 +05:30
SagarRajput-7
cb91fee7c3 Merge branch 'develop' into variable-update-queue 2024-12-17 17:52:11 +05:30
Vibhu Pandey
14fbb1fcda fix(flag): add missing flag back (#6647) 2024-12-17 01:12:31 +05:30
Vikrant Gupta
96da21df05 fix: correlation time stamps for APM to traces and logs (#6632) 2024-12-17 00:00:15 +05:30
Vikrant Gupta
8608f02263 fix: timerange selected for traces to logs (#6634) 2024-12-16 22:35:25 +05:30
Vikrant Gupta
2701ae5c34 fix: unable to remove query tags from the beginning when similar tags present (#6645)
* fix: unable to remove query tags from the begining

* fix: focus only when the filters dropdown is opened

* fix: used auto focus prop
2024-12-16 22:25:37 +05:30
Vibhu Pandey
951593b0a3 feat(licenses): deprecate licenses v2 (#6626)
deprecate licenses v2
2024-12-16 10:23:23 +00:00
Srikanth Chekuri
e6766023dd chore: use tag attributes v2 table (#6616) 2024-12-16 09:59:16 +00:00
Srikanth Chekuri
bef5b96c5c chore: add queries with tag attributes v1 (#6643) 2024-12-16 13:04:55 +05:30
Shaheer Kochai
b29359dee0 fix: improve traces table ux by making the table scrollable (#6423)
* fix: improve traces table ux by making the table scrollable

* chore: remove explicit scroll.x and modify the existing scroll.x from true to max-content

* fix(Traces Explorer): make trace explorer card full-width when collapsed, resizable when expanded
2024-12-16 07:00:40 +00:00
Amlan Kumar Nandy
9a1cd65b73 feat: add functionality to export dashboard as json from listing page 2024-12-16 12:10:21 +05:30
SagarRajput-7
351178ef34 Merge branch 'limiting-api-via-keys' into dashboard-variable-test-cases 2024-12-16 12:03:20 +05:30
SagarRajput-7
4b6e934510 Merge branch 'variable-update-queue' into limiting-api-via-keys 2024-12-16 12:03:14 +05:30
SagarRajput-7
99fb8c2a64 Merge branch 'develop' into variable-update-queue 2024-12-16 12:03:05 +05:30
Amlan Kumar Nandy
8ab0c066d6 Merge branch 'develop' into dashboard-list-export-json 2024-12-16 11:39:18 +05:30
Shaheer Kochai
b333aa3775 Feat: Timezone picker feature (#6474)
* feat: time picker hint and timezone picker UI with basic functionality + helper to get timezones

* feat: add support for esc keypress to close the timezone picker

* chore: add the selected timezone as url param and close timezone picker on select

* fix: overall improvement + add searchIndex to timezone

* feat: timezone preferences UI

* chore: improve timezone utils

* chore: change timezone item from div to button

* feat: display timezone in timepicker input

* chore: fix the typo

* fix: don't focus on time picker when timezone is clicked

* fix: fix the issue of timezone breaking for browser and utc timezones

* fix: display the timezone in timepicker hint 'You are at'

* feat: timezone basic functionality (#6492)

* chore: change div to fragment + change type to any as the ESLint complains otherwise

* chore: manage etc timezone filtering with an arg

* chore: update timezone wrapper class name

* fix: add timezone support to downloaded logs

* feat: add current timezone to dashboard list and configure metadata modal

* fix: add pencil icon next to timezone hint + change the copy to Current timezone

* fix: properly handle the escape button behavior for timezone picker

* chore: replace @vvo/tzdb with native Intl API for timezones

* feat: lightmode for timezone picker and timezone adaptation components

* fix: use normald tz in browser timezone

* fix: timezone picker lightmode fixes

* feat: display selected time range in 12 hour format

* chore: remove unnecessary optional chaining

* fix: fix the typo in css variable

* chore: add em dash and change icon for timezone hint in date/time picker

* chore: move pen line icon to the right of timezone offset

* fix: fix the failing tests

* feat: handle switching off the timezone adaptation
2024-12-16 11:27:20 +05:30
amlannandy
8a3319cdf5 chore: address comments 2024-12-16 11:24:57 +05:30
amlannandy
d09c4d947e feat: update unit test 2024-12-16 11:24:57 +05:30
amlannandy
2508e6f9f1 feat: update unit test 2024-12-16 11:24:57 +05:30
amlannandy
1b8213653a feat: update tests 2024-12-16 11:24:57 +05:30
amlannandy
b499b10333 feat: add unit test 2024-12-16 11:24:57 +05:30
amlannandy
b35b975798 chore: address comments 2024-12-16 11:24:57 +05:30
amlannandy
715f8a2363 feat: address comments 2024-12-16 11:24:57 +05:30
amlannandy
8d1c4491b7 feat: add functionality to export dashboard as json from listing page 2024-12-16 11:24:57 +05:30
dependabot[bot]
e3caa6a8f5 chore(deps): bump golang.org/x/crypto from 0.27.0 to 0.31.0 (#6638)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-14 04:29:25 +00:00
Vibhu Pandey
a1059ed949 feat(signoz-ingestion-key): rename signoz-access-token to signoz-ingestion-key (#6633) 2024-12-13 18:18:12 +05:30
SagarRajput-7
8c46de8eac feat: added table column and row logic for the new api response structure for producer overview (#6433)
* feat: added table column and row logic for the new api response structure for prodcure overview

* feat: fixed typo in function name

* feat: consumed new 2 table merging logic for producer latency overview

* feat: added 3 digit precision to 'ingestion_rate' and 'byte_rate'in consumer overview
2024-12-13 11:25:39 +05:30
SagarRajput-7
ae98aaad2d feat: added more test on graph utilities 2024-12-12 16:52:50 +05:30
SagarRajput-7
23d808af08 feat: refactor code 2024-12-12 11:13:49 +05:30
SagarRajput-7
c991ee6239 feat: added test for checkAPIInvocation 2024-12-12 11:04:55 +05:30
SagarRajput-7
f098518faa feat: add jest test cases for new logic's utils, functions and processors - dashboardVariables 2024-12-12 09:53:47 +05:30
Nityananda Gohain
0fbfb6b22b fix: fix count aggregate attribute column name (#6613)
* fix: fix count aggregate attribute column name

* fix: add test for column as well

* fix: use new schema in threshold rule enrichment
2024-12-11 13:37:25 +05:30
SagarRajput-7
421d355e29 feat: fixed dropdown open triggering the api calls for single-select and misc 2024-12-11 13:18:56 +05:30
Nityananda Gohain
b25df66381 fix: migration for ingestion dashboard (#6610)
* fix: migration for ingestion dashboard

* fix: remove redundant uuid

* Update pkg/query-service/app/dashboards/model.go

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-12-10 18:49:21 +05:30
Nityananda Gohain
32fa5a403c fix: update default alert ch queries for traces and logs (#6552)
* fix: update default alert ch queries for traces and logs

* fix: use service.name instead of peer.service

* fix: add link to docs

* fix: add alias for service name
2024-12-10 18:36:02 +05:30
Nityananda Gohain
f9d4cf19e9 fix: smartTraceAlgo for new schema (#6602) 2024-12-10 13:43:51 +05:30
Amlan Kumar Nandy
81775c7d55 fix: resolve unnecessary refetching of graphs on service details page 2024-12-10 12:00:55 +05:30
amlannandy
8d2666004b fix: resolve the unneccessary refetching of graphs on service details page 2024-12-10 11:39:26 +05:30
SagarRajput-7
eb75e636e8 feat: added API limiting to reduce unneccesary api call for dashboard variables 2024-12-10 11:20:34 +05:30
SagarRajput-7
f121240c82 Merge branch 'develop' into variable-update-queue 2024-12-10 11:18:28 +05:30
Shivanshu Raj Shrivastava
51baf7f8d3 feat: add byte rate to producer API (#6579)
* fix: fix partition check for topic throughput
2024-12-09 15:18:41 +00:00
Amlan Kumar Nandy
31a2926375 chore: replace the 'Get started' CTA with 'New source' 2024-12-09 14:34:18 +05:30
amlannandy
8c6225185d chore: replace the 'Get started' CTA with 'New source' 2024-12-09 14:00:41 +05:30
deepakdinesh1123
d4458d65ad fix(docker): otel-collector-migrator command fixed in docker-compose-core.yaml (#6564)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-12-07 20:15:07 +05:30
Nityananda Gohain
02d8fdb212 fix: enable smart trace detail (#6596) 2024-12-05 09:30:39 +05:30
Prashant Shahi
a383c708e3 Merge pull request #6591 from SigNoz/release/v0.61.x
Release/v0.61.x
2024-12-04 20:49:18 +05:30
Prashant Shahi
99367be850 chore(signoz): pin versions: SigNoz 0.61.0, SigNoz OtelCollector 0.111.14
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-04 16:21:39 +05:30
Prashant Shahi
73bcc2af46 Merge branch 'main' into release/v0.61.x 2024-12-04 16:17:00 +05:30
SagarRajput-7
a60dbf7f89 Merge branch 'develop' into variable-update-queue 2024-12-04 10:46:17 +05:30
SagarRajput-7
fa4aeae508 feat: updated the logic for variable update queue 2024-12-04 10:19:33 +05:30
Vikrant Gupta
43f856c41b fix: make the entire row clickable in logs quick filters (#6574) 2024-12-03 13:06:07 +05:30
Vikrant Gupta
6384b25af3 feat: show failed payment banner and block the workspace integration (#6560)
* feat: added new API endpoint for fetching the active license

* feat: add setup for apis on frontend

* feat: frontend infrastructure changes for app context and workspace suspended

* feat: added workspace suspended component

* feat: send back to application if workspace is not suspended

* feat: added the missing creative

* chore: only move to suspended state when state is payment_failed

* chore: address review comments

* fix: tab naming
2024-12-02 19:58:38 +05:30
Yunus M
507c0600cd chore: remove infra monitoring ff (#6571) 2024-12-02 17:36:37 +05:30
Yunus M
3d092ec2ae chore: update sentry react to v8 (#6569) 2024-12-02 14:21:35 +05:30
SagarRajput-7
2b8a610a07 fix: fixed typo in metric view dashboard title (#6557) 2024-11-29 11:27:54 +05:30
Nityananda Gohain
f7f8bf1867 fix: add dashbard names and update logs count in telemetry (#6553) 2024-11-28 20:46:06 +05:30
Vikrant Gupta
813cd845f4 fix: explicit mouse move event user action (#6549)
* fix: explicit mouse move event user action

* fix: handle next element selection on click

* fix: handle the last 0 case

* fix: handle search reset
2024-11-27 19:51:57 +05:30
Yunus M
6aee991633 feat: handle keyboard navigations for column selection in logs explor… (#6548)
* feat: handle keyboard navigations for column selection in logs explorer options view

* chore: remove console log
2024-11-27 13:04:23 +00:00
Yunus M
2bfd31841e feat: improve empty hosts, incorrect metrics and no filter views (#6530)
* feat: improve empty hosts, incorrect metrics and no filter views

* feat: add infra monitoring - host lists - usage events (#6536)

* feat: add usasge events

* feat: add reqrest early access events

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-11-27 16:00:08 +05:30
Prashant Shahi
a320a16556 Merge pull request #6546 from SigNoz/release/v0.60.x
Release/v0.60.x
2024-11-27 15:54:49 +05:30
Prashant Shahi
7cd8442e6e chore(signoz): pin versions: SigNoz 0.60.0, SigNoz OtelCollector 0.111.13
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-11-27 15:13:08 +05:30
Raj Kamal Singh
486632b64e Chore/qs use collector simulator from signoz otel collector (#6539)
* chore: qs: logs pipeline preview: use collectorsimulator from signoz-otel-collector

* chore: qs: remove collectorsimulator: located in signoz-otel-collector now
2024-11-27 11:53:39 +05:30
Philipp
328d955a74 Fix for running signoz-schema-migrator under docker-swarm (#6489) 2024-11-27 03:15:32 +00:00
Nityananda Gohain
a3e57a1829 fix: add telemetry for trace migration (#6537) 2024-11-26 20:57:36 +05:30
Vikrant Gupta
24ab18d988 chore: move away from parallel usages of licenses v2 and v3 (#6527)
* chore: move all the usages of license v2 to license v3

* chore: added log lines for debugging
2024-11-25 23:51:45 +05:30
Srikanth Chekuri
2e4956c2f7 chore: add additional info for host metrics onboarding (#6529) 2024-11-25 23:23:42 +05:30
Nityananda Gohain
b85f7921f4 fix: update api's to repect the new changes in attributes table (#6526) 2024-11-25 17:05:43 +05:30
Nityananda Gohain
0c2a15d86f fix: add settings to allow distributed_product_mode for trace panel (#6517)
* fix: add settings to allow distributed_product_mode for trace panel

* fix: tests fixed

* fix: add 10gb memory limit

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-11-22 22:48:35 +05:30
Nityananda Gohain
afbba1ed44 fix: add v2 for getServices and GetTopOperations (#6516)
* fix: add v2 for getServices and GetTopOperations

* fix: add comments

* fix: update logic for filters

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-11-22 21:57:25 +05:30
Yunus M
20f748f9c4 chore: update http proxy middleware (#6499) 2024-11-22 14:21:55 +00:00
Vikrant Gupta
96b5e0920f fix: decrease the pad angle and remove the empty legend entries (#6507)
* fix: pie chart should not have padding between adjacent items

* fix: remove the labels which do not have any data
2024-11-22 11:06:47 +00:00
Srikanth Chekuri
7fe4f8cc56 chore: remove the fancy text while fetching data (#6505) 2024-11-22 10:50:25 +00:00
Srikanth Chekuri
ed6abe5a95 chore: enable sorting for hosts list (#6502) 2024-11-22 10:37:42 +00:00
SagarRajput-7
a6968d452c fix: updated folder name and structure (#6488) 2024-11-22 14:05:22 +05:30
SagarRajput-7
0c5db1937e fix: fixed back navigation issue for dashboard details coming from dashboard list table (#6496) 2024-11-22 13:49:02 +05:30
Nityananda Gohain
67058b2a17 feat: trace v4 integration (#6226)
* feat: trace v4 inital commit

* fix: add remaining files

* fix: integrate with querier

* fix: get trace by id api updated

* fix: add servicename resource filter

* fix: tests

* fix: use correct prepQUery

* fix: services page

* fix: minor fixes to use the new table in api's and querier

* fix: add support for window based pagination

* feat: support for faster trace detail

* fix: searchTraces

* fix: attribute enrichment updated and issue in group by

* fix: issues in group by

* fix: enrichment using alias

* fix: test file added

* fix: tests

* fix: group by with filters

* fix: add subquery

* fix: trigger builde

* fix: update pagination logic and few ch column names

* fix: update qb

* fix: add tests

* feat: minor fixes

* fix: update pagination logic

* fix: update pagination logic

* fix: remove utils

* fix: remove unwanted API's

* fix: attribute and attribute values v2

* fix: autocomplete api's updated

* fix: tests fixed

* feat: minor fixes

* fix: update telemetry functions

* fix: dont use alias, use proper col names

* fix: move models to it's own file

* fix: minor fixes

* fix: address comments

* fix: add to serviceoverview function

* fix: add changes to overview function

* fix: address comments

* fix: remove printlines

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-11-22 12:00:29 +05:30
Nityananda Gohain
e46d969143 feat: ttl api for new trace tables (#6497)
* feat: tt api for new trace tables

* fix: remove print and use correct context

* fix: update var name
2024-11-21 22:37:28 +05:30
Yunus M
e4505693b0 feat: added the host list view and filters (#6210) (#6501)
* feat: added the host list view and filters (#6210)

* feat: added the host list view and filters

* feat: removed group by filter and added autocomplete for where clause

* feat: updated the table view and added the pagination

* feat: pass updated filters to api to get filtered data in the list

* feat: added global time range and order by for cpu,memory,iowait,load

* feat: added order by and color codes for cpu and memory usage progress bar

* refactor: removed inline styles

* Host lists improvement (#6366)

* style: added new style changes for date time selection in host lists view

* style: added padding to date time selector

* style: removed unnecessary styles for host tabs

* style: removed unused css

* feat: added the host detail view (#6267)

* Host containers (#6297)

* feat: added the host detail view

* feat: completed containers and processes details view

* Show host metrics panels in metrics tab. (#6306)

* feat: added the host detail view

* feat: completed containers and processes details view

* feat: added host metrics panels in metrics tabs

* refactor: removed inline styles from host containers and processes tabs

* style: added top and bottom margin to containers and processes tab

* Metrics time selection (#6360)

* feat: added the host detail view

* feat: completed containers and processes details view

* feat: added host metrics panels in metrics tabs

* refactor: removed inline styles from host containers and processes tabs

* feat: added logs and traces tab in host metrics detail view

* chore: removed console statements

* feat: added DateTimeSelection component in metrics tab

* style: added top and bottom margin to containers and processes tab

* style: removed inline styles

* feat: added logs and traces tab in host metrics detail view (#6359)

* feat: added the host detail view

* feat: completed containers and processes details view

* feat: added host metrics panels in metrics tabs

* refactor: removed inline styles from host containers and processes tabs

* feat: added logs and traces tab in host metrics detail view

* chore: removed console statements

* feat: added filters and time selection in traces tab

* fix: resolved metrics,logs and traces tab issues

* feat: added navigation for logs and traces to respective explorer pages

* fix: added the code for logs tab and navigation to respective explorer page

* fix: added fixes for date time selection custom issue

* style: added styles for light mode

* refactor: removed unused code and added comments

* refactor: added new code for host metric attribute keys

* feat: reset query data once we are on infra monitoring page

* chore: remove optional parameter from get attributes and groupby interfaces

* feat: update ui as per the designs

* fix: logs list, time select and other ui issues

* feat: update title for infra monitoring page

* feat: update copies

* feat: update styles for light mode

* fix: reset page size on filter, open explorers in new tab, enable horizontal scroll

* feat: traces tab updates

* feat: move infra monitoring behind ff

* fix: remove sorting from host listing page

---------

Co-authored-by: Yunus M <myounis.ar@live.com>

* chore: fix lint errors

---------

Co-authored-by: rahulkeswani101 <rahul@signoz.io>
2024-11-21 21:53:05 +05:30
Prashant Shahi
2dad9a3093 Merge pull request #6498 from SigNoz/sync/post-release-v0.59
Sync/post release v0.59
2024-11-21 19:58:05 +05:30
Prashant Shahi
7b6bd83e9a Merge branch 'main' into sync/post-release-v0.59 2024-11-21 16:10:05 +05:30
Nityananda Gohain
d43adc24ef feat: update clickhouse reader to support new trace schema (#6479)
* feat: update clickhouse reader to support new trace schema

* fix: minor fixes

* fix: address comments

* fix: add changes to overview function

* fix: add changes to overview function

* fix: use hardcoded true

* fix: address comments
2024-11-20 23:35:44 +05:30
Nityananda Gohain
5044861773 fix: remove service overview API (#6495) 2024-11-20 22:51:29 +05:30
Prashant Shahi
71d1e12be7 Merge pull request #6490 from SigNoz/release/v0.59.x
Release/v0.59.x
2024-11-20 19:29:36 +05:30
Prashant Shahi
5a70123b06 chore(signoz): pin versions: SigNoz 0.59.0, SigNoz OtelCollector 0.111.9
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-11-20 16:45:18 +05:30
Prashant Shahi
f410df846a Merge branch 'main' into release/v0.59.x 2024-11-20 16:35:05 +05:30
Yunus M
d7bd72e2aa chore: update http proxy middleware (#6486) 2024-11-20 14:34:38 +05:30
Shaheer Kochai
20e64b5102 chore: add log events to alert history page (#6396)
* chore: pass active route with RouteTab's onChangeHandler

* chore: add log events to alert history page

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-11-20 04:38:27 +00:00
Vikrant Gupta
0b03ff07f1 chore: exactly match the list licenses v2 structure with FF on (#6481) 2024-11-19 19:10:59 +05:30
Vibhu Pandey
c01060ccf7 feat(key): enable edit access for gateway (#6480) 2024-11-19 12:43:25 +00:00
Nityananda Gohain
57c2326908 Trace static fields and structs for trace v4. (#6469)
* fix: update static fields and add response structs

* fix: update ch names

* fix: move models to it's own file
2024-11-19 14:38:12 +05:30
Vikrant Gupta
649560265e chore: set zeusURL via build time variable (#6475) 2024-11-19 13:19:01 +05:30
Prashant Shahi
c8d0f7638e Merge pull request #6471 from SigNoz/release/v0.58.x
Release/v0.58.2
2024-11-19 12:16:18 +05:30
Prashant Shahi
25484caa4c Merge branch 'main' into release/v0.58.x 2024-11-19 11:35:28 +05:30
Prashant Shahi
9ccc686c63 chore(signoz): pin versions: SigNoz 0.58.2
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-11-19 11:27:28 +05:30
Prashant Shahi
3ad6ff73df Merge branch 'develop' into release/v0.58.x 2024-11-19 11:27:01 +05:30
Srikanth Chekuri
c93cf1ce95 fix: incorrect formula for apdex (#6460) 2024-11-19 05:42:04 +00:00
Prashant Shahi
a9ced66258 Merge branch 'develop' into release/v0.58.x 2024-11-19 11:06:27 +05:30
Nityananda Gohain
98a350692b fix: update TestListTsRange to return all range (#6470) 2024-11-19 10:51:40 +05:30
Vikrant Gupta
d93f72f18d chore: use the license v2 key to fill licenses v3 on startup (#6468)
* feat: use the license v2 key to fill licenses v3 on startup

* chore: make the init only if the licenses v2 is present

* chore: address review comments
2024-11-18 17:55:00 +05:30
Shaheer Kochai
a59e7b9dfb feat: add 'create channel' option in channels list and refetch alert channels on opening the channels dropdown (#6416)
* feat: add channel creation option and auto-refresh channels list on dropdown open

* chore: move inline styles to style.ts

* fix: show the prompt to ask admin if the user doesn't have permissions

* fix: display create channel option only if the user has permission

* fix: prevent repeated new alert event logs + log new channel option inside dropdown
2024-11-18 06:30:06 +00:00
Nityananda Gohain
91bbeaf175 fix: remove unwanted trace API's (#6464) 2024-11-18 10:27:08 +05:30
Yunus M
22e61e1605 [Snyk] Security upgrade alpine from 3.18.6 to 3.20.3 (#6463)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE318-BUSYBOX-6913411
- https://snyk.io/vuln/SNYK-ALPINE318-BUSYBOX-7249236
- https://snyk.io/vuln/SNYK-ALPINE318-BUSYBOX-7249265
- https://snyk.io/vuln/SNYK-ALPINE318-BUSYBOX-7249265
- https://snyk.io/vuln/SNYK-ALPINE318-BUSYBOX-7249419

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-11-17 21:56:15 +05:30
Srikanth Chekuri
656d1c2b1c chore: add missing alert telemetry (#6459) 2024-11-16 19:16:05 +00:00
Srikanth Chekuri
493ae4fd07 chore: add user email to log_comment (#6461) 2024-11-17 00:36:10 +05:30
Srikanth Chekuri
cd1ec561b1 fix: compare op outside bounds for anomaly alert (#6458) 2024-11-16 20:17:34 +05:30
Nityananda Gohain
0acf39a532 feat: support for new enrichment logic in traces (#6438)
* feat: support for new enrichment logic in traces

* fix: default test added

* fix: update func name in links

* Update pkg/query-service/utils/logs_test.go

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2024-11-16 15:19:25 +05:30
Prashant Shahi
d859301d30 Merge branch 'develop' into release/v0.58.x 2024-11-16 04:50:33 +05:30
Nityananda Gohain
35f4eaa23b fix: update logs struct to fix live logs (#6453) 2024-11-15 22:42:16 +05:30
Prashant Shahi
07c24bcdf3 Merge pull request #6452 from SigNoz/release/v0.58.x
Release/v0.58.1
2024-11-15 22:36:10 +05:30
Nityananda Gohain
77c5f17dce feat: support for window based pagination in new trace v4 (#6440)
* feat: support for window based pagination in new trace v4

* fix: update pagination logic

* fix: update comment

* fix: substract correct length

* fix: revert changes

* fix: add tests for querier

* fix: rename matcher

* fix: handle offset inmemory for list queries in traces

* fix: correct var name

* fix: add max pagination limit for traces
2024-11-15 22:13:28 +05:30
Prashant Shahi
a11aadb712 Merge branch 'main' into release/v0.58.x 2024-11-15 22:06:09 +05:30
Prashant Shahi
bc9c7b5f1d chore(signoz): pin versions: SigNoz 0.58.1
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-11-15 22:02:47 +05:30
Prashant Shahi
1bba932d08 Merge branch 'develop' into release/v0.58.x 2024-11-15 22:01:55 +05:30
SagarRajput-7
c1478c4e54 feat: removed dashboard uuid is all cases be it duplicate, empty or somevalid, while import json (#6448)
* feat: removed dashboard uuid is all cases be it duplicate, empty or somevalid, while import json

* feat: added comment to better explain the logic
2024-11-15 19:35:29 +05:30
Yunus M
371224a64a fix: show org onboarding only to cloud customers (#6451) 2024-11-15 19:12:38 +05:30
Yunus M
504bc0d541 feat: ingestion limits - add toggle feature (#6430) 2024-11-15 08:32:31 +00:00
Nityananda Gohain
2faa0c6d4f feat: trace V4 QB (#6407)
* feat: trace V4 QB

* fix: update get column name and remove id

* fix: handle contains and update tests

* fix: remove unwanted step interval calculation

* fix: add test cases

* fix: add tests for static columns in QB

* fix: add more order by tests

* fix: update order by logic
2024-11-13 20:30:01 +05:30
Srikanth Chekuri
969ac5028e chore: add v2 metric writer to pipelines (#6345) 2024-11-13 10:41:28 +00:00
Prashant Shahi
3f7adeb040 Merge branch 'develop' into release/v0.58.x 2024-11-13 12:01:54 +05:30
Srikanth Chekuri
323da3494b chore: add experimental rate/increase calc (#6432) 2024-11-13 11:47:56 +05:30
Vikrant Gupta
01fda51959 chore: return proper http codes on unique constraint error (#6428) 2024-11-13 00:25:00 +05:30
Srikanth Chekuri
85ac21f253 fix: update request payload for span metrics queries (#6323) 2024-11-12 17:22:42 +00:00
Srikanth Chekuri
fd9e9f0fb3 chore: add k8s {deployment, daemonset, statefulset, job} resources (#6401) 2024-11-12 15:23:40 +00:00
Nityananda Gohain
d5523fc092 fix: ignore ts for panel type table (#6419) 2024-11-12 08:04:45 +00:00
Nityananda Gohain
2ec641b99e fix: add severity_text legend (#6415) 2024-11-12 05:54:22 +00:00
Ekansh Gupta
d1503f1418 feat: fixProducerAPI (#6422)
chore: bugfix
2024-11-12 05:30:36 +00:00
Vikrant Gupta
e974e9d47f feat: consume the new licenses v3 structure. (#6341)
* feat: setup for licenses v3 integration

* feat: added some more logic

* feat: validator changes

* chore: added a couple of todos

* feat: added config parameter for licenses v3 and the boot option

* feat: some typo fix

* feat: added refresh licenses handler

* feat: handle the start manager license activation

* chore: text updates

* feat: added list licenses call

* chore: refactor the entire code to cleanup interfaces

* fix: nil pointer error

* chore: some minor edits

* feat: model changes

* feat: model changes

* fix: utilise factory pattern

* feat: added default basic plan

* chore: added test cases for new license function

* feat: added more test cases

* chore: make the licenses id not null

* feat: cosmetic changes

* feat: cosmetic changes

* feat: update zeus URL

* chore: license testing fixes

* feat: added license status and category handling for query-service

* chore: added v3 support in v2 endpoint

* chore: http response codes and some code cleanup

* chore: added detailed test cases

* chore: address review comments

* chore: some misc cleanup
2024-11-12 01:40:10 +05:30
Shaheer Kochai
577a169508 feat: alert rename interaction (#6208)
* feat: alert rename interaction

* feat: add support for enter and escape shortcuts to submit and cancel rename

* chore: add missing alert field

* chore: update the style similar to dashboard rename

* refactor: remove buttonProps

* chore: add missing alert property to fix the build

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-11-11 19:13:07 +00:00
Shaheer Kochai
939e2a3570 fix: fix the issue of adding new query in new alert page changing the data source (#6286)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-11-11 07:34:20 +00:00
Yunus M
b64326070c [Snyk] Fix for 2 vulnerabilities (#6215)
* fix: frontend/package.json & frontend/yarn.lock to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-UPLOT-6209224
- https://snyk.io/vuln/SNYK-JS-VUETEMPLATECOMPILER-8219888

* chore: upgrade design tokens to 1.1.3

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: ahmadshaheer <ashaheerki@gmail.com>
2024-11-11 06:45:43 +00:00
SagarRajput-7
63872983c6 feat: added metric page in messaging queues (#6399)
* feat: added metric page in messaging queues

* feat: added misc fixes

* feat: removed a class name from mqcards

* feat: added lightMode styles for kafka 2.0 (#6400)

* feat: resolved comments and used strings
2024-11-09 13:04:43 +05:30
Prashant Shahi
eb6670980a Merge pull request #6405 from SigNoz/release/v0.58.x
Release/v0.58.x
2024-11-08 21:49:57 +05:30
Srikanth Chekuri
831540eaf0 fix: test notification missing for anomaly alert (#6391) 2024-11-08 15:40:09 +00:00
Prashant Shahi
48f3b9cacb chore(signoz): pin versions: SigNoz 0.58.0, SigNoz OtelCollector 0.111.8
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-11-08 16:02:59 +05:30
Prashant Shahi
eaf8571fe9 Merge branch 'main' into release/v0.58.x 2024-11-08 15:49:41 +05:30
Nityananda Gohain
22c10f9479 Issue 6367 (#6376)
* fix: issue with orderby by materialized column

* fix: tests

* fix: order by issue in old explorer as well
2024-11-08 07:05:32 +00:00
Yunus M
e748fb0655 chore: update events for onboarding part 2 (#6397) 2024-11-08 06:52:39 +00:00
SagarRajput-7
fdc54a62a9 fix: kafka - misc fix and features (#6379)
* feat: fixed multiple fixes and chores in kafka 2.0

* feat: fixed producer latency - producer-detail call

* feat: fixed mq-detail page layout and pagination

* feat: resolved comments
2024-11-07 23:49:47 +05:30
SagarRajput-7
abe0ab69b0 feat: added kafka - scenario - 4 - drop rate table (#6380)
* feat: added kafka - scenario - 4 - drop rate table

* feat: added api, new table and traceid redirection

* feat: code refactor
2024-11-07 23:37:54 +05:30
Shaheer Kochai
e623c92615 fix: adding the key requires double enter before it gets added as label key after the first label (#6296) 2024-11-07 15:03:59 +00:00
Shaheer Kochai
dc5917db01 chore: setup router compatibility package (#6285) 2024-11-07 19:02:23 +04:30
dependabot[bot]
d6a7f0b6f4 chore(deps): bump express from 4.19.2 to 4.21.1 in /frontend (#6166)
Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.21.1.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 11:48:05 +05:30
Vikrant Gupta
471803115e feat: added support for instrumentation scope in logs (#6378)
* feat: added support for instrumentation scope in logs

* chore: remove console logs

* fix: the logic for rendering prefix

* feat: address review comments

---------

Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
2024-11-07 11:47:35 +05:30
SagarRajput-7
8403a3362d feat: corrected the handling of relativeTime as null in alertHistory page (#6392) 2024-11-07 02:34:11 +05:30
Nityananda Gohain
64d46bc855 feat: support for scope in logs old and new qb (#6339) 2024-11-06 20:47:04 +05:30
SagarRajput-7
c9fee27604 feat: updated the design for Messaging Queue - summary section (#6319)
* feat: updated the design for Messaging Queue - summary section

* feat: resolved comments

* feat: added better logic for switch options and resolved query
2024-11-06 14:44:39 +05:30
SagarRajput-7
f1b6b2d3d8 feat: added onboarding detail for consumer setup (#6372)
* feat: added onboarding detail for consumer setup

* feat: corrected spelling

* feat: reorders imports correctly
2024-11-06 14:43:54 +05:30
SagarRajput-7
468f056530 feat: added kafka - scenario - 4 (#6370)
* feat: added kafka - scenario - 4

* feat: added error handling and correct api handler for kafka apis
2024-11-06 14:24:38 +05:30
SagarRajput-7
7086470ce2 feat: added healthcheck and attribute checklist component for Kafka (#6371)
* feat: added healthcheck and attribute checklist component for Kafka

* feat: corrected the onboardingapi payload

* feat: added missing configuration button at overview and onboarding flow
2024-11-06 14:23:51 +05:30
Yunus M
352296c6cd fix: initialize target to 3 in anomaly detection alert (#6362) 2024-11-05 22:13:12 +05:30
SagarRajput-7
975307a8b8 feat: added onboarding setup for Producer for Messaging queues (#6236)
* fix: added onboarding setup for producer/consumer for Messaging queues

* fix: polled onboarding status api

* feat: added onboarding status api with useQueury functions and updated endpoints

* feat: added onboarding status api util for attribute data

* feat: refactoring and url query changes

* feat: changed start and end time to nanosecond for api payload

* feat: added comment description
2024-11-05 19:40:23 +05:30
SagarRajput-7
12377be809 feat: added generic UI for scenario 1,3,4 (#6287)
* feat: added generic table component for scenario 1,3,4

* feat: added generic logic for mq detail tables and consumed for sc-1,2

* feat: added overview and details table for scenario-3

* feat: added table row clicks func

* feat: resolved comments
2024-11-05 19:26:41 +05:30
Yunus M
9d90b8d19c chore: github wf update pr labels and block pr until related docs are shipped for the feature (#6333) 2024-11-04 23:58:38 +05:30
Yunus M
5005923ef4 fix: re add threshold for promql alerts (#6355) 2024-11-04 15:19:05 +05:30
Srikanth Chekuri
db4338be42 chore: add feature flag, handle out-of-index error, some house keeping work (#6344) 2024-11-02 01:23:43 +05:30
Yunus M
c7d0598ec0 feat: improve async handling for org onboarding cases (#6342) 2024-11-01 23:55:29 +05:30
Yunus M
4978fb9599 fix: add safety check to check if anomaly rule in uplot chart options (#6343) 2024-11-01 22:51:09 +05:30
Shivanshu Raj Shrivastava
7b18c3ba06 enable scenario 4 on staging (#6269)
* fix: enable env at docker compose
2024-11-01 21:19:58 +05:30
Prashant Shahi
b10c22223b Merge pull request #6300 from SigNoz/release/signoz-0.57.0
Release/signoz 0.57.0
2024-10-29 00:45:14 +05:30
Prashant Shahi
cdde369748 Merge branch 'develop' into release/signoz-0.57.0 2024-10-28 21:26:47 +05:30
Prashant Shahi
523cbcd6fc chore: go mod tidy
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-10-28 19:40:23 +05:30
Prashant Shahi
eeadc021e1 chore(signoz): pin versions: SigNoz 0.57.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-10-28 19:40:10 +05:30
669 changed files with 24688 additions and 6531 deletions

83
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,83 @@
name: "Update PR labels and Block PR until related docs are shipped for the feature"
on:
pull_request:
branches:
- develop
types: [opened, edited, labeled, unlabeled]
permissions:
pull-requests: write
contents: read
jobs:
docs_label_check:
runs-on: ubuntu-latest
steps:
- name: Check PR Title and Manage Labels
uses: actions/github-script@v6
with:
script: |
const prTitle = context.payload.pull_request.title;
const prNumber = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
// Fetch the current PR details to get labels
const pr = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
const labels = pr.data.labels.map(label => label.name);
if (prTitle.startsWith('feat:')) {
const hasDocsRequired = labels.includes('docs required');
const hasDocsShipped = labels.includes('docs shipped');
const hasDocsNotRequired = labels.includes('docs not required');
// If "docs not required" is present, skip the checks
if (hasDocsNotRequired && !hasDocsRequired) {
console.log("Skipping checks due to 'docs not required' label.");
return; // Exit the script early
}
// If "docs shipped" is present, remove "docs required" if it exists
if (hasDocsShipped && hasDocsRequired) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: 'docs required'
});
console.log("Removed 'docs required' label.");
}
// Add "docs required" label if neither "docs shipped" nor "docs required" are present
if (!hasDocsRequired && !hasDocsShipped) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: ['docs required']
});
console.log("Added 'docs required' label.");
}
}
// Fetch the updated labels after any changes
const updatedPr = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
const updatedLabels = updatedPr.data.labels.map(label => label.name);
const updatedHasDocsRequired = updatedLabels.includes('docs required');
const updatedHasDocsShipped = updatedLabels.includes('docs shipped');
// Block PR if "docs required" is still present and "docs shipped" is missing
if (updatedHasDocsRequired && !updatedHasDocsShipped) {
core.setFailed("This PR requires documentation. Please remove the 'docs required' label and add the 'docs shipped' label to proceed.");
}

View File

@@ -31,7 +31,6 @@ jobs:
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
KAFKA_SPAN_EVAL: true
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
@@ -39,6 +38,7 @@ jobs:
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
export OTELCOL_TAG="main"
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
export KAFKA_SPAN_EVAL="true"
docker system prune --force
docker pull signoz/signoz-otel-collector:main
docker pull signoz/signoz-schema-migrator:main

View File

@@ -8,6 +8,7 @@ BUILD_HASH ?= $(shell git rev-parse --short HEAD)
BUILD_TIME ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
BUILD_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
DEV_LICENSE_SIGNOZ_IO ?= https://staging-license.signoz.io/api/v1
ZEUS_URL ?= https://api.signoz.cloud
DEV_BUILD ?= "" # set to any non-empty value to enable dev build
# Internal variables or constants.
@@ -33,8 +34,9 @@ buildHash=${PACKAGE}/pkg/query-service/version.buildHash
buildTime=${PACKAGE}/pkg/query-service/version.buildTime
gitBranch=${PACKAGE}/pkg/query-service/version.gitBranch
licenseSignozIo=${PACKAGE}/ee/query-service/constants.LicenseSignozIo
zeusURL=${PACKAGE}/ee/query-service/constants.ZeusURL
LD_FLAGS=-X ${buildHash}=${BUILD_HASH} -X ${buildTime}=${BUILD_TIME} -X ${buildVersion}=${BUILD_VERSION} -X ${gitBranch}=${BUILD_BRANCH}
LD_FLAGS=-X ${buildHash}=${BUILD_HASH} -X ${buildTime}=${BUILD_TIME} -X ${buildVersion}=${BUILD_VERSION} -X ${gitBranch}=${BUILD_BRANCH} -X ${zeusURL}=${ZEUS_URL}
DEV_LD_FLAGS=-X ${licenseSignozIo}=${DEV_LICENSE_SIGNOZ_IO}
all: build-push-frontend build-push-query-service

View File

@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.56.0
image: signoz/query-service:0.61.0
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.56.0
image: signoz/frontend:0.61.0
deploy:
restart_policy:
condition: on-failure
@@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.111.5
image: signoz/signoz-otel-collector:0.111.14
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -237,13 +237,15 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.111.5
image: signoz/signoz-schema-migrator:0.111.14
deploy:
restart_policy:
condition: on-failure
delay: 5s
command:
- "--dsn=tcp://clickhouse:9000"
- "sync"
- "--dsn=tcp://clickhouse:9000"
- "--up="
depends_on:
- clickhouse
# - clickhouse-2

View File

@@ -66,28 +66,6 @@ processors:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors: [env, system] # include ec2 for AWS, gcp for GCP and azure for Azure.
timeout: 2s
signozspanmetrics/cumulative:
metrics_exporter: clickhousemetricswrite
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
dimensions:
- name: service.namespace
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
# memory_limiter:
# # 80% of maximum memory up to 2G
# limit_mib: 1500
@@ -138,6 +116,8 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhousemetricswritev2:
dsn: tcp://clickhouse:9000/signoz_metrics
# logging: {}
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
@@ -161,20 +141,20 @@ service:
pipelines:
traces:
receivers: [jaeger, otlp]
processors: [signozspanmetrics/cumulative, signozspanmetrics/delta, batch]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite]
metrics/generic:
exporters: [clickhousemetricswrite, clickhousemetricswritev2]
metrics/hostmetrics:
receivers: [hostmetrics]
processors: [resourcedetection, batch]
exporters: [clickhousemetricswrite]
exporters: [clickhousemetricswrite, clickhousemetricswritev2]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus]
exporters: [clickhousemetricswrite/prometheus, clickhousemetricswritev2]
logs:
receivers: [otlp, tcplog/docker]
processors: [batch]

View File

@@ -69,10 +69,12 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14}
container_name: otel-migrator
command:
- "sync"
- "--dsn=tcp://clickhouse:9000"
- "--up="
depends_on:
clickhouse:
condition: service_healthy
@@ -84,7 +86,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`
otel-collector:
container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.111.5
image: signoz/signoz-otel-collector:0.111.14
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@@ -162,7 +162,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:${DOCKER_TAG:-0.56.0}
image: signoz/query-service:${DOCKER_TAG:-0.61.0}
container_name: signoz-query-service
command:
[
@@ -201,7 +201,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
image: signoz/frontend:${DOCKER_TAG:-0.61.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -213,7 +213,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator-sync:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14}
container_name: otel-migrator-sync
command:
- "sync"
@@ -228,7 +228,7 @@ services:
# condition: service_healthy
otel-collector-migrator-async:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14}
container_name: otel-migrator-async
command:
- "async"
@@ -245,7 +245,7 @@ services:
# condition: service_healthy
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.14}
container_name: signoz-otel-collector
command:
[

View File

@@ -167,7 +167,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:${DOCKER_TAG:-0.56.0}
image: signoz/query-service:${DOCKER_TAG:-0.61.0}
container_name: signoz-query-service
command:
[
@@ -191,6 +191,7 @@ services:
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-amd
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
restart: on-failure
healthcheck:
test:
@@ -207,7 +208,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
image: signoz/frontend:${DOCKER_TAG:-0.61.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -219,7 +220,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -233,7 +234,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.14}
container_name: signoz-otel-collector
command:
[

View File

@@ -57,35 +57,11 @@ receivers:
labels:
job_name: otel-collector
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
signozspanmetrics/cumulative:
metrics_exporter: clickhousemetricswrite
metrics_flush_interval: 60s
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
dimensions:
- name: service.namespace
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
# memory_limiter:
# # 80% of maximum memory up to 2G
# limit_mib: 1500
@@ -149,6 +125,8 @@ exporters:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhousemetricswritev2:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
timeout: 10s
@@ -168,20 +146,20 @@ service:
pipelines:
traces:
receivers: [jaeger, otlp]
processors: [signozspanmetrics/cumulative, signozspanmetrics/delta, batch]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite]
metrics/generic:
exporters: [clickhousemetricswrite, clickhousemetricswritev2]
metrics/hostmetrics:
receivers: [hostmetrics]
processors: [resourcedetection, batch]
exporters: [clickhousemetricswrite]
exporters: [clickhousemetricswrite, clickhousemetricswritev2]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus]
exporters: [clickhousemetricswrite/prometheus, clickhousemetricswritev2]
logs:
receivers: [otlp, tcplog/docker]
processors: [batch]

View File

@@ -1,5 +1,5 @@
# use a minimal alpine image
FROM alpine:3.18.6
FROM alpine:3.20.3
# Add Maintainer Info
LABEL maintainer="signoz"

View File

@@ -38,8 +38,9 @@ type APIHandlerOptions struct {
Cache cache.Cache
Gateway *httputil.ReverseProxy
// Querier Influx Interval
FluxInterval time.Duration
UseLogsNewSchema bool
FluxInterval time.Duration
UseLogsNewSchema bool
UseTraceNewSchema bool
}
type APIHandler struct {
@@ -65,6 +66,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
Cache: opts.Cache,
FluxInterval: opts.FluxInterval,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema,
})
if err != nil {
@@ -173,14 +175,22 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
// v2
router.HandleFunc("/api/v2/licenses",
am.ViewAccess(ah.listLicensesV2)).
Methods(http.MethodGet)
// v3
router.HandleFunc("/api/v3/licenses", am.ViewAccess(ah.listLicensesV3)).Methods(http.MethodGet)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.applyLicenseV3)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.refreshLicensesV3)).Methods(http.MethodPut)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.getActiveLicenseV3)).Methods(http.MethodGet)
// v4
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
// Gateway
router.PathPrefix(gateway.RoutePrefix).HandlerFunc(am.AdminAccess(ah.ServeGatewayHTTP))
router.PathPrefix(gateway.RoutePrefix).HandlerFunc(am.EditAccess(ah.ServeGatewayHTTP))
ah.APIHandler.RegisterRoutes(router, am)

View File

@@ -9,6 +9,7 @@ import (
"go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model"
"go.signoz.io/signoz/pkg/http/render"
"go.uber.org/zap"
)
@@ -59,6 +60,21 @@ type billingDetails struct {
} `json:"data"`
}
type ApplyLicenseRequest struct {
LicenseKey string `json:"key"`
}
type ListLicenseResponse map[string]interface{}
func convertLicenseV3ToListLicenseResponse(licensesV3 []*model.LicenseV3) []ListLicenseResponse {
listLicenses := []ListLicenseResponse{}
for _, license := range licensesV3 {
listLicenses = append(listLicenses, license.Data)
}
return listLicenses
}
func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicenses(context.Background())
if apiError != nil {
@@ -79,7 +95,7 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
return
}
license, apiError := ah.LM().Activate(r.Context(), l.Key)
license, apiError := ah.LM().ActivateV3(r.Context(), l.Key)
if apiError != nil {
RespondError(w, apiError, nil)
return
@@ -88,6 +104,68 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
ah.Respond(w, license)
}
func (ah *APIHandler) listLicensesV3(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicensesV3(r.Context())
if apiError != nil {
RespondError(w, apiError, nil)
return
}
ah.Respond(w, convertLicenseV3ToListLicenseResponse(licenses))
}
func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request) {
activeLicense, err := ah.LM().GetRepo().GetActiveLicenseV3(r.Context())
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
// return 404 not found if there is no active license
if activeLicense == nil {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no active license found")}, nil)
return
}
// TODO deprecate this when we move away from key for stripe
activeLicense.Data["key"] = activeLicense.Key
render.Success(w, http.StatusOK, activeLicense.Data)
}
// this function is called by zeus when inserting licenses in the query-service
func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
var licenseKey ApplyLicenseRequest
if err := json.NewDecoder(r.Body).Decode(&licenseKey); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
if licenseKey.LicenseKey == "" {
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
return
}
_, apiError := ah.LM().ActivateV3(r.Context(), licenseKey.LicenseKey)
if apiError != nil {
RespondError(w, apiError, nil)
return
}
render.Success(w, http.StatusAccepted, nil)
}
func (ah *APIHandler) refreshLicensesV3(w http.ResponseWriter, r *http.Request) {
apiError := ah.LM().RefreshLicense(r.Context())
if apiError != nil {
RespondError(w, apiError, nil)
return
}
render.Success(w, http.StatusNoContent, nil)
}
func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
type checkoutResponse struct {
@@ -154,12 +232,38 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
ah.Respond(w, billingResponse.Data)
}
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicenses(context.Background())
if apiError != nil {
RespondError(w, apiError, nil)
func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License {
licensesV2 := []model.License{}
for _, l := range licenses {
planKeyFromPlanName, ok := model.MapOldPlanKeyToNewPlanName[l.PlanName]
if !ok {
planKeyFromPlanName = model.Basic
}
licenseV2 := model.License{
Key: l.Key,
ActivationId: "",
PlanDetails: "",
FeatureSet: l.Features,
ValidationMessage: "",
IsCurrent: l.IsCurrent,
LicensePlan: model.LicensePlan{
PlanKey: planKeyFromPlanName,
ValidFrom: l.ValidFrom,
ValidUntil: l.ValidUntil,
Status: l.Status},
}
licensesV2 = append(licensesV2, licenseV2)
}
return licensesV2
}
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
licensesV3, apierr := ah.LM().GetLicensesV3(r.Context())
if apierr != nil {
RespondError(w, apierr, nil)
return
}
licenses := convertLicenseV3ToLicenseV2(licensesV3)
resp := model.Licenses{
TrialStart: -1,

View File

@@ -26,8 +26,9 @@ func NewDataConnector(
dialTimeout time.Duration,
cluster string,
useLogsNewSchema bool,
useTraceNewSchema bool,
) *ClickhouseReader {
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema)
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema)
return &ClickhouseReader{
conn: ch.GetConn(),
appdb: localDB,

View File

@@ -31,7 +31,6 @@ import (
"go.signoz.io/signoz/ee/query-service/rules"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
licensepkg "go.signoz.io/signoz/ee/query-service/license"
@@ -78,6 +77,7 @@ type ServerOptions struct {
Cluster string
GatewayUrl string
UseLogsNewSchema bool
UseTraceNewSchema bool
}
// Server runs HTTP api service
@@ -156,6 +156,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.DialTimeout,
serverOptions.Cluster,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
)
go qb.Start(readerReady)
reader = qb
@@ -189,6 +190,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.DisableRules,
lm,
serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema,
)
if err != nil {
@@ -270,6 +272,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
FluxInterval: fluxInterval,
Gateway: gatewayProxy,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
}
apiHandler, err := api.NewAPIHandler(apiOpts)
@@ -312,10 +315,10 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
r := baseapp.NewRouter()
r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware)
r.Use(loggingMiddlewarePrivate)
r.Use(baseapp.LogCommentEnricher)
apiHandler.RegisterPrivateRoutes(r)
@@ -348,17 +351,17 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
}
if user.User.OrgId == "" {
return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims"))
return nil, basemodel.UnauthorizedError(errors.New("orgId is missing in the claims"))
}
return user, nil
}
am := baseapp.NewAuthMiddleware(getUserFromRequest)
r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware)
r.Use(loggingMiddleware)
r.Use(baseapp.LogCommentEnricher)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
@@ -736,7 +739,8 @@ func makeRulesManager(
cache cache.Cache,
disableRules bool,
fm baseint.FeatureLookup,
useLogsNewSchema bool) (*baserules.Manager, error) {
useLogsNewSchema bool,
useTraceNewSchema bool) (*baserules.Manager, error) {
// create engine
pqle, err := pqle.FromConfigPath(promConfigPath)
@@ -765,8 +769,10 @@ func makeRulesManager(
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
UseLogsNewSchema: useLogsNewSchema,
PrepareTaskFunc: rules.PrepareTaskFunc,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
PrepareTestRuleFunc: rules.TestNotification,
}
// create Manager

View File

@@ -14,6 +14,9 @@ var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
// this is set via build time variable
var ZeusURL = "https://api.signoz.cloud"
func GetOrDefaultEnv(key string, fallback string) string {
v := os.Getenv(key)
if len(v) == 0 {

View File

@@ -2,14 +2,7 @@ package signozio
type status string
type ActivationResult struct {
Status status `json:"status"`
Data *ActivationResponse `json:"data,omitempty"`
ErrorType string `json:"errorType,omitempty"`
Error string `json:"error,omitempty"`
}
type ActivationResponse struct {
ActivationId string `json:"ActivationId"`
PlanDetails string `json:"PlanDetails"`
type ValidateLicenseResponse struct {
Status status `json:"status"`
Data map[string]interface{} `json:"data"`
}

View File

@@ -7,9 +7,9 @@ import (
"fmt"
"io"
"net/http"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
"go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model"
@@ -23,12 +23,14 @@ const (
)
type Client struct {
Prefix string
Prefix string
GatewayUrl string
}
func New() *Client {
return &Client{
Prefix: constants.LicenseSignozIo,
Prefix: constants.LicenseSignozIo,
GatewayUrl: constants.ZeusURL,
}
}
@@ -36,82 +38,56 @@ func init() {
C = New()
}
// ActivateLicense sends key to license.signoz.io and gets activation data
func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError) {
licenseReq := map[string]string{
"key": key,
"siteId": siteId,
func ValidateLicenseV3(licenseKey string) (*model.LicenseV3, *model.ApiError) {
// Creating an HTTP client with a timeout for better control
client := &http.Client{
Timeout: 10 * time.Second,
}
reqString, _ := json.Marshal(licenseReq)
httpResponse, err := http.Post(C.Prefix+"/licenses/activate", APPLICATION_JSON, bytes.NewBuffer(reqString))
req, err := http.NewRequest("GET", C.GatewayUrl+"/v2/licenses/me", nil)
if err != nil {
zap.L().Error("failed to connect to license.signoz.io", zap.Error(err))
return nil, model.BadRequest(fmt.Errorf("unable to connect with license.signoz.io, please check your network connection"))
return nil, model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to create request: %w", err)))
}
httpBody, err := io.ReadAll(httpResponse.Body)
// Setting the custom header
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
response, err := client.Do(req)
if err != nil {
zap.L().Error("failed to read activation response from license.signoz.io", zap.Error(err))
return nil, model.BadRequest(fmt.Errorf("failed to read activation response from license.signoz.io"))
}
defer httpResponse.Body.Close()
// read api request result
result := ActivationResult{}
err = json.Unmarshal(httpBody, &result)
if err != nil {
zap.L().Error("failed to marshal activation response from license.signoz.io", zap.Error(err))
return nil, model.InternalError(errors.Wrap(err, "failed to marshal license activation response"))
}
switch httpResponse.StatusCode {
case 200, 201:
return result.Data, nil
case 400, 401:
return nil, model.BadRequest(fmt.Errorf(fmt.Sprintf("failed to activate: %s", result.Error)))
default:
return nil, model.InternalError(fmt.Errorf(fmt.Sprintf("failed to activate: %s", result.Error)))
}
}
// ValidateLicense validates the license key
func ValidateLicense(activationId string) (*ActivationResponse, *model.ApiError) {
validReq := map[string]string{
"activationId": activationId,
}
reqString, _ := json.Marshal(validReq)
response, err := http.Post(C.Prefix+"/licenses/validate", APPLICATION_JSON, bytes.NewBuffer(reqString))
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "unable to connect with license.signoz.io, please check your network connection"))
return nil, model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to make post request: %w", err)))
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to read validation response from license.signoz.io"))
return nil, model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to read validation response from %v", C.GatewayUrl)))
}
defer response.Body.Close()
switch response.StatusCode {
case 200, 201:
a := ActivationResult{}
case 200:
a := ValidateLicenseResponse{}
err = json.Unmarshal(body, &a)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to marshal license validation response"))
}
return a.Data, nil
case 400, 401:
license, err := model.NewLicenseV3(a.Data)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to generate new license v3"))
}
return license, nil
case 400:
return nil, model.BadRequest(errors.Wrap(fmt.Errorf(string(body)),
"bad request error received from license.signoz.io"))
fmt.Sprintf("bad request error received from %v", C.GatewayUrl)))
case 401:
return nil, model.Unauthorized(errors.Wrap(fmt.Errorf(string(body)),
fmt.Sprintf("unauthorized request error received from %v", C.GatewayUrl)))
default:
return nil, model.InternalError(errors.Wrap(fmt.Errorf(string(body)),
"internal error received from license.signoz.io"))
fmt.Sprintf("internal request error received from %v", C.GatewayUrl)))
}
}

View File

@@ -3,10 +3,12 @@ package license
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
"go.signoz.io/signoz/ee/query-service/license/sqlite"
"go.signoz.io/signoz/ee/query-service/model"
@@ -48,9 +50,35 @@ func (r *Repo) GetLicenses(ctx context.Context) ([]model.License, error) {
return licenses, nil
}
// GetActiveLicense fetches the latest active license from DB.
// If the license is not present, expect a nil license and a nil error in the output.
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
licensesData := []model.LicenseDB{}
licenseV3Data := []*model.LicenseV3{}
query := "SELECT id,key,data FROM licenses_v3"
err := r.db.Select(&licensesData, query)
if err != nil {
return nil, fmt.Errorf("failed to get licenses from db: %v", err)
}
for _, l := range licensesData {
var licenseData map[string]interface{}
err := json.Unmarshal([]byte(l.Data), &licenseData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
}
license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
if err != nil {
return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
}
licenseV3Data = append(licenseV3Data, license)
}
return licenseV3Data, nil
}
func (r *Repo) GetActiveLicenseV2(ctx context.Context) (*model.License, *basemodel.ApiError) {
var err error
licenses := []model.License{}
@@ -79,6 +107,60 @@ func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel
return active, nil
}
// GetActiveLicense fetches the latest active license from DB.
// If the license is not present, expect a nil license and a nil error in the output.
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
if activeLicenseV3 == nil {
return nil, nil
}
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
return activeLicenseV2, nil
}
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {
var err error
licenses := []model.LicenseDB{}
query := "SELECT id,key,data FROM licenses_v3"
err = r.db.Select(&licenses, query)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
var active *model.LicenseV3
for _, l := range licenses {
var licenseData map[string]interface{}
err := json.Unmarshal([]byte(l.Data), &licenseData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
}
license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
if err != nil {
return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
}
if active == nil &&
(license.ValidFrom != 0) &&
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
active = license
}
if active != nil &&
license.ValidFrom > active.ValidFrom &&
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
active = license
}
}
return active, nil
}
// InsertLicense inserts a new license in db
func (r *Repo) InsertLicense(ctx context.Context, l *model.License) error {
@@ -204,3 +286,59 @@ func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
}
return nil
}
// InsertLicenseV3 inserts a new license v3 in db
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
// licsense is the entity of zeus so putting the entire license here without defining schema
licenseData, err := json.Marshal(l.Data)
if err != nil {
return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
}
_, err = r.db.ExecContext(ctx,
query,
l.ID,
l.Key,
string(licenseData),
)
if err != nil {
if sqliteErr, ok := err.(sqlite3.Error); ok {
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
}
}
zap.L().Error("error in inserting license data: ", zap.Error(err))
return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
}
return nil
}
// UpdateLicenseV3 updates a new license v3 in db
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
// the key and id for the license can't change so only update the data here!
query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
license, err := json.Marshal(l.Data)
if err != nil {
return fmt.Errorf("insert license failed: license marshal error")
}
_, err = r.db.ExecContext(ctx,
query,
license,
l.ID,
)
if err != nil {
zap.L().Error("error in updating license data: ", zap.Error(err))
return fmt.Errorf("failed to update license in db: %v", err)
}
return nil
}

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"sync"
@@ -45,8 +46,9 @@ type Manager struct {
failedAttempts uint64
// keep track of active license and features
activeLicense *model.License
activeFeatures basemodel.FeatureSet
activeLicense *model.License
activeLicenseV3 *model.LicenseV3
activeFeatures basemodel.FeatureSet
}
func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
@@ -74,9 +76,7 @@ func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*M
// start loads active license in memory and initiates validator
func (lm *Manager) start(features ...basemodel.Feature) error {
err := lm.LoadActiveLicense(features...)
return err
return lm.LoadActiveLicenseV3(features...)
}
func (lm *Manager) Stop() {
@@ -84,7 +84,7 @@ func (lm *Manager) Stop() {
<-lm.terminated
}
func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) {
func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
@@ -92,8 +92,8 @@ func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) {
return
}
lm.activeLicense = l
lm.activeFeatures = append(l.FeatureSet, features...)
lm.activeLicenseV3 = l
lm.activeFeatures = append(l.Features, features...)
// set default features
setDefaultFeatures(lm)
@@ -105,7 +105,7 @@ func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) {
// we want to make sure only one validator runs,
// we already have lock() so good to go
lm.validatorRunning = true
go lm.Validator(context.Background())
go lm.ValidatorV3(context.Background())
}
}
@@ -114,14 +114,13 @@ func setDefaultFeatures(lm *Manager) {
lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
}
// LoadActiveLicense loads the most recent active license
func (lm *Manager) LoadActiveLicense(features ...basemodel.Feature) error {
active, err := lm.repo.GetActiveLicense(context.Background())
func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
active, err := lm.repo.GetActiveLicenseV3(context.Background())
if err != nil {
return err
}
if active != nil {
lm.SetActive(active, features...)
lm.SetActiveV3(active, features...)
} else {
zap.L().Info("No active license found, defaulting to basic plan")
// if no active license is found, we default to basic(free) plan with all default features
@@ -163,13 +162,36 @@ func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, a
return
}
func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.LicenseV3, apiError *model.ApiError) {
licenses, err := lm.repo.GetLicensesV3(ctx)
if err != nil {
return nil, model.InternalError(err)
}
for _, l := range licenses {
if lm.activeLicenseV3 != nil && l.Key == lm.activeLicenseV3.Key {
l.IsCurrent = true
}
if l.ValidUntil == -1 {
// for subscriptions, there is no end-date as such
// but for showing user some validity we default one year timespan
l.ValidUntil = l.ValidFrom + 31556926
}
response = append(response, l)
}
return response, nil
}
// Validator validates license after an epoch of time
func (lm *Manager) Validator(ctx context.Context) {
func (lm *Manager) ValidatorV3(ctx context.Context) {
zap.L().Info("ValidatorV3 started!")
defer close(lm.terminated)
tick := time.NewTicker(validationFrequency)
defer tick.Stop()
lm.Validate(ctx)
lm.ValidateV3(ctx)
for {
select {
@@ -180,17 +202,33 @@ func (lm *Manager) Validator(ctx context.Context) {
case <-lm.done:
return
case <-tick.C:
lm.Validate(ctx)
lm.ValidateV3(ctx)
}
}
}
}
// Validate validates the current active license
func (lm *Manager) Validate(ctx context.Context) (reterr error) {
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)
if apiError != nil {
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
return apiError
}
err := lm.repo.UpdateLicenseV3(ctx, license)
if err != nil {
return model.BadRequest(errors.Wrap(err, "failed to update the new license"))
}
lm.SetActiveV3(license)
return nil
}
func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
zap.L().Info("License validation started")
if lm.activeLicense == nil {
if lm.activeLicenseV3 == nil {
return nil
}
@@ -210,52 +248,15 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
lm.mutex.Unlock()
}()
response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId)
if apiError != nil {
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
return apiError.Err
err := lm.RefreshLicense(ctx)
if err != nil {
return err
}
if response.PlanDetails == lm.activeLicense.PlanDetails {
// license plan hasnt changed, nothing to do
return nil
}
if response.PlanDetails != "" {
// copy and replace the active license record
l := model.License{
Key: lm.activeLicense.Key,
CreatedAt: lm.activeLicense.CreatedAt,
PlanDetails: response.PlanDetails,
ValidationMessage: lm.activeLicense.ValidationMessage,
ActivationId: lm.activeLicense.ActivationId,
}
if err := l.ParsePlan(); err != nil {
zap.L().Error("failed to parse updated license", zap.Error(err))
return err
}
// updated plan is parsable, check if plan has changed
if lm.activeLicense.PlanDetails != response.PlanDetails {
err := lm.repo.UpdatePlanDetails(ctx, lm.activeLicense.Key, response.PlanDetails)
if err != nil {
// unexpected db write issue but we can let the user continue
// and wait for update to work in next cycle.
zap.L().Error("failed to validate license", zap.Error(err))
}
}
// activate the update license plan
lm.SetActive(&l)
}
return nil
}
// Activate activates a license key with signoz server
func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *model.License, errResponse *model.ApiError) {
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
defer func() {
if errResponse != nil {
userEmail, err := auth.GetEmailFromJwt(ctx)
@@ -266,36 +267,22 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
}
}()
response, apiError := validate.ActivateLicense(key, "")
license, apiError := validate.ValidateLicenseV3(licenseKey)
if apiError != nil {
zap.L().Error("failed to activate license", zap.Error(apiError.Err))
zap.L().Error("failed to get the license", zap.Error(apiError.Err))
return nil, apiError
}
l := &model.License{
Key: key,
ActivationId: response.ActivationId,
PlanDetails: response.PlanDetails,
}
// parse validity and features from the plan details
err := l.ParsePlan()
// insert the new license to the sqlite db
err := lm.repo.InsertLicenseV3(ctx, license)
if err != nil {
zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err)
}
// store the license before activating it
err = lm.repo.InsertLicense(ctx, l)
if err != nil {
zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err)
return nil, err
}
// license is valid, activate it
lm.SetActive(l)
return l, nil
lm.SetActiveV3(license)
return license, nil
}
// CheckFeature will be internally used by backend routines

View File

@@ -48,5 +48,16 @@ func InitDB(db *sqlx.DB) error {
return fmt.Errorf("error in creating feature_status table: %s", err.Error())
}
table_schema = `CREATE TABLE IF NOT EXISTS licenses_v3 (
id TEXT PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
data TEXT
);`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating licenses_v3 table: %s", err.Error())
}
return nil
}

View File

@@ -94,6 +94,7 @@ func main() {
var cluster string
var useLogsNewSchema bool
var useTraceNewSchema bool
var cacheConfigPath, fluxInterval string
var enableQueryServiceLogOTLPExport bool
var preferSpanMetrics bool
@@ -102,8 +103,10 @@ func main() {
var maxOpenConns int
var dialTimeout time.Duration
var gatewayUrl string
var useLicensesV3 bool
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
@@ -117,6 +120,7 @@ func main() {
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.Parse()
@@ -143,6 +147,7 @@ func main() {
Cluster: cluster,
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
}
// Read the jwt secret key

View File

@@ -46,6 +46,13 @@ func BadRequest(err error) *ApiError {
}
}
func Unauthorized(err error) *ApiError {
return &ApiError{
Typ: basemodel.ErrorUnauthorized,
Err: err,
}
}
// BadRequestStr returns a ApiError object of bad request for string input
func BadRequestStr(s string) *ApiError {
return &ApiError{

View File

@@ -3,6 +3,8 @@ package model
import (
"encoding/base64"
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/pkg/errors"
@@ -104,3 +106,165 @@ type SubscriptionServerResp struct {
Status string `json:"status"`
Data Licenses `json:"data"`
}
type Plan struct {
Name string `json:"name"`
}
type LicenseDB struct {
ID string `json:"id"`
Key string `json:"key"`
Data string `json:"data"`
}
type LicenseV3 struct {
ID string
Key string
Data map[string]interface{}
PlanName string
Features basemodel.FeatureSet
Status string
IsCurrent bool
ValidFrom int64
ValidUntil int64
}
func extractKeyFromMapStringInterface[T any](data map[string]interface{}, key string) (T, error) {
var zeroValue T
if val, ok := data[key]; ok {
if value, ok := val.(T); ok {
return value, nil
}
return zeroValue, fmt.Errorf("%s key is not a valid %s", key, reflect.TypeOf(zeroValue))
}
return zeroValue, fmt.Errorf("%s key is missing", key)
}
func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
var features basemodel.FeatureSet
// extract id from data
licenseID, err := extractKeyFromMapStringInterface[string](data, "id")
if err != nil {
return nil, err
}
delete(data, "id")
// extract key from data
licenseKey, err := extractKeyFromMapStringInterface[string](data, "key")
if err != nil {
return nil, err
}
delete(data, "key")
// extract status from data
status, err := extractKeyFromMapStringInterface[string](data, "status")
if err != nil {
return nil, err
}
planMap, err := extractKeyFromMapStringInterface[map[string]any](data, "plan")
if err != nil {
return nil, err
}
planName, err := extractKeyFromMapStringInterface[string](planMap, "name")
if err != nil {
return nil, err
}
// if license status is inactive then default it to basic
if status == LicenseStatusInactive {
planName = PlanNameBasic
}
featuresFromZeus := basemodel.FeatureSet{}
if _features, ok := data["features"]; ok {
featuresData, err := json.Marshal(_features)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal features data")
}
if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal features data")
}
}
switch planName {
case PlanNameTeams:
features = append(features, ProPlan...)
case PlanNameEnterprise:
features = append(features, EnterprisePlan...)
case PlanNameBasic:
features = append(features, BasicPlan...)
default:
features = append(features, BasicPlan...)
}
if len(featuresFromZeus) > 0 {
for _, feature := range featuresFromZeus {
exists := false
for i, existingFeature := range features {
if existingFeature.Name == feature.Name {
features[i] = feature // Replace existing feature
exists = true
break
}
}
if !exists {
features = append(features, feature) // Append if it doesn't exist
}
}
}
data["features"] = features
_validFrom, err := extractKeyFromMapStringInterface[float64](data, "valid_from")
if err != nil {
_validFrom = 0
}
validFrom := int64(_validFrom)
_validUntil, err := extractKeyFromMapStringInterface[float64](data, "valid_until")
if err != nil {
_validUntil = 0
}
validUntil := int64(_validUntil)
return &LicenseV3{
ID: licenseID,
Key: licenseKey,
Data: data,
PlanName: planName,
Features: features,
ValidFrom: validFrom,
ValidUntil: validUntil,
Status: status,
}, nil
}
func NewLicenseV3WithIDAndKey(id string, key string, data map[string]interface{}) (*LicenseV3, error) {
licenseDataWithIdAndKey := data
licenseDataWithIdAndKey["id"] = id
licenseDataWithIdAndKey["key"] = key
return NewLicenseV3(licenseDataWithIdAndKey)
}
func ConvertLicenseV3ToLicenseV2(l *LicenseV3) *License {
planKeyFromPlanName, ok := MapOldPlanKeyToNewPlanName[l.PlanName]
if !ok {
planKeyFromPlanName = Basic
}
return &License{
Key: l.Key,
ActivationId: "",
PlanDetails: "",
FeatureSet: l.Features,
ValidationMessage: "",
IsCurrent: l.IsCurrent,
LicensePlan: LicensePlan{
PlanKey: planKeyFromPlanName,
ValidFrom: l.ValidFrom,
ValidUntil: l.ValidUntil,
Status: l.Status},
}
}

View File

@@ -0,0 +1,170 @@
package model
import (
"encoding/json"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/query-service/model"
)
func TestNewLicenseV3(t *testing.T) {
testCases := []struct {
name string
data []byte
pass bool
expected *LicenseV3
error error
}{
{
name: "Error for missing license id",
data: []byte(`{}`),
pass: false,
error: errors.New("id key is missing"),
},
{
name: "Error for license id not being a valid string",
data: []byte(`{"id": 10}`),
pass: false,
error: errors.New("id key is not a valid string"),
},
{
name: "Error for missing license key",
data: []byte(`{"id":"does-not-matter"}`),
pass: false,
error: errors.New("key key is missing"),
},
{
name: "Error for invalid string license key",
data: []byte(`{"id":"does-not-matter","key":10}`),
pass: false,
error: errors.New("key key is not a valid string"),
},
{
name: "Error for missing license status",
data: []byte(`{"id":"does-not-matter", "key": "does-not-matter","category":"FREE"}`),
pass: false,
error: errors.New("status key is missing"),
},
{
name: "Error for invalid string license status",
data: []byte(`{"id":"does-not-matter","key": "does-not-matter", "category":"FREE", "status":10}`),
pass: false,
error: errors.New("status key is not a valid string"),
},
{
name: "Error for missing license plan",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE"}`),
pass: false,
error: errors.New("plan key is missing"),
},
{
name: "Error for invalid json license plan",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":10}`),
pass: false,
error: errors.New("plan key is not a valid map[string]interface {}"),
},
{
name: "Error for invalid license plan",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{}}`),
pass: false,
error: errors.New("name key is missing"),
},
{
name: "Parse the entire license properly",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from": 1730899309,"valid_until": -1}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"category": "FREE",
"status": "ACTIVE",
"valid_from": float64(1730899309),
"valid_until": float64(-1),
},
PlanName: PlanNameTeams,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "ACTIVE",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
{
name: "Fallback to basic plan if license status is inactive",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"INACTIVE","plan":{"name":"TEAMS"},"valid_from": 1730899309,"valid_until": -1}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"category": "FREE",
"status": "INACTIVE",
"valid_from": float64(1730899309),
"valid_until": float64(-1),
},
PlanName: PlanNameBasic,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "INACTIVE",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
{
name: "fallback states for validFrom and validUntil",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from":1234.456,"valid_until":5678.567}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"valid_from": 1234.456,
"valid_until": 5678.567,
"category": "FREE",
"status": "ACTIVE",
},
PlanName: PlanNameTeams,
ValidFrom: 1234,
ValidUntil: 5678,
Status: "ACTIVE",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
}
for _, tc := range testCases {
var licensePayload map[string]interface{}
err := json.Unmarshal(tc.data, &licensePayload)
require.NoError(t, err)
license, err := NewLicenseV3(licensePayload)
if license != nil {
license.Features = make(model.FeatureSet, 0)
delete(license.Data, "features")
}
if tc.pass {
require.NoError(t, err)
require.NotNil(t, license)
assert.Equal(t, tc.expected, license)
} else {
require.Error(t, err)
assert.EqualError(t, err, tc.error.Error())
require.Nil(t, license)
}
}
}

View File

@@ -1,6 +1,7 @@
package model
import (
"go.signoz.io/signoz/pkg/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
)
@@ -8,6 +9,21 @@ const SSO = "SSO"
const Basic = "BASIC_PLAN"
const Pro = "PRO_PLAN"
const Enterprise = "ENTERPRISE_PLAN"
var (
PlanNameEnterprise = "ENTERPRISE"
PlanNameTeams = "TEAMS"
PlanNameBasic = "BASIC"
)
var (
MapOldPlanKeyToNewPlanName map[string]string = map[string]string{PlanNameBasic: Basic, PlanNameTeams: Pro, PlanNameEnterprise: Enterprise}
)
var (
LicenseStatusInactive = "INACTIVE"
)
const DisableUpsell = "DISABLE_UPSELL"
const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
@@ -134,6 +150,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.HostsInfraMonitoring,
Active: constants.EnableHostsInfraMonitoring(),
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
@@ -249,6 +272,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.HostsInfraMonitoring,
Active: constants.EnableHostsInfraMonitoring(),
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -378,4 +408,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.HostsInfraMonitoring,
Active: constants.EnableHostsInfraMonitoring(),
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -61,6 +61,11 @@ func NewAnomalyRule(
zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts))
if p.RuleCondition.CompareOp == baserules.ValueIsBelow {
target := -1 * *p.RuleCondition.Target
p.RuleCondition.Target = &target
}
baseRule, err := baserules.NewBaseRule(id, p, reader, opts...)
if err != nil {
return nil, err

View File

@@ -1,10 +1,15 @@
package rules
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
baserules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/utils/labels"
"go.uber.org/zap"
)
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
@@ -21,6 +26,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.FF,
opts.Reader,
opts.UseLogsNewSchema,
opts.UseTraceNewSchema,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
)
@@ -79,6 +85,107 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
return task, nil
}
// TestNotification prepares a dummy rule for given rule parameters and
// sends a test notification. returns alert count and error (if any)
func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.ApiError) {
ctx := context.Background()
if opts.Rule == nil {
return 0, basemodel.BadRequest(fmt.Errorf("rule is required"))
}
parsedRule := opts.Rule
var alertname = parsedRule.AlertName
if alertname == "" {
// alertname is not mandatory for testing, so picking
// a random string here
alertname = uuid.New().String()
}
// append name to indicate this is test alert
parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, baserules.TestAlertPostFix)
var rule baserules.Rule
var err error
if parsedRule.RuleType == baserules.RuleTypeThreshold {
// add special labels for test alerts
parsedRule.Annotations[labels.AlertSummaryLabel] = fmt.Sprintf("The rule threshold is set to %.4f, and the observed metric value is {{$value}}.", *parsedRule.RuleCondition.Target)
parsedRule.Labels[labels.RuleSourceLabel] = ""
parsedRule.Labels[labels.AlertRuleIdLabel] = ""
// create a threshold rule
rule, err = baserules.NewThresholdRule(
alertname,
parsedRule,
opts.FF,
opts.Reader,
opts.UseLogsNewSchema,
opts.UseTraceNewSchema,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
)
if err != nil {
zap.L().Error("failed to prepare a new threshold rule for test", zap.String("name", rule.Name()), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
} else if parsedRule.RuleType == baserules.RuleTypeProm {
// create promql rule
rule, err = baserules.NewPromRule(
alertname,
parsedRule,
opts.Logger,
opts.Reader,
opts.ManagerOpts.PqlEngine,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
)
if err != nil {
zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", rule.Name()), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
} else if parsedRule.RuleType == baserules.RuleTypeAnomaly {
// create anomaly rule
rule, err = NewAnomalyRule(
alertname,
parsedRule,
opts.FF,
opts.Reader,
opts.Cache,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
)
if err != nil {
zap.L().Error("failed to prepare a new anomaly rule for test", zap.String("name", rule.Name()), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
} else {
return 0, basemodel.BadRequest(fmt.Errorf("failed to derive ruletype with given information"))
}
// set timestamp to current utc time
ts := time.Now().UTC()
count, err := rule.Eval(ctx, ts)
if err != nil {
zap.L().Error("evaluating rule failed", zap.String("rule", rule.Name()), zap.Error(err))
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
}
alertsFound, ok := count.(int)
if !ok {
return 0, basemodel.InternalError(fmt.Errorf("something went wrong"))
}
rule.SendAlerts(ctx, ts, 0, time.Duration(1*time.Minute), opts.NotifyFunc)
return alertsFound, nil
}
// newTask returns an appropriate group for
// rule type
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, ruleDB baserules.RuleDB) baserules.Task {

View File

@@ -40,9 +40,9 @@
"@monaco-editor/react": "^4.3.1",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "7.102.1",
"@sentry/webpack-plugin": "2.16.0",
"@signozhq/design-tokens": "0.0.8",
"@sentry/react": "8.41.0",
"@sentry/webpack-plugin": "2.22.6",
"@signozhq/design-tokens": "1.1.4",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/shape": "3.5.0",
@@ -76,7 +76,7 @@
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"html-webpack-plugin": "5.5.0",
"http-proxy-middleware": "2.0.7",
"http-proxy-middleware": "3.0.3",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
@@ -87,6 +87,8 @@
"lodash-es": "^4.17.21",
"lucide-react": "0.379.0",
"mini-css-extract-plugin": "2.4.5",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.160.3",
"rc-tween-one": "3.0.6",
@@ -107,11 +109,10 @@
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"overlayscrollbars-react": "^0.5.6",
"overlayscrollbars": "^2.8.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
@@ -127,7 +128,7 @@
"uuid": "^8.3.2",
"web-vitals": "^0.2.4",
"webpack": "5.94.0",
"webpack-dev-server": "^4.15.1",
"webpack-dev-server": "^4.15.2",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0"
},
@@ -240,6 +241,7 @@
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3"
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.3"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none"><path fill="#A65F3E" d="M8.04 10.331a.41.41 0 0 1-.414-.414.4.4 0 0 1 .121-.292l8.071-8.071a.414.414 0 1 1 .585.585l-8.07 8.071a.4.4 0 0 1-.293.121"/><path fill="#A65F3E" d="M16.11 1.5c.09 0 .178.034.245.101a.35.35 0 0 1 0 .492l-8.07 8.07a.346.346 0 0 1-.49 0 .35.35 0 0 1 0-.49l8.07-8.072a.35.35 0 0 1 .245-.101m0-.133a.48.48 0 0 0-.338.14L7.7 9.578a.47.47 0 0 0-.14.34.475.475 0 0 0 .478.478c.13 0 .25-.05.34-.14l8.07-8.071a.48.48 0 0 0-.339-.818"/><path fill="#FFE082" d="m1.701 12.438 3.89 3.889c.873-.963 1.62-2.057 2.023-3.313.03-.091.034-.24.128-.359.451-.566 1.865-2.008.706-3.167-1.106-1.106-2.438.227-2.994.686-.17.14-.384.228-.606.276-1.493.326-3.034 1.869-3.147 1.988"/><path fill="#FFE082" d="M8.385 8.577a.62.62 0 0 1 .393-.085c.098.018.237.135.38.28.144.143.28.304.32.408s-.005.242-.005.242c-.116.23-.383.69-.6.624-.24-.074-.482-.305-.66-.479a1.5 1.5 0 0 1-.276-.328c-.096-.177.008-.324.129-.447.086-.082.232-.17.319-.215"/><path fill="#F9C248" d="M8.327 8.975c.116.11.21.243.339.338.252.185.455.097.62-.052.049-.044.122-.1.17-.055a.1.1 0 0 1 .025.051.45.45 0 0 1-.045.273 1.3 1.3 0 0 1-.433.529c-.032.022-.07.044-.11.032a.12.12 0 0 1-.056-.045c-.207-.244-.37-.533-.626-.724-.103-.076-.364-.132-.298-.303.1-.262.317-.137.414-.044"/><path fill="#F9C248" d="M7.614 13.014c.028-.091.033-.24.127-.359.515-.645 1.223-1.38 1.145-2.275-.01-.123-.169-.75-.342-.514-.04.052-.024.315-.03.379-.1 1.172-1.02 1.821-1.19 2.024s-.164.393-.31.695a5 5 0 0 1-.61.947c-.379.47-.825.88-1.286 1.27a.8.8 0 0 0-.203.217c-.131.241.153.406.305.558l.369.368c.873-.961 1.62-2.055 2.025-3.31"/><path fill="#E2A610" d="M5.537 15.809c-.1-.157-.242-.3-.317-.458a.24.24 0 0 1-.03-.123c.01-.08.13-.15.187-.198q.129-.108.254-.22c.162-.149.314-.314.419-.509.017-.031.032-.07.016-.102-.035-.065-.238.152-.275.186-.105.092-.208.187-.318.272-.146.113-.422.304-.618.213-.1-.046-.19-.169-.263-.249-.084-.094-.164-.191-.252-.283a17 17 0 0 0-.592-.582c-.05-.046-.06-.066-.003-.122a10 10 0 0 0 .546-.58c.022-.025.044-.067.017-.09-.018-.015-.048-.004-.07.007-.26.138-.467.354-.692.544-.055.046-.214-.13-.249-.158-.092-.073-.154-.102-.046-.21.484-.49.972-.946 1.554-1.323.107-.07.22-.14.28-.253-.01-.03-.054-.026-.085-.015-.807.29-1.89 1.291-1.983 1.38-.162.158-.454-.206-.885-.481-.147-.094 0-.235.038-.279.26-.307.603-.642.603-.642-.127.013-.956.76-1.054.873-.084.097-.17.184-.175.318a.52.52 0 0 0 .107.325c.77 1.05 2.586 2.794 3.23 3.253.384.274.502.224.659.068a.35.35 0 0 0 .105-.3.65.65 0 0 0-.108-.263"/><path fill="#A65F3E" d="M8.835 10.176s-.438-.825-1.017-1.074c0 0-.02-.054.02-.1.052-.057.12-.07.157-.046.427.265.812.619 1.007 1.102.039.093-.131.23-.167.118"/><path fill="#F44336" d="M7.64 12.88c-.528-.818-1.63-1.937-2.46-2.524-.066-.046.204-.204.272-.156a9.7 9.7 0 0 1 2.31 2.398c.045.067-.091.33-.123.282M8.193 12.078c-.506-.71-1.521-1.738-2.238-2.312-.062-.05.182-.22.232-.181.755.602 1.668 1.499 2.18 2.263.037.057-.138.282-.174.23"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" fill="none"><path fill="#616161" fill-rule="evenodd" d="M8.096 2.885H4.372V2.51h3.724zM8.096 4.79H4.372v-.375h3.724z" clip-rule="evenodd"/><path fill="#9E9E9E" d="M7.098 15.539H5.662V.936s.134-.311.719-.311.719.311.719.311v14.603z"/><path fill="#757575" d="M6.73.671V12.47H5.662v1.074c.181.001.345.023.493.055.336.074.576.37.576.714v1.227H7.1V.936c-.002 0-.08-.179-.37-.265"/><path fill="#2196F3" d="M10.58.54a3.03 3.03 0 0 0-3.028 3.038 3.02 3.02 0 0 0 3.027 3.028 3.025 3.025 0 0 0 3.028-3.028A3.035 3.035 0 0 0 10.579.54"/><path fill="#fff" d="M11.902 1.671c-.19-.048-.569-.098-1.321-.098-.753 0-1.132.05-1.322.098-.112.029-.488.185-.488.606v2.598c0 .142.115.258.258.258h.095v.288c0 .084.068.151.152.151h.306a.15.15 0 0 0 .151-.151v-.288h1.693v.288c0 .084.067.151.15.151h.307a.15.15 0 0 0 .151-.151v-.288h.098a.26.26 0 0 0 .259-.258V2.277c0-.404-.377-.579-.49-.606m-2.139.206c0-.064.051-.115.115-.115h1.403c.063 0 .115.051.115.115v.204a.115.115 0 0 1-.115.115H9.878a.115.115 0 0 1-.115-.115zm.024 2.736a.08.08 0 0 1-.078.078h-.308a.264.264 0 0 1-.264-.264v-.139c0-.042.035-.077.077-.077h.31c.144 0 .263.117.263.264zm2.235-.186a.264.264 0 0 1-.264.264h-.309a.08.08 0 0 1-.077-.078v-.138c0-.145.117-.264.264-.264h.308c.043 0 .078.035.078.077zm.07-1.129c0 .168-.363.46-1.513.46s-1.512-.27-1.512-.46v-.767c0-.05.05-.175.175-.175h2.695c.125 0 .155.126.155.175z"/><path fill="#F5F5F5" d="M8.61 12.867H4.15a.285.285 0 0 1-.285-.285v-5.15c0-.158.127-.285.285-.285h4.457c.158 0 .285.127.285.285v5.15a.285.285 0 0 1-.284.285"/><path fill="#82AEC0" d="M8.128 12.015H4.632l-.01-4.07H8.12z" opacity=".8"/><path fill="#F5F5F5" fill-rule="evenodd" d="M6.246 12.07V7.945h.25v4.123z" clip-rule="evenodd"/><path fill="#616161" d="M6.246 7.946H4.622v.34h1.624z"/><path fill="#F5F5F5" fill-rule="evenodd" d="M8.142 11.307H4.618v-.125h3.524zM8.142 10.482H4.618v-.125h3.524zM8.12 9.657H4.621v-.125H8.12zM8.12 8.833H4.617v-.125H8.12z" clip-rule="evenodd"/><path fill="#616161" d="M8.118 9.426H6.495v.34h1.623zM6.253 10.25H4.635v.34h1.618z"/><path fill="#9E9E9E" fill-rule="evenodd" d="M4.15 7.334a.097.097 0 0 0-.097.098v5.15c0 .054.044.097.098.097h4.458a.097.097 0 0 0 .097-.097v-5.15a.097.097 0 0 0-.098-.098zm-.472.098c0-.261.212-.473.473-.473h4.457c.261 0 .473.212.473.473v5.15c0 .26-.211.472-.472.472H4.151a.47.47 0 0 1-.473-.472z" clip-rule="evenodd"/><path fill="#757575" d="M4.17 12.682c-.194.017-.194-.11-.194-.145V7.493c0-.141.115-.256.256-.256H8.56c.118 0 .172.071.148.216 0 0-.015-.092-.128-.092H4.233a.133.133 0 0 0-.132.132v5.045c0 .117.069.144.069.144"/><path fill="#FFCA28" d="M4.9 1.775H.642v3.7h4.26z"/><path fill="#9E9E9E" d="M4.663 1.775c.132 0 .238.106.238.237v3.225a.237.237 0 0 1-.238.237H.88a.237.237 0 0 1-.238-.237V2.012c0-.131.107-.237.238-.237zm0-.25H.88a.49.49 0 0 0-.488.487v3.225c0 .269.22.487.488.487h3.783a.49.49 0 0 0 .488-.487V2.012a.487.487 0 0 0-.488-.487"/><path fill="#FFFDE7" fill-rule="evenodd" d="M4.902 3.11H.642v-.25h4.26zM4.902 4.388H.642v-.25h4.26z" clip-rule="evenodd"/><path fill="#757575" d="M1.975 2.186H.904v.282h1.07zM1.711 4.777H.904v.283h.807zM4.552 4.777h-.807v.283h.807zM4.552 2.186h-.807v.282h.807zM3.795 3.482H3.33v.282h.465zM4.552 3.482h-.465v.282h.465zM2.388 3.482H.904v.282h1.484z"/></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 408 KiB

View File

@@ -0,0 +1,12 @@
{
"workspaceSuspended": "Your workspace is locked",
"gotQuestions": "Got Questions?",
"contactUs": "Contact Us",
"actionHeader": "Pay to continue",
"actionDescription": "Pay now to keep enjoying all the great features youve been using.",
"yourDataIsSafe": "Your data is safe with us until",
"actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Settle your bill to continue",
"somethingWentWrong": "Something went wrong"
}

View File

@@ -0,0 +1,8 @@
{
"containers_visualization_message": "The ability to visualise containers is in active development and should be available to you soon.",
"processes_visualization_message": "The ability to visualise processes is in active development and should be available to you soon.",
"working_message": "We're working to extend infrastructure monitoring to take care of a bunch of different cases. Thank you for your patience.",
"waitlist_message": "Join the waitlist for early access.",
"waitlist_success_message": "We have received your request for early access. We will get back to you as soon as we launch the feature.",
"contact_support": "Contact Support"
}

View File

@@ -0,0 +1,24 @@
{
"metricGraphCategory": {
"brokerMetrics": {
"title": "Broker Metrics",
"description": "The Kafka Broker metrics here inform you of data loss/delay through unclean leader elections and network throughputs, as well as request fails through request purgatories and timeouts metrics"
},
"consumerMetrics": {
"title": "Consumer Metrics",
"description": "Kafka Consumer metrics provide insights into lag between message production and consumption, success rates and latency of message delivery, and the volume of data consumed."
},
"producerMetrics": {
"title": "Producer Metrics",
"description": "Kafka Producers send messages to brokers for storage and distribution by topic. These metrics inform you of the volume and rate of data sent, and the success rate of message delivery."
},
"brokerJVMMetrics": {
"title": "Broker JVM Metrics",
"description": "Kafka brokers are Java applications that expose JVM metrics to inform on the broker's system health. Garbage collection metrics like those below provide key insights into free memory, broker performance, and heap size. You need to enable new_gc_metrics for this section to populate."
},
"partitionMetrics": {
"title": "Partition Metrics",
"description": "Kafka partitions are the unit of parallelism in Kafka. These metrics inform you of the number of partitions per topic, the current offset of each partition, the oldest offset, and the number of in-sync replicas."
}
}
}

View File

@@ -1,30 +1,54 @@
{
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details"
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
}
}
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details",
"consumer": {
"title": "Consumer lag view",
"description": "Connect and Monitor Your Data Streams"
},
"producer": {
"title": "Producer latency view",
"description": "Connect and Monitor Your Data Streams"
},
"partition": {
"title": "Partition Latency view",
"description": "Connect and Monitor Your Data Streams"
},
"dropRate": {
"title": "Drop Rate view",
"description": "Connect and Monitor Your Data Streams"
},
"metricPage": {
"title": "Metric View",
"description": "Connect and Monitor Your Data Streams"
}
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
},
"overviewSummarySection": {
"title": "Monitor Your Data Streams",
"subtitle": "Monitor key Kafka metrics like consumer lag and latency to ensure efficient data flow and troubleshoot in real time."
}
}

View File

@@ -37,8 +37,10 @@
"PASSWORD_RESET": "SigNoz | Password Reset",
"LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"WORKSPACE_SUSPENDED": "SigNoz | Workspace Suspended",
"SUPPORT": "SigNoz | Support",
"DEFAULT": "Open source Observability Platform | SigNoz",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview"
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
}

View File

@@ -7,5 +7,5 @@
"save": "Save",
"edit": "Edit",
"logged_in": "Logged In",
"pending_data_placeholder": "Just a bit of patience, just a little bits enough ⎯ were getting your {{dataSource}}!"
"pending_data_placeholder": "Retrieving your {{dataSource}}!"
}

View File

@@ -0,0 +1,12 @@
{
"workspaceSuspended": "Your workspace is locked",
"gotQuestions": "Got Questions?",
"contactUs": "Contact Us",
"actionHeader": "Pay to continue",
"actionDescription": "Pay now to keep enjoying all the great features youve been using.",
"yourDataIsSafe": "Your data is safe with us until",
"actNow": "Act now to avoid any disruptions and continue where you left off.",
"contactAdmin": "Contact your admin to proceed with the upgrade.",
"continueMyJourney": "Settle your bill to continue",
"somethingWentWrong": "Something went wrong"
}

View File

@@ -0,0 +1,8 @@
{
"containers_visualization_message": "The ability to visualise containers is in active development and should be available to you soon.",
"processes_visualization_message": "The ability to visualise processes is in active development and should be available to you soon.",
"working_message": "We're working to extend infrastructure monitoring to take care of a bunch of different cases. Thank you for your patience.",
"waitlist_message": "Join the waitlist for early access.",
"waitlist_success_message": "We have received your request for early access. We will get back to you as soon as we launch the feature.",
"contact_support": "Contact Support"
}

View File

@@ -0,0 +1,24 @@
{
"metricGraphCategory": {
"brokerMetrics": {
"title": "Broker Metrics",
"description": "The Kafka Broker metrics here inform you of data loss/delay through unclean leader elections and network throughputs, as well as request fails through request purgatories and timeouts metrics"
},
"consumerMetrics": {
"title": "Consumer Metrics",
"description": "Kafka Consumer metrics provide insights into lag between message production and consumption, success rates and latency of message delivery, and the volume of data consumed."
},
"producerMetrics": {
"title": "Producer Metrics",
"description": "Kafka Producers send messages to brokers for storage and distribution by topic. These metrics inform you of the volume and rate of data sent, and the success rate of message delivery."
},
"brokerJVMMetrics": {
"title": "Broker JVM Metrics",
"description": "Kafka brokers are Java applications that expose JVM metrics to inform on the broker's system health. Garbage collection metrics like those below provide key insights into free memory, broker performance, and heap size. You need to enable new_gc_metrics for this section to populate."
},
"partitionMetrics": {
"title": "Partition Metrics",
"description": "Kafka partitions are the unit of parallelism in Kafka. These metrics inform you of the number of partitions per topic, the current offset of each partition, the oldest offset, and the number of in-sync replicas."
}
}
}

View File

@@ -1,30 +1,54 @@
{
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details"
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
}
}
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details",
"consumer": {
"title": "Consumer lag view",
"description": "Connect and Monitor Your Data Streams"
},
"producer": {
"title": "Producer latency view",
"description": "Connect and Monitor Your Data Streams"
},
"partition": {
"title": "Partition Latency view",
"description": "Connect and Monitor Your Data Streams"
},
"dropRate": {
"title": "Drop Rate view",
"description": "Connect and Monitor Your Data Streams"
},
"metricPage": {
"title": "Metric View",
"description": "Connect and Monitor Your Data Streams"
}
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
},
"overviewSummarySection": {
"title": "Monitor Your Data Streams",
"subtitle": "Monitor key Kafka metrics like consumer lag and latency to ensure efficient data flow and troubleshoot in real time."
}
}

View File

@@ -45,6 +45,7 @@
"PASSWORD_RESET": "SigNoz | Password Reset",
"LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"WORKSPACE_SUSPENDED": "SigNoz | Workspace Suspended",
"SUPPORT": "SigNoz | Support",
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
@@ -53,5 +54,6 @@
"INTEGRATIONS": "SigNoz | Integrations",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues"
"MESSAGING_QUEUES": "SigNoz | Messaging Queues",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
}

View File

@@ -9,7 +9,8 @@ import ROUTES from 'constants/routes';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { isEmpty } from 'lodash-es';
import { isEmpty, isNull } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { ReactChild, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
@@ -20,8 +21,10 @@ import { AppState } from 'store/reducers';
import { getInitialUserTokenRefreshToken } from 'store/utils';
import AppActions from 'types/actions';
import { UPDATE_USER_IS_FETCH } from 'types/actions/app';
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
import { Organization } from 'types/api/user/getOrganization';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import { routePermission } from 'utils/permission';
import routes, {
@@ -35,13 +38,20 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const location = useLocation();
const { pathname } = location;
const { org, orgPreferences } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isOnboardingComplete, setIsOnboardingComplete] = useState<
boolean | null
>(null);
const {
org,
orgPreferences,
user,
role,
isUserFetching,
isUserFetchingError,
isLoggedIn: isLoggedInState,
isFetchingOrgPreferences,
} = useSelector<AppState, AppReducer>((state) => state.app);
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
const mapRoutes = useMemo(
() =>
@@ -56,32 +66,23 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
[pathname],
);
useEffect(() => {
if (orgPreferences && !isEmpty(orgPreferences)) {
const onboardingPreference = orgPreferences?.find(
const isOnboardingComplete = useMemo(
() =>
orgPreferences?.find(
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
);
if (onboardingPreference) {
setIsOnboardingComplete(onboardingPreference.value);
}
}
}, [orgPreferences]);
)?.value,
[orgPreferences],
);
const {
data: licensesData,
isFetching: isFetchingLicensesData,
} = useLicense();
const {
isUserFetching,
isUserFetchingError,
isLoggedIn: isLoggedInState,
isFetchingOrgPreferences,
} = useSelector<AppState, AppReducer>((state) => state.app);
const { t } = useTranslation(['common']);
const isCloudUserVal = isCloudUser();
const localStorageUserAuthToken = getInitialUserTokenRefreshToken();
const dispatch = useDispatch<Dispatch<AppActions>>();
@@ -135,7 +136,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
// Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load
const shouldShowOnboarding = (): boolean => {
// Only run this effect if the org users and preferences are loaded
if (!isLoadingOrgUsers) {
if (!isLoadingOrgUsers && !isFetchingOrgPreferences) {
const isFirstUser = checkFirstTimeUser();
// Redirect to get started if it's not the first user or if the onboarding is complete
@@ -145,6 +147,31 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
return false;
};
const handleRedirectForOrgOnboarding = (key: string): void => {
if (
isLoggedInState &&
isCloudUserVal &&
!isFetchingOrgPreferences &&
!isLoadingOrgUsers &&
!isEmpty(orgUsers?.payload) &&
!isNull(orgPreferences)
) {
if (key === 'ONBOARDING' && isOnboardingComplete) {
history.push(ROUTES.APPLICATION);
}
const isFirstTimeUser = checkFirstTimeUser();
if (isFirstTimeUser && !isOnboardingComplete) {
history.push(ROUTES.ONBOARDING);
}
}
if (!isCloudUserVal && key === 'ONBOARDING') {
history.push(ROUTES.APPLICATION);
}
};
const handleUserLoginIfTokenPresent = async (
key: keyof typeof ROUTES,
): Promise<void> => {
@@ -166,15 +193,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
response.payload.refreshJwt,
);
const showOnboarding = shouldShowOnboarding();
if (
userResponse &&
showOnboarding &&
userResponse.payload.role === 'ADMIN'
) {
history.push(ROUTES.ONBOARDING);
}
handleRedirectForOrgOnboarding(key);
if (
userResponse &&
@@ -203,7 +222,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
) {
handleUserLoginIfTokenPresent(key);
} else {
// user does have localstorage values
handleRedirectForOrgOnboarding(key);
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
}
@@ -234,6 +253,33 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
}
}, [isFetchingLicensesData]);
const navigateToWorkSpaceSuspended = (route: any): void => {
const { path } = route;
if (path && path !== ROUTES.WORKSPACE_SUSPENDED) {
history.push(ROUTES.WORKSPACE_SUSPENDED);
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
}
};
useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
const shouldSuspendWorkspace =
activeLicenseV3.status === LicenseStatus.SUSPENDED &&
activeLicenseV3.state === LicenseState.PAYMENT_FAILED;
if (shouldSuspendWorkspace) {
navigateToWorkSpaceSuspended(currentRoute);
}
}
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
useEffect(() => {
if (org && org.length > 0 && org[0].id !== undefined) {
setOrgData(org[0]);
@@ -241,9 +287,9 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
}, [org]);
const handleRouting = (): void => {
const showOnboarding = shouldShowOnboarding();
const showOrgOnboarding = shouldShowOnboarding();
if (showOnboarding) {
if (showOrgOnboarding && !isOnboardingComplete && isCloudUserVal) {
history.push(ROUTES.ONBOARDING);
} else {
history.push(ROUTES.APPLICATION);
@@ -251,17 +297,27 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
};
useEffect(() => {
// Only run this effect if the org users and preferences are loaded
if (!isLoadingOrgUsers && isOnboardingComplete !== null) {
const isFirstUser = checkFirstTimeUser();
const { isPrivate } = currentRoute || {
isPrivate: false,
};
// Redirect to get started if it's not the first user or if the onboarding is complete
if (isFirstUser && !isOnboardingComplete) {
history.push(ROUTES.ONBOARDING);
}
if (isLoggedInState && role && role !== 'ADMIN') {
setIsLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoadingOrgUsers, isOnboardingComplete, orgUsers]);
if (!isPrivate) {
setIsLoading(false);
}
if (
!isEmpty(user) &&
!isFetchingOrgPreferences &&
!isEmpty(orgUsers?.payload) &&
!isNull(orgPreferences)
) {
setIsLoading(false);
}
}, [currentRoute, user, role, orgUsers, orgPreferences]);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
@@ -284,7 +340,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
handlePrivateRoutes(key);
} else {
// no need to fetch the user and make user fetching false
if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') {
handleRouting();
}
@@ -311,13 +366,20 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
history.push(ROUTES.SOMETHING_WENT_WRONG);
}
})();
}, [dispatch, isLoggedInState, currentRoute, licensesData]);
}, [
dispatch,
isLoggedInState,
currentRoute,
licensesData,
orgUsers,
orgPreferences,
]);
if (isUserFetchingError) {
return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />;
}
if (isUserFetching || (isLoggedInState && isFetchingOrgPreferences)) {
if (isUserFetching || isLoading) {
return <Spinner tip="Loading..." />;
}

View File

@@ -22,12 +22,14 @@ import history from 'lib/history';
import { identity, pick, pickBy } from 'lodash-es';
import posthog from 'posthog-js';
import AlertRuleProvider from 'providers/Alert';
import { AppProvider } from 'providers/App/App';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
@@ -290,40 +292,44 @@ function App(): JSX.Element {
}, []);
return (
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<NotificationProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<AppProvider>
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<NotificationProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</NotificationProvider>
</Router>
</ConfigProvider>
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</NotificationProvider>
</CompatRouter>
</Router>
</ConfigProvider>
</AppProvider>
);
}

View File

@@ -206,6 +206,13 @@ export const WorkspaceBlocked = Loadable(
import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'),
);
export const WorkspaceSuspended = Loadable(
() =>
import(
/* webpackChunkName: "WorkspaceSuspended" */ 'pages/WorkspaceSuspended/WorkspaceSuspended'
),
);
export const ShortcutsPage = Loadable(
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
);
@@ -228,3 +235,10 @@ export const MQDetailPage = Loadable(
/* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage'
),
);
export const InfrastructureMonitoring = Loadable(
() =>
import(
/* webpackChunkName: "InfrastructureMonitoring" */ 'pages/InfrastructureMonitoring'
),
);

View File

@@ -15,6 +15,7 @@ import {
EditAlertChannelsAlerts,
EditRulesPage,
ErrorDetails,
InfrastructureMonitoring,
IngestionSettings,
InstalledIntegrations,
LicensePage,
@@ -52,6 +53,7 @@ import {
UnAuthorized,
UsageExplorerPage,
WorkspaceBlocked,
WorkspaceSuspended,
} from './pageComponents';
const routes: AppRoutes[] = [
@@ -363,6 +365,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'WORKSPACE_LOCKED',
},
{
path: ROUTES.WORKSPACE_SUSPENDED,
exact: true,
component: WorkspaceSuspended,
isPrivate: true,
key: 'WORKSPACE_SUSPENDED',
},
{
path: ROUTES.SHORTCUTS,
exact: true,
@@ -391,6 +400,13 @@ const routes: AppRoutes[] = [
key: 'MESSAGING_QUEUES_DETAIL',
isPrivate: true,
},
{
path: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
exact: true,
component: InfrastructureMonitoring,
key: 'INFRASTRUCTURE_MONITORING_HOSTS',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -0,0 +1,37 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios';
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
BaseAutocompleteData,
IQueryAutocompleteResponse,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
export const getHostAttributeKeys = async (
searchText = '',
): Promise<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse> => {
try {
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await ApiBaseInstance.get(
`/hosts/attribute_keys?dataSource=metrics&searchText=${searchText}`,
);
const payload: BaseAutocompleteData[] =
response.data.data.attributeKeys?.map(({ id: _, ...item }) => ({
...item,
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
})) || [];
return {
statusCode: 200,
error: null,
message: response.statusText,
payload: { attributeKeys: payload },
};
} catch (e) {
return ErrorResponseHandler(e as AxiosError);
}
};

View File

@@ -0,0 +1,77 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface HostListPayload {
filters: TagFilter;
groupBy: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface TimeSeriesValue {
timestamp: number;
value: string;
}
export interface TimeSeries {
labels: Record<string, string>;
labelsArray: Array<Record<string, string>>;
values: TimeSeriesValue[];
}
export interface HostData {
hostName: string;
active: boolean;
os: string;
cpu: number;
cpuTimeSeries: TimeSeries;
memory: number;
memoryTimeSeries: TimeSeries;
wait: number;
waitTimeSeries: TimeSeries;
load15: number;
load15TimeSeries: TimeSeries;
}
export interface HostListResponse {
status: string;
data: {
type: string;
records: HostData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getHostLists = async (
props: HostListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<HostListResponse> | ErrorResponse> => {
try {
const response = await ApiBaseInstance.post('/hosts/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,38 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IAttributeValuesResponse,
IGetAttributeValuesPayload,
} from 'types/api/queryBuilder/getAttributesValues';
export const getInfraAttributesValues = async ({
dataSource,
attributeKey,
filterAttributeKeyDataType,
tagType,
searchText,
}: IGetAttributeValuesPayload): Promise<
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
> => {
try {
const response = await ApiBaseInstance.get(
`/hosts/attribute_values?${createQueryParams({
dataSource,
attributeKey,
searchText,
})}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,18 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { LicenseV3EventQueueResModel } from 'types/api/licensesV3/getActive';
const getActive = async (): Promise<
SuccessResponse<LicenseV3EventQueueResModel> | ErrorResponse
> => {
const response = await axios.get('/licenses/active');
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default getActive;

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import {
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from 'pages/MessagingQueues/MQDetails/MQTables/getConsumerLagDetails';
import { ErrorResponse, SuccessResponse } from 'types/api';
export const getTopicThroughputOverview = async (
props: Omit<MessagingQueueServicePayload, 'variables'>,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, start, end } = props;
const response = await axios.post(
`messaging-queues/kafka/topic-throughput/${detailType}`,
{
start,
end,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};

View File

@@ -0,0 +1,39 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface OnboardingStatusResponse {
status: string;
data: {
attribute?: string;
error_message?: string;
status?: string;
}[];
}
const getOnboardingStatus = async (props: {
start: number;
end: number;
endpointService?: string;
}): Promise<SuccessResponse<OnboardingStatusResponse> | ErrorResponse> => {
const { endpointService, ...rest } = props;
try {
const response = await ApiBaseInstance.post(
`/messaging-queues/kafka/onboarding/${endpointService || 'consumers'}`,
rest,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};
export default getOnboardingStatus;

View File

@@ -5,7 +5,6 @@ import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
// ** Types
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
import {
BaseAutocompleteData,

View File

@@ -40,7 +40,7 @@
&.custom-time {
input:not(:focus) {
min-width: 240px;
min-width: 280px;
}
}
@@ -119,3 +119,69 @@
color: var(--bg-slate-400) !important;
}
}
.date-time-popover__footer {
border-top: 1px solid var(--bg-ink-200);
padding: 8px 14px;
.timezone-container {
&,
.timezone {
font-family: Inter;
font-size: 12px;
line-height: 16px;
letter-spacing: -0.06px;
}
display: flex;
align-items: center;
color: var(--bg-vanilla-400);
gap: 6px;
.timezone {
display: flex;
align-items: center;
gap: 4px;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
cursor: pointer;
padding: 0px 4px;
color: var(--bg-vanilla-100);
border: none;
}
}
}
.timezone-badge {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
cursor: pointer;
}
.lightMode {
.date-time-popover__footer {
border-color: var(--bg-vanilla-400);
}
.timezone-container {
color: var(--bg-ink-400);
&__clock-icon {
stroke: var(--bg-ink-400);
}
.timezone {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
&__icon {
stroke: var(--bg-ink-100);
}
}
}
.timezone-badge {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
}
}

View File

@@ -15,11 +15,14 @@ import { isValidTimeFormat } from 'lib/getMinMax';
import { defaultTo, isFunction, noop } from 'lodash-es';
import debounce from 'lodash-es/debounce';
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
ChangeEvent,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
@@ -28,6 +31,8 @@ import { popupContainer } from 'utils/selectPopupContainer';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
const maxAllowedMinTimeInMonths = 6;
type ViewType = 'datetime' | 'timezone';
const DEFAULT_VIEW: ViewType = 'datetime';
interface CustomTimePickerProps {
onSelect: (value: string) => void;
@@ -81,11 +86,42 @@ function CustomTimePicker({
const location = useLocation();
const [isInputFocused, setIsInputFocused] = useState(false);
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
const { timezone, browserTimezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
const isTimezoneOverridden = useMemo(
() => timezone.offset !== browserTimezone.offset,
[timezone, browserTimezone],
);
const handleViewChange = useCallback(
(newView: 'timezone' | 'datetime'): void => {
if (activeView !== newView) {
setActiveView(newView);
}
setOpen(true);
},
[activeView, setOpen],
);
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
const getSelectedTimeRangeLabel = (
selectedTime: string,
selectedTimeValue: string,
): string => {
if (selectedTime === 'custom') {
// Convert the date range string to 12-hour format
const dates = selectedTimeValue.split(' - ');
if (dates.length === 2) {
const startDate = dayjs(dates[0], 'DD/MM/YYYY HH:mm');
const endDate = dayjs(dates[1], 'DD/MM/YYYY HH:mm');
return `${startDate.format('DD/MM/YYYY hh:mm A')} - ${endDate.format(
'DD/MM/YYYY hh:mm A',
)}`;
}
return selectedTimeValue;
}
@@ -120,7 +156,6 @@ function CustomTimePicker({
useEffect(() => {
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
setSelectedTimePlaceholderValue(value);
}, [selectedTime, selectedValue]);
@@ -132,6 +167,7 @@ function CustomTimePicker({
setOpen(newOpen);
if (!newOpen) {
setCustomDTPickerVisible?.(false);
setActiveView('datetime');
}
};
@@ -245,6 +281,7 @@ function CustomTimePicker({
const handleFocus = (): void => {
setIsInputFocused(true);
setActiveView('datetime');
};
const handleBlur = (): void => {
@@ -281,6 +318,10 @@ function CustomTimePicker({
handleGoLive={defaultTo(handleGoLive, noop)}
options={items}
selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
/>
) : (
content
@@ -317,12 +358,24 @@ function CustomTimePicker({
)
}
suffix={
<ChevronDown
size={14}
onClick={(): void => {
setOpen(!open);
}}
/>
<>
{!!isTimezoneOverridden && activeTimezoneOffset && (
<div
className="timezone-badge"
onClick={(e): void => {
e.stopPropagation();
handleViewChange('timezone');
setIsOpenedFromFooter(false);
}}
>
<span>{activeTimezoneOffset}</span>
</div>
)}
<ChevronDown
size={14}
onClick={(): void => handleViewChange('datetime')}
/>
</>
}
/>
</Popover>

View File

@@ -1,5 +1,6 @@
import './CustomTimePicker.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import cx from 'classnames';
import ROUTES from 'constants/routes';
@@ -9,10 +10,13 @@ import {
Option,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { Clock, PenLine } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import RangePickerModal from './RangePickerModal';
import TimezonePicker from './TimezonePicker';
interface CustomTimePickerPopoverContentProps {
options: any[];
@@ -26,8 +30,13 @@ interface CustomTimePickerPopoverContentProps {
onSelectHandler: (label: string, value: string) => void;
handleGoLive: () => void;
selectedTime: string;
activeView: 'datetime' | 'timezone';
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function CustomTimePickerPopoverContent({
options,
setIsOpen,
@@ -37,12 +46,18 @@ function CustomTimePickerPopoverContent({
onSelectHandler,
handleGoLive,
selectedTime,
activeView,
setActiveView,
isOpenedFromFooter,
setIsOpenedFromFooter,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const { timezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
function getTimeChips(options: Option[]): JSX.Element {
return (
@@ -63,55 +78,99 @@ function CustomTimePickerPopoverContent({
);
}
const handleTimezoneHintClick = (): void => {
setActiveView('timezone');
setIsOpenedFromFooter(true);
};
if (activeView === 'timezone') {
return (
<div className="date-time-popover">
<TimezonePicker
setActiveView={setActiveView}
setIsOpen={setIsOpen}
isOpenedFromFooter={isOpenedFromFooter}
/>
</div>
);
}
return (
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
<>
<div className="date-time-popover">
<div className="date-time-options">
{isLogsExplorerPage && (
<Button className="data-time-live" type="text" onClick={handleGoLive}>
Live
</Button>
)}
{options.map((option) => (
<Button
type="text"
key={option.label + option.value}
onClick={(): void => {
onSelectHandler(option.label, option.value);
}}
className={cx(
'date-time-options-btn',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
</div>
</div>
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
<div className="date-time-popover__footer">
<div className="timezone-container">
<Clock
color={Color.BG_VANILLA_400}
className="timezone-container__clock-icon"
height={12}
width={12}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
<span className="timezone__icon">Current timezone</span>
<div></div>
<button
type="button"
className="timezone"
onClick={handleTimezoneHintClick}
>
<span>{activeTimezoneOffset}</span>
<PenLine
color={Color.BG_VANILLA_100}
className="timezone__icon"
size={10}
/>
</button>
</div>
</div>
</div>
</>
);
}

View File

@@ -3,7 +3,8 @@ import './RangePickerModal.styles.scss';
import { DatePicker } from 'antd';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -31,7 +32,10 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
(state) => state.globalTime,
);
const disabledDate = (current: Dayjs): boolean => {
// Using any type here because antd's DatePicker expects its own internal Dayjs type
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
const disabledDate = (current: any): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
@@ -49,16 +53,22 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
const { timezone } = useTimezone();
return (
<div className="custom-date-picker">
<RangePicker
disabledDate={disabledDate}
allowClear
showTime
format="YYYY-MM-DD hh:mm A"
onOk={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
defaultValue: [
dayjs(minTime / 1000000).tz(timezone.value),
dayjs(maxTime / 1000000).tz(timezone.value),
],
})}
/>
</div>

View File

@@ -0,0 +1,166 @@
// Variables
$font-family: 'Inter';
$item-spacing: 8px;
:root {
--border-color: var(--bg-slate-400);
}
.lightMode {
--border-color: var(--bg-vanilla-400);
}
// Mixins
@mixin text-style-base {
font-family: $font-family;
font-style: normal;
font-weight: 400;
}
@mixin flex-center {
display: flex;
align-items: center;
}
.timezone-picker {
width: 532px;
color: var(--bg-vanilla-400);
font-family: $font-family;
&__search {
@include flex-center;
justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid var(--border-color);
}
&__input-container {
@include flex-center;
gap: 6px;
width: -webkit-fill-available;
}
&__input {
@include text-style-base;
width: 100%;
background: transparent;
border: none;
outline: none;
color: var(--bg-vanilla-100);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
padding: 0;
&.ant-input:focus {
box-shadow: none;
}
&::placeholder {
color: var(--bg-vanilla-400);
}
}
&__esc-key {
@include text-style-base;
font-size: 8px;
color: var(--bg-vanilla-400);
letter-spacing: -0.04px;
border-radius: 2.286px;
border: 1.143px solid var(--bg-ink-200);
border-bottom-width: 2.286px;
background: var(--bg-ink-400);
padding: 0 1px;
}
&__list {
max-height: 310px;
overflow-y: auto;
}
&__item {
@include flex-center;
justify-content: space-between;
padding: 7.5px 6px 7.5px $item-spacing;
margin: 4px $item-spacing;
cursor: pointer;
background: transparent;
border: none;
width: -webkit-fill-available;
color: var(--bg-vanilla-400);
font-family: $font-family;
&:hover,
&.selected {
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
}
&.has-divider {
position: relative;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: -$item-spacing;
right: -$item-spacing;
border-bottom: 1px solid var(--border-color);
}
}
}
&__name {
@include text-style-base;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
&__offset {
color: var(--bg-vanilla-100);
font-size: 12px;
line-height: 16px;
letter-spacing: -0.06px;
}
}
.timezone-name-wrapper {
@include flex-center;
gap: 6px;
&__selected-icon {
height: 15px;
width: 15px;
}
}
.lightMode {
.timezone-picker {
&__search {
.search-icon {
stroke: var(--bg-ink-400);
}
}
&__input {
color: var(--bg-ink-100);
}
&__esc-key {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-400);
}
&__item {
color: var(--bg-ink-400);
}
&__offset {
color: var(--bg-ink-100);
}
}
.timezone-name-wrapper {
&__selected-icon {
.check-icon {
stroke: var(--bg-ink-100);
}
}
}
}

View File

@@ -0,0 +1,201 @@
import './TimezonePicker.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Input } from 'antd';
import cx from 'classnames';
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Check, Search } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { Timezone, TIMEZONE_DATA } from './timezoneUtils';
interface SearchBarProps {
value: string;
onChange: (value: string) => void;
setIsOpen: Dispatch<SetStateAction<boolean>>;
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
}
interface TimezoneItemProps {
timezone: Timezone;
isSelected?: boolean;
onClick?: () => void;
}
const ICON_SIZE = 14;
function SearchBar({
value,
onChange,
setIsOpen,
setActiveView,
isOpenedFromFooter = false,
}: SearchBarProps): JSX.Element {
const handleKeyDown = useCallback(
(e: React.KeyboardEvent): void => {
if (e.key === 'Escape') {
if (isOpenedFromFooter) {
setActiveView('datetime');
} else {
setIsOpen(false);
}
}
},
[setActiveView, setIsOpen, isOpenedFromFooter],
);
return (
<div className="timezone-picker__search">
<div className="timezone-picker__input-container">
<Search
color={Color.BG_VANILLA_400}
className="search-icon"
height={ICON_SIZE}
width={ICON_SIZE}
/>
<Input
type="text"
className="timezone-picker__input"
placeholder="Search timezones..."
value={value}
onChange={(e): void => onChange(e.target.value)}
onKeyDown={handleKeyDown}
tabIndex={0}
autoFocus
/>
</div>
<kbd className="timezone-picker__esc-key">esc</kbd>
</div>
);
}
function TimezoneItem({
timezone,
isSelected = false,
onClick,
}: TimezoneItemProps): JSX.Element {
return (
<button
type="button"
className={cx('timezone-picker__item', {
selected: isSelected,
'has-divider': timezone.hasDivider,
})}
onClick={onClick}
>
<div className="timezone-name-wrapper">
<div className="timezone-name-wrapper__selected-icon">
{isSelected && (
<Check
className="check-icon"
color={Color.BG_VANILLA_100}
height={ICON_SIZE}
width={ICON_SIZE}
/>
)}
</div>
<div className="timezone-picker__name">{timezone.name}</div>
</div>
<div className="timezone-picker__offset">{timezone.offset}</div>
</button>
);
}
TimezoneItem.defaultProps = {
isSelected: false,
onClick: undefined,
};
interface TimezonePickerProps {
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
isOpenedFromFooter: boolean;
}
function TimezonePicker({
setActiveView,
setIsOpen,
isOpenedFromFooter,
}: TimezonePickerProps): JSX.Element {
console.log({ isOpenedFromFooter });
const [searchTerm, setSearchTerm] = useState('');
const { timezone, updateTimezone } = useTimezone();
const [selectedTimezone, setSelectedTimezone] = useState<string>(
timezone.name ?? TIMEZONE_DATA[0].name,
);
const getFilteredTimezones = useCallback((searchTerm: string): Timezone[] => {
const normalizedSearch = searchTerm.toLowerCase();
return TIMEZONE_DATA.filter(
(tz) =>
tz.name.toLowerCase().includes(normalizedSearch) ||
tz.offset.toLowerCase().includes(normalizedSearch) ||
tz.searchIndex.toLowerCase().includes(normalizedSearch),
);
}, []);
const handleCloseTimezonePicker = useCallback(() => {
if (isOpenedFromFooter) {
setActiveView('datetime');
} else {
setIsOpen(false);
}
}, [isOpenedFromFooter, setActiveView, setIsOpen]);
const handleTimezoneSelect = useCallback(
(timezone: Timezone) => {
setSelectedTimezone(timezone.name);
updateTimezone(timezone);
handleCloseTimezonePicker();
setIsOpen(false);
},
[handleCloseTimezonePicker, setIsOpen, updateTimezone],
);
// Register keyboard shortcuts
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
useEffect(() => {
registerShortcut(
TimezonePickerShortcuts.CloseTimezonePicker,
handleCloseTimezonePicker,
);
return (): void => {
deregisterShortcut(TimezonePickerShortcuts.CloseTimezonePicker);
};
}, [deregisterShortcut, handleCloseTimezonePicker, registerShortcut]);
return (
<div className="timezone-picker">
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
setIsOpen={setIsOpen}
setActiveView={setActiveView}
isOpenedFromFooter={isOpenedFromFooter}
/>
<div className="timezone-picker__list">
{getFilteredTimezones(searchTerm).map((timezone) => (
<TimezoneItem
key={timezone.value}
timezone={timezone}
isSelected={timezone.name === selectedTimezone}
onClick={(): void => handleTimezoneSelect(timezone)}
/>
))}
</div>
</div>
);
}
export default TimezonePicker;

View File

@@ -0,0 +1,152 @@
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
dayjs.extend(timezone);
export interface Timezone {
name: string;
value: string;
offset: string;
searchIndex: string;
hasDivider?: boolean;
}
const TIMEZONE_TYPES = {
BROWSER: 'BROWSER',
UTC: 'UTC',
STANDARD: 'STANDARD',
} as const;
type TimezoneType = typeof TIMEZONE_TYPES[keyof typeof TIMEZONE_TYPES];
export const UTC_TIMEZONE: Timezone = {
name: 'Coordinated Universal Time — UTC, GMT',
value: 'UTC',
offset: 'UTC',
searchIndex: 'UTC',
hasDivider: true,
};
const normalizeTimezoneName = (timezone: string): string => {
// https://github.com/tc39/proposal-temporal/issues/1076
if (timezone === 'Asia/Calcutta') {
return 'Asia/Kolkata';
}
return timezone;
};
const formatOffset = (offsetMinutes: number): string => {
if (offsetMinutes === 0) return 'UTC';
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
const minutes = Math.abs(offsetMinutes) % 60;
const sign = offsetMinutes > 0 ? '+' : '-';
return `UTC ${sign} ${hours}${
minutes ? `:${minutes.toString().padStart(2, '0')}` : ':00'
}`;
};
const createTimezoneEntry = (
name: string,
offsetMinutes: number,
type: TimezoneType = TIMEZONE_TYPES.STANDARD,
hasDivider = false,
): Timezone => {
const offset = formatOffset(offsetMinutes);
let value = name;
let displayName = name;
switch (type) {
case TIMEZONE_TYPES.BROWSER:
displayName = `Browser time — ${name}`;
value = name;
break;
case TIMEZONE_TYPES.UTC:
displayName = 'Coordinated Universal Time — UTC, GMT';
value = 'UTC';
break;
case TIMEZONE_TYPES.STANDARD:
displayName = name;
value = name;
break;
default:
console.error(`Invalid timezone type: ${type}`);
}
return {
name: displayName,
value,
offset,
searchIndex: offset.replace(/ /g, ''),
...(hasDivider && { hasDivider }),
};
};
const getOffsetByTimezone = (timezone: string): number => {
const dayjsTimezone = dayjs().tz(timezone);
return dayjsTimezone.utcOffset();
};
export const getBrowserTimezone = (): Timezone => {
const browserTz = dayjs.tz.guess();
const normalizedTz = normalizeTimezoneName(browserTz);
const browserOffset = getOffsetByTimezone(normalizedTz);
return createTimezoneEntry(
normalizedTz,
browserOffset,
TIMEZONE_TYPES.BROWSER,
);
};
const filterAndSortTimezones = (
allTimezones: string[],
browserTzName?: string,
includeEtcTimezones = false,
): Timezone[] =>
allTimezones
.filter((tz) => {
const isNotBrowserTz = tz !== browserTzName;
const isNotEtcTz = includeEtcTimezones || !tz.startsWith('Etc/');
return isNotBrowserTz && isNotEtcTz;
})
.sort((a, b) => a.localeCompare(b))
.map((tz) => {
const normalizedTz = normalizeTimezoneName(tz);
const offset = getOffsetByTimezone(normalizedTz);
return createTimezoneEntry(normalizedTz, offset);
});
const generateTimezoneData = (includeEtcTimezones = false): Timezone[] => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const allTimezones = (Intl as any).supportedValuesOf('timeZone');
const timezones: Timezone[] = [];
// Add browser timezone
const browserTzObject = getBrowserTimezone();
timezones.push(browserTzObject);
// Add UTC timezone with divider
timezones.push(UTC_TIMEZONE);
timezones.push(
...filterAndSortTimezones(
allTimezones,
browserTzObject.value,
includeEtcTimezones,
),
);
return timezones;
};
export const getTimezoneObjectByTimezoneString = (
timezone: string,
): Timezone => {
const utcOffset = getOffsetByTimezone(timezone);
return createTimezoneEntry(timezone, utcOffset);
};
export const TIMEZONE_DATA = generateTimezoneData();

View File

@@ -1,4 +1,5 @@
import {
_adapters,
BarController,
BarElement,
CategoryScale,
@@ -18,8 +19,10 @@ import {
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import isEqual from 'lodash-es/isEqual';
import { useTimezone } from 'providers/Timezone';
import {
forwardRef,
memo,
@@ -62,6 +65,17 @@ Chart.register(
Tooltip.positioners.custom = TooltipPositionHandler;
// Map of Chart.js time formats to dayjs format strings
const formatMap = {
'HH:mm:ss': 'HH:mm:ss',
'HH:mm': 'HH:mm',
'MM/DD HH:mm': 'MM/DD HH:mm',
'MM/dd HH:mm': 'MM/DD HH:mm',
'MM/DD': 'MM/DD',
'YY-MM': 'YY-MM',
YY: 'YY',
};
const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
(
{
@@ -80,11 +94,13 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
dragSelectColor,
},
ref,
// eslint-disable-next-line sonarjs/cognitive-complexity
): JSX.Element => {
const nearestDatasetIndex = useRef<null | number>(null);
const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode();
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
const { timezone } = useTimezone();
const currentTheme = isDarkMode ? 'dark' : 'light';
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
@@ -112,6 +128,22 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
return 'rgba(231,233,237,0.8)';
}, [currentTheme]);
// Override Chart.js date adapter to use dayjs with timezone support
useEffect(() => {
_adapters._date.override({
format(time: number | Date, fmt: string) {
const dayjsTime = dayjs(time).tz(timezone.value);
const format = formatMap[fmt as keyof typeof formatMap];
if (!format) {
console.warn(`Missing datetime format for ${fmt}`);
return dayjsTime.format('YYYY-MM-DD HH:mm:ss'); // fallback format
}
return dayjsTime.format(format);
},
});
}, [timezone]);
const buildChart = useCallback(() => {
if (lineChartRef.current !== undefined) {
lineChartRef.current.destroy();
@@ -132,6 +164,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked,
onClickHandler,
data,
timezone,
);
const chartHasData = hasData(data);
@@ -166,6 +199,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked,
onClickHandler,
data,
timezone,
name,
type,
]);

View File

@@ -1,5 +1,6 @@
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import dayjs from 'dayjs';
import { MutableRefObject } from 'react';
@@ -50,6 +51,7 @@ export const getGraphOptions = (
isStacked: boolean | undefined,
onClickHandler: GraphOnClickHandler | undefined,
data: ChartData,
timezone: Timezone,
// eslint-disable-next-line sonarjs/cognitive-complexity
): CustomChartOptions => ({
animation: {
@@ -97,7 +99,7 @@ export const getGraphOptions = (
callbacks: {
title(context): string | string[] {
const date = dayjs(context[0].parsed.x);
return date.format('MMM DD, YYYY, HH:mm:ss');
return date.tz(timezone.value).format('MMM DD, YYYY, HH:mm:ss');
},
label(context): string | string[] {
let label = context.dataset.label || '';

View File

@@ -0,0 +1,66 @@
.host-containers {
gap: 24px;
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
.infra-container-card-container {
display: flex;
flex-direction: column;
gap: 24px;
}
.dev-status-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.infra-container-card {
display: flex;
flex-direction: column;
justify-content: center;
}
.infra-container-card-text {
font-size: var(--font-size-sm);
color: var(--text-vanilla-400);
line-height: 20px;
letter-spacing: -0.07px;
width: 400px;
font-family: 'Inter';
margin-top: 12px;
font-weight: 300;
}
.infra-container-working-msg {
display: flex;
width: 400px;
padding: 12px;
align-items: flex-start;
gap: 12px;
border-radius: 4px;
background: rgba(171, 189, 255, 0.04);
.ant-space {
align-items: flex-start;
}
}
.infra-container-contact-support-btn {
display: flex;
align-items: center;
justify-content: center;
margin: auto;
}
}
.lightMode {
.infra-container-card-text {
color: var(--text-ink-200);
}
}

View File

@@ -0,0 +1,44 @@
import './Containers.styles.scss';
import { Space, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import WaitlistFragment from '../WaitlistFragment/WaitlistFragment';
const { Text } = Typography;
function Containers(): JSX.Element {
const { t } = useTranslation(['infraMonitoring']);
return (
<Space direction="vertical" className="host-containers" size={24}>
<div className="infra-container-card-container">
<div className="dev-status-container">
<div className="infra-container-card">
<img
src="/Icons/infraContainers.svg"
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('containers_visualization_message')}
</Text>
</div>
<div className="infra-container-working-msg">
<Space>
<img src="/Icons/broom.svg" alt="broom" width={24} height={24} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
</div>
</div>
<WaitlistFragment entityType="containers" />
</div>
</Space>
);
}
export default Containers;

View File

@@ -0,0 +1,7 @@
import { HostData } from 'api/infraMonitoring/getHostLists';
export type HostDetailProps = {
host: HostData | null;
isModalTimeSelection: boolean;
onClose: () => void;
};

View File

@@ -0,0 +1,193 @@
.host-metric-traces {
margin-top: 1rem;
.host-metric-traces-header {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}
}
.host-metric-traces-table {
.ant-table-content {
overflow: hidden !important;
}
.ant-table {
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
background: rgba(171, 189, 255, 0.01);
border-bottom: none;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;
}
}
.ant-table-thead > tr > th:has(.hostname-column-header) {
background: var(--bg-ink-400);
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--bg-vanilla-100);
background: rgba(171, 189, 255, 0.01);
}
.ant-table-cell:has(.hostname-column-value) {
background: var(--bg-ink-400);
}
.hostname-column-value {
color: var(--bg-vanilla-100);
font-family: 'Geist Mono';
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.status-cell {
.active-tag {
color: var(--bg-forest-500);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
}
.progress-container {
.ant-progress-bg {
height: 8px !important;
border-radius: 4px;
}
}
.ant-table-tbody > tr:hover > td {
background: rgba(255, 255, 255, 0.04);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
.ant-table-thead
> tr
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
background-color: transparent;
}
.ant-empty-normal {
visibility: hidden;
}
}
.ant-table-container::after {
content: none;
}
}
}
.lightMode {
.host-metric-traces-header {
.filter-section {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}
.host-metric-traces-table {
.ant-table {
border-radius: 3px;
border: 1px solid var(--bg-vanilla-300);
.ant-table-thead > tr > th {
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.ant-table-thead > tr > th:has(.hostname-column-header) {
background: var(--bg-vanilla-100);
}
.ant-table-cell {
background: var(--bg-vanilla-100);
color: var(--bg-ink-500);
}
.ant-table-cell:has(.hostname-column-value) {
background: var(--bg-vanilla-100);
}
.hostname-column-value {
color: var(--bg-ink-300);
}
.ant-table-tbody > tr:hover > td {
background: rgba(0, 0, 0, 0.04);
}
}
}
}

View File

@@ -0,0 +1,195 @@
import './HostMetricTraces.styles.scss';
import { ResizeTable } from 'components/ResizeTable';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import NoLogs from 'container/NoLogs/NoLogs';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { ErrorText } from 'container/TimeSeriesView/styles';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import TraceExplorerControls from 'container/TracesExplorer/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination } from 'hooks/queryPagination';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getHostTracesQueryPayload, selectedColumns } from './constants';
import { getListColumns } from './utils';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeTracesFilters: (value: IBuilderQuery['filters']) => void;
tracesFilters: IBuilderQuery['filters'];
selectedInterval: Time;
}
function HostMetricTraces({
timeRange,
isModalTimeSelection,
handleTimeChange,
handleChangeTracesFilters,
tracesFilters,
selectedInterval,
}: Props): JSX.Element {
const [traces, setTraces] = useState<any[]>([]);
const [offset] = useState<number>(0);
const { currentQuery } = useQueryBuilder();
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
QueryParams.pagination,
);
const queryPayload = useMemo(
() =>
getHostTracesQueryPayload(
timeRange.startTime,
timeRange.endTime,
paginationQueryData?.offset || offset,
tracesFilters,
),
[
timeRange.startTime,
timeRange.endTime,
offset,
tracesFilters,
paginationQueryData,
],
);
const { data, isLoading, isFetching, isError } = useQuery({
queryKey: [
'hostMetricTraces',
timeRange.startTime,
timeRange.endTime,
offset,
tracesFilters,
DEFAULT_ENTITY_VERSION,
paginationQueryData,
],
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
enabled: !!queryPayload,
});
const traceListColumns = getListColumns(selectedColumns);
useEffect(() => {
if (data?.payload?.data?.newResult?.data?.result) {
const currentData = data.payload.data.newResult.data.result;
if (currentData.length > 0 && currentData[0].list) {
if (offset === 0) {
setTraces(currentData[0].list ?? []);
} else {
setTraces((prev) => [...prev, ...(currentData[0].list ?? [])]);
}
}
}
}, [data, offset]);
const isDataEmpty =
!isLoading && !isFetching && !isError && traces.length === 0;
const hasAdditionalFilters = tracesFilters.items.length > 1;
const totalCount =
data?.payload?.data?.newResult?.data?.result?.[0]?.list?.length || 0;
return (
<div className="host-metric-traces">
<div className="host-metric-traces-header">
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeTracesFilters}
disableNavigationShortcuts
/>
)}
</div>
<div className="datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
/>
</div>
</div>
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
{isLoading && traces.length === 0 && <TracesLoading />}
{isDataEmpty && !hasAdditionalFilters && (
<NoLogs dataSource={DataSource.TRACES} />
)}
{isDataEmpty && hasAdditionalFilters && (
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="LIST" />
)}
{!isError && traces.length > 0 && (
<div className="host-metric-traces-table">
<TraceExplorerControls
isLoading={isFetching}
totalCount={totalCount}
perPageOptions={PER_PAGE_OPTIONS}
showSizeChanger={false}
/>
<ResizeTable
tableLayout="fixed"
pagination={false}
scroll={{ x: true }}
loading={isFetching}
dataSource={traces}
columns={traceListColumns}
/>
</div>
)}
</div>
);
}
export default HostMetricTraces;

View File

@@ -0,0 +1,200 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { nanoToMilli } from 'utils/timeUtils';
export const columns = [
{
dataIndex: 'timestamp',
key: 'timestamp',
title: 'Timestamp',
width: 200,
render: (timestamp: string): string => new Date(timestamp).toLocaleString(),
},
{
title: 'Service Name',
dataIndex: ['data', 'serviceName'],
key: 'serviceName-string-tag',
width: 150,
},
{
title: 'Name',
dataIndex: ['data', 'name'],
key: 'name-string-tag',
width: 145,
},
{
title: 'Duration',
dataIndex: ['data', 'durationNano'],
key: 'durationNano-float64-tag',
width: 145,
render: (duration: number): string => `${nanoToMilli(duration)}ms`,
},
{
title: 'HTTP Method',
dataIndex: ['data', 'httpMethod'],
key: 'httpMethod-string-tag',
width: 145,
},
{
title: 'Status Code',
dataIndex: ['data', 'responseStatusCode'],
key: 'responseStatusCode-string-tag',
width: 145,
},
];
export const selectedColumns: BaseAutocompleteData[] = [
{
key: 'timestamp',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'serviceName',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'name',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'durationNano',
dataType: DataTypes.Float64,
type: 'tag',
isColumn: true,
},
{
key: 'httpMethod',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
{
key: 'responseStatusCode',
dataType: DataTypes.String,
type: 'tag',
isColumn: true,
},
];
export const getHostTracesQueryPayload = (
start: number,
end: number,
offset = 0,
filters: IBuilderQuery['filters'],
): GetQueryResultsProps => ({
query: {
promql: [],
clickhouse_sql: [],
builder: {
queryData: [
{
dataSource: DataSource.TRACES,
queryName: 'A',
aggregateOperator: 'noop',
aggregateAttribute: {
id: '------false',
dataType: DataTypes.EMPTY,
key: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters,
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
queryType: EQueryType.QUERY_BUILDER,
},
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
start,
end,
params: {
dataSource: DataSource.TRACES,
},
tableParams: {
pagination: {
limit: 10,
offset,
},
selectColumns: [
{
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
isIndexed: false,
},
{
key: 'name',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
isIndexed: false,
},
{
key: 'durationNano',
dataType: 'float64',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
isIndexed: false,
},
{
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
isIndexed: false,
},
{
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
},
],
},
});

View File

@@ -0,0 +1,84 @@
import { Tag, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import {
BlockLink,
getTraceLink,
} from 'container/TracesExplorer/ListView/utils';
import dayjs from 'dayjs';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
const keyToLabelMap: Record<string, string> = {
timestamp: 'Timestamp',
serviceName: 'Service Name',
name: 'Name',
durationNano: 'Duration',
httpMethod: 'HTTP Method',
responseStatusCode: 'Status Code',
};
export const getListColumns = (
selectedColumns: BaseAutocompleteData[],
): ColumnsType<RowData> => {
const columns: ColumnsType<RowData> =
selectedColumns.map(({ dataType, key, type }) => ({
title: keyToLabelMap[key],
dataIndex: key,
key: `${key}-${dataType}-${type}`,
width: 145,
render: (value, item): JSX.Element => {
const itemData = item.data as any;
if (key === 'timestamp') {
const date =
typeof value === 'string'
? dayjs(value).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(value / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return (
<BlockLink to={getTraceLink(item)} openInNewTab>
<Typography.Text>{date}</Typography.Text>
</BlockLink>
);
}
if (value === '') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>N/A</Typography>
</BlockLink>
);
}
if (key === 'httpMethod' || key === 'responseStatusCode') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Tag data-testid={key} color="magenta">
{itemData[key]}
</Tag>
</BlockLink>
);
}
if (key === 'durationNano') {
const durationNano = itemData[key];
return (
<BlockLink to={getTraceLink(item)} openInNewTab>
<Typography data-testid={key}>{getMs(durationNano)}ms</Typography>
</BlockLink>
);
}
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>{itemData[key]}</Typography>
</BlockLink>
);
},
responsive: ['md'],
})) || [];
return columns;
};

View File

@@ -0,0 +1,232 @@
.host-detail-drawer {
border-left: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
padding: 8px 16px;
border-bottom: none;
align-items: stretch;
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
}
.ant-drawer-close {
margin-inline-end: 0px;
}
.ant-drawer-body {
display: flex;
flex-direction: column;
padding: 16px;
}
.title {
color: var(--text-vanilla-400);
font-family: 'Geist Mono';
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.radio-button {
display: flex;
align-items: center;
justify-content: center;
padding-top: var(--padding-1);
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.host-detail-drawer__host {
.host-details-grid {
.labels-row,
.values-row {
display: grid;
grid-template-columns: 1fr 1.5fr 1.5fr 1.5fr;
gap: 30px;
align-items: center;
}
.labels-row {
margin-bottom: 8px;
}
.host-details-metadata-label {
color: var(--text-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.status-tag {
margin: 0;
&.active {
color: var(--success-500);
background: var(--success-100);
border-color: var(--success-500);
}
&.inactive {
color: var(--error-500);
background: var(--error-100);
border-color: var(--error-500);
}
}
.progress-container {
width: 158px;
.ant-progress {
margin: 0;
.ant-progress-text {
font-weight: 600;
}
}
}
.ant-card {
&.ant-card-bordered {
border: 1px solid var(--bg-slate-500) !important;
}
}
}
}
.tabs-and-search {
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 0;
.action-btn {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
}
.views-tabs-container {
margin-top: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
.views-tabs {
color: var(--text-vanilla-400);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
.tab {
border: 1px solid var(--bg-slate-400);
width: 114px;
}
.tab::before {
background: var(--bg-slate-400);
}
.selected_view {
background: var(--bg-slate-300);
color: var(--text-vanilla-100);
border: 1px solid var(--bg-slate-400);
}
.selected_view::before {
background: var(--bg-slate-400);
}
}
.compass-button {
width: 30px;
height: 30px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}
}
.lightMode {
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
}
.host-detail-drawer {
.title {
color: var(--text-ink-300);
}
.host-detail-drawer__host {
.ant-typography {
color: var(--text-ink-300);
background: transparent;
}
}
.radio-button {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.views-tabs {
.tab {
background: var(--bg-vanilla-100);
}
.selected_view {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--text-ink-400);
}
.selected_view::before {
background: var(--bg-vanilla-300);
border-left: 1px solid var(--bg-slate-300);
}
}
.compass-button {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.tabs-and-search {
.action-btn {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
}
}
}

View File

@@ -0,0 +1,517 @@
import './HostMetricsDetail.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import {
Button,
Divider,
Drawer,
Progress,
Radio,
Tag,
Typography,
} from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import {
BarChart2,
ChevronsLeftRight,
Compass,
DraftingCompass,
Package2,
ScrollText,
X,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import { VIEW_TYPES, VIEWS } from './constants';
import Containers from './Containers/Containers';
import { HostDetailProps } from './HostMetricDetail.interfaces';
import HostMetricLogsDetailedView from './HostMetricsLogs/HostMetricLogsDetailedView';
import HostMetricTraces from './HostMetricTraces/HostMetricTraces';
import Metrics from './Metrics/Metrics';
import Processes from './Processes/Processes';
// eslint-disable-next-line sonarjs/cognitive-complexity
function HostMetricsDetails({
host,
onClose,
isModalTimeSelection,
}: HostDetailProps): JSX.Element {
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
minTime,
]);
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
maxTime,
]);
const urlQuery = useUrlQuery();
const [modalTimeRange, setModalTimeRange] = useState(() => ({
startTime: startMs,
endTime: endMs,
}));
const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time,
);
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(
() => ({
op: 'AND',
items: [
{
id: uuidv4(),
key: {
key: 'host.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
id: 'host.name--string--resource--false',
},
op: '=',
value: host?.hostName || '',
},
],
}),
[host?.hostName],
);
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
initialFilters,
);
useEffect(() => {
logEvent('Infra Monitoring: Hosts list details page visited', {
host: host?.hostName,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setLogFilters(initialFilters);
setTracesFilters(initialFilters);
}, [initialFilters]);
useEffect(() => {
setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
}, [selectedTime, minTime, maxTime]);
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
};
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) {
setModalTimeRange({
startTime: Math.floor(dateTimeRange[0] / 1000),
endTime: Math.floor(dateTimeRange[1] / 1000),
});
} else {
const { maxTime, minTime } = GetMinMax(interval);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
logEvent('Infra Monitoring: Hosts list details time updated', {
host: host?.hostName,
interval,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setLogFilters((prevFilters) => {
const hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === 'host.name',
);
const paginationFilter = value.items.find((item) => item.key?.key === 'id');
const newFilters = value.items.filter(
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
);
logEvent('Infra Monitoring: Hosts list details logs filters applied', {
host: host?.hostName,
});
return {
op: 'AND',
items: [
hostNameFilter,
...newFilters,
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setTracesFilters((prevFilters) => {
const hostNameFilter = prevFilters.items.find(
(item) => item.key?.key === 'host.name',
);
logEvent('Infra Monitoring: Hosts list details traces filters applied', {
host: host?.hostName,
});
return {
op: 'AND',
items: [
hostNameFilter,
...value.items.filter((item) => item.key?.key !== 'host.name'),
].filter((item): item is TagFilterItem => item !== undefined),
};
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleExplorePagesRedirect = (): void => {
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
}
logEvent('Infra Monitoring: Hosts list details explore clicked', {
host: host?.hostName,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logFilters,
items: logFilters.items.filter((item) => item.key?.key !== 'id'),
};
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
filters: filtersWithoutPagination,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: tracesFilters,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};
const handleClose = (): void => {
setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime);
setModalTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
setSelectedView(VIEW_TYPES.METRICS);
onClose();
};
return (
<Drawer
width="70%"
title={
<>
<Divider type="vertical" />
<Typography.Text className="title">{host?.hostName}</Typography.Text>
</>
}
placement="right"
onClose={handleClose}
open={!!host}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="host-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{host && (
<>
<div className="host-detail-drawer__host">
<div className="host-details-grid">
<div className="labels-row">
<Typography.Text
type="secondary"
className="host-details-metadata-label"
>
STATUS
</Typography.Text>
<Typography.Text
type="secondary"
className="host-details-metadata-label"
>
OPERATING SYSTEM
</Typography.Text>
<Typography.Text
type="secondary"
className="host-details-metadata-label"
>
CPU USAGE
</Typography.Text>
<Typography.Text
type="secondary"
className="host-details-metadata-label"
>
MEMORY USAGE
</Typography.Text>
</div>
<div className="values-row">
<Tag
bordered
className={`infra-monitoring-tags ${
host.active ? 'active' : 'inactive'
}`}
>
{host.active ? 'ACTIVE' : 'INACTIVE'}
</Tag>
<Tag className="infra-monitoring-tags" bordered>
{host.os}
</Tag>
<div className="progress-container">
<Progress
percent={Number((host.cpu * 100).toFixed(1))}
size="small"
strokeColor={((): string => {
const cpuPercent = Number((host.cpu * 100).toFixed(1));
if (cpuPercent >= 90) return Color.BG_SAKURA_500;
if (cpuPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
<div className="progress-container">
<Progress
percent={Number((host.memory * 100).toFixed(1))}
size="small"
strokeColor={((): string => {
const memoryPercent = Number((host.memory * 100).toFixed(1));
if (memoryPercent >= 90) return Color.BG_CHERRY_500;
if (memoryPercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
</div>
</div>
</div>
<div className="views-tabs-container">
<Radio.Group
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.METRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.LOGS}
>
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.TRACES}
>
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTAINERS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTAINERS}
>
<div className="view-title">
<Package2 size={14} />
Containers
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.PROCESSES ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.PROCESSES}
>
<div className="view-title">
<ChevronsLeftRight size={14} />
Processes
</div>
</Radio.Button>
</Radio.Group>
{(selectedView === VIEW_TYPES.LOGS ||
selectedView === VIEW_TYPES.TRACES) && (
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
)}
</div>
{selectedView === VIEW_TYPES.METRICS && (
<Metrics
selectedInterval={selectedInterval}
hostName={host.hostName}
timeRange={modalTimeRange}
handleTimeChange={handleTimeChange}
isModalTimeSelection={isModalTimeSelection}
/>
)}
{selectedView === VIEW_TYPES.LOGS && (
<HostMetricLogsDetailedView
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logFilters}
selectedInterval={selectedInterval}
/>
)}
{selectedView === VIEW_TYPES.TRACES && (
<HostMetricTraces
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={tracesFilters}
selectedInterval={selectedInterval}
/>
)}
{selectedView === VIEW_TYPES.CONTAINERS && <Containers />}
{selectedView === VIEW_TYPES.PROCESSES && <Processes />}
</>
)}
</Drawer>
);
}
export default HostMetricsDetails;

View File

@@ -0,0 +1,133 @@
.host-metrics-logs-container {
margin-top: 1rem;
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-300) !important;
input {
font-size: 12px;
}
.ant-tag .ant-typography {
font-size: 12px;
}
}
}
.host-metrics-logs-header {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.host-metrics-logs {
margin-top: 1rem;
.virtuoso-list {
overflow-y: hidden !important;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
.ant-row {
width: fit-content;
}
}
.skeleton-container {
height: 100%;
padding: 16px;
}
}
}
.host-metrics-logs-list-container {
flex: 1;
height: calc(100vh - 272px) !important;
display: flex;
height: 100%;
.raw-log-content {
width: 100%;
text-wrap: inherit;
word-wrap: break-word;
}
}
.host-metrics-logs-list-card {
width: 100%;
margin-top: 12px;
.ant-card-body {
padding: 0;
height: 100%;
width: 100%;
}
}
.logs-loading-skeleton {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 0;
.ant-skeleton-input-sm {
height: 18px;
}
}
.no-logs-found {
height: 50vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
box-sizing: border-box;
.ant-typography {
display: flex;
align-items: center;
gap: 16px;
}
}
.lightMode {
.filter-section {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
color: var(--bg-ink-200);
}
}
}

View File

@@ -0,0 +1,95 @@
import './HostMetricLogs.styles.scss';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import HostMetricsLogs from './HostMetricsLogs';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
handleChangeLogFilters: (value: IBuilderQuery['filters']) => void;
logFilters: IBuilderQuery['filters'];
selectedInterval: Time;
}
function HostMetricLogsDetailedView({
timeRange,
isModalTimeSelection,
handleTimeChange,
handleChangeLogFilters,
logFilters,
selectedInterval,
}: Props): JSX.Element {
const { currentQuery } = useQueryBuilder();
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
dataSource: DataSource.LOGS,
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
},
],
},
}),
[currentQuery],
);
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
return (
<div className="host-metrics-logs-container">
<div className="host-metrics-logs-header">
<div className="filter-section">
{query && (
<QueryBuilderSearch
query={query}
onChange={handleChangeLogFilters}
disableNavigationShortcuts
/>
)}
</div>
<div className="datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
isModalTimeSelection={isModalTimeSelection}
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
modalSelectedInterval={selectedInterval}
/>
</div>
</div>
<HostMetricsLogs
timeRange={timeRange}
handleChangeLogFilters={handleChangeLogFilters}
filters={logFilters}
/>
</div>
);
}
export default HostMetricLogsDetailedView;

View File

@@ -0,0 +1,216 @@
/* eslint-disable no-nested-ternary */
import './HostMetricLogs.styles.scss';
import { Card } from 'antd';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso } from 'react-virtuoso';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 } from 'uuid';
import { getHostLogsQueryPayload } from './constants';
import NoLogsContainer from './NoLogsContainer';
interface Props {
timeRange: {
startTime: number;
endTime: number;
};
handleChangeLogFilters: (filters: IBuilderQuery['filters']) => void;
filters: IBuilderQuery['filters'];
}
function HostMetricsLogs({
timeRange,
handleChangeLogFilters,
filters,
}: Props): JSX.Element {
const [logs, setLogs] = useState<ILog[]>([]);
const [hasReachedEndOfLogs, setHasReachedEndOfLogs] = useState(false);
const [restFilters, setRestFilters] = useState<TagFilterItem[]>([]);
const [resetLogsList, setResetLogsList] = useState<boolean>(false);
useEffect(() => {
const newRestFilters = filters.items.filter(
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
);
const areFiltersSame = isEqual(restFilters, newRestFilters);
if (!areFiltersSame) {
setResetLogsList(true);
}
setRestFilters(newRestFilters);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters]);
const queryPayload = useMemo(() => {
const basePayload = getHostLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
filters,
);
basePayload.query.builder.queryData[0].pageSize = 100;
basePayload.query.builder.queryData[0].orderBy = [
{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC },
];
return basePayload;
}, [timeRange.startTime, timeRange.endTime, filters]);
const [isPaginating, setIsPaginating] = useState(false);
const { data, isLoading, isFetching, isError } = useQuery({
queryKey: [
'hostMetricsLogs',
timeRange.startTime,
timeRange.endTime,
filters,
],
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
enabled: !!queryPayload,
keepPreviousData: isPaginating,
});
useEffect(() => {
if (data?.payload?.data?.newResult?.data?.result) {
const currentData = data.payload.data.newResult.data.result;
if (resetLogsList) {
const currentLogs: ILog[] =
currentData[0].list?.map((item) => ({
...item.data,
timestamp: item.timestamp,
})) || [];
setLogs(currentLogs);
setResetLogsList(false);
}
if (currentData.length > 0 && currentData[0].list) {
const currentLogs: ILog[] =
currentData[0].list.map((item) => ({
...item.data,
timestamp: item.timestamp,
})) || [];
setLogs((prev) => [...prev, ...currentLogs]);
} else {
setHasReachedEndOfLogs(true);
}
}
}, [data, restFilters, isPaginating, resetLogsList]);
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isReadOnly
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
/>
),
[],
);
const loadMoreLogs = useCallback(() => {
if (!logs.length) return;
setIsPaginating(true);
const lastLog = logs[logs.length - 1];
const newItems = [
...filters.items.filter((item) => item.key?.key !== 'id'),
{
id: v4(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: '<',
value: lastLog.id,
},
];
const newFilters = {
op: 'AND',
items: newItems,
} as IBuilderQuery['filters'];
handleChangeLogFilters(newFilters);
}, [logs, filters, handleChangeLogFilters]);
useEffect(() => {
setIsPaginating(false);
}, [data]);
const renderFooter = useCallback(
(): JSX.Element | null => (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>
{isFetching ? (
<div className="logs-loading-skeleton"> Loading more logs ... </div>
) : hasReachedEndOfLogs ? (
<div className="logs-loading-skeleton"> *** End *** </div>
) : null}
</>
),
[isFetching, hasReachedEndOfLogs],
);
const renderContent = useMemo(
() => (
<Card bordered={false} className="host-metrics-logs-list-card">
<OverlayScrollbar isVirtuoso>
<Virtuoso
className="host-metrics-logs-virtuoso"
key="host-metrics-logs-virtuoso"
data={logs}
endReached={loadMoreLogs}
totalCount={logs.length}
itemContent={getItemContent}
overscan={200}
components={{
Footer: renderFooter,
}}
/>
</OverlayScrollbar>
</Card>
),
[logs, loadMoreLogs, getItemContent, renderFooter],
);
return (
<div className="host-metrics-logs">
{isLoading && <LogsLoading />}
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<div className="host-metrics-logs-list-container">{renderContent}</div>
)}
</div>
);
}
export default HostMetricsLogs;

View File

@@ -0,0 +1,16 @@
import { Color } from '@signozhq/design-tokens';
import { Typography } from 'antd';
import { Ghost } from 'lucide-react';
const { Text } = Typography;
export default function NoLogsContainer(): React.ReactElement {
return (
<div className="no-logs-found">
<Text type="secondary">
<Ghost size={24} color={Color.BG_AMBER_500} /> No logs found for this host
in the selected time range.
</Text>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuidv4 } from 'uuid';
export const getHostLogsQueryPayload = (
start: number,
end: number,
filters: IBuilderQuery['filters'],
): GetQueryResultsProps => ({
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
query: {
clickhouse_sql: [],
promql: [],
builder: {
queryData: [
{
dataSource: DataSource.LOGS,
queryName: 'A',
aggregateOperator: 'noop',
aggregateAttribute: {
id: '------false',
dataType: DataTypes.String,
key: '',
isColumn: false,
type: '',
isJSON: false,
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters,
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
groupBy: [],
legend: '',
reduceTo: 'avg',
offset: 0,
pageSize: 100,
},
],
queryFormulas: [],
},
id: uuidv4(),
queryType: EQueryType.QUERY_BUILDER,
},
params: {
lastLogLineTimestamp: null,
},
start,
end,
});

View File

@@ -0,0 +1,45 @@
.empty-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.host-metrics-container {
margin-top: 1rem;
}
.metrics-header {
display: flex;
justify-content: flex-end;
margin-top: 1rem;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
}
.host-metrics-card {
margin: 8px 0 1rem 0;
height: 300px;
padding: 10px;
border: 1px solid var(--bg-slate-500);
.ant-card-body {
padding: 0;
}
.chart-container {
width: 100%;
height: 100%;
}
.no-data-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

View File

@@ -0,0 +1,142 @@
import './Metrics.styles.scss';
import { Card, Col, Row, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import Uplot from 'components/Uplot';
import { ENTITY_VERSION_V4 } from 'constants/app';
import {
getHostQueryPayload,
hostWidgetInfo,
} from 'container/LogDetailedView/InfraMetrics/constants';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useMemo, useRef } from 'react';
import { useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
interface MetricsTabProps {
timeRange: {
startTime: number;
endTime: number;
};
isModalTimeSelection: boolean;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
selectedInterval: Time;
hostName: string;
}
function Metrics({
selectedInterval,
hostName,
timeRange,
handleTimeChange,
isModalTimeSelection,
}: MetricsTabProps): JSX.Element {
const queryPayloads = useMemo(
() => getHostQueryPayload(hostName, timeRange.startTime, timeRange.endTime),
[hostName, timeRange.startTime, timeRange.endTime],
);
const queries = useQueries(
queryPayloads.map((payload) => ({
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
})),
);
const isDarkMode = useIsDarkMode();
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
const chartData = useMemo(
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
[queries],
);
const options = useMemo(
() =>
queries.map(({ data }, idx) =>
getUPlotChartOptions({
apiResponse: data?.payload,
isDarkMode,
dimensions,
yAxisUnit: hostWidgetInfo[idx].yAxisUnit,
softMax: null,
softMin: null,
minTimeScale: timeRange.startTime,
maxTimeScale: timeRange.endTime,
}),
),
[queries, isDarkMode, dimensions, timeRange.startTime, timeRange.endTime],
);
const renderCardContent = (
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
idx: number,
): JSX.Element => {
if (query.isLoading) {
return <Skeleton />;
}
if (query.error) {
const errorMessage =
(query.error as Error)?.message || 'Something went wrong';
return <div>{errorMessage}</div>;
}
return (
<div
className={cx('chart-container', {
'no-data-container':
!query.isLoading && !query?.data?.payload?.data?.result?.length,
})}
>
<Uplot options={options[idx]} data={chartData[idx]} />
</div>
);
};
return (
<>
<div className="metrics-header">
<div className="metrics-datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
onTimeChange={handleTimeChange}
defaultRelativeTime="5m"
isModalTimeSelection={isModalTimeSelection}
modalSelectedInterval={selectedInterval}
/>
</div>
</div>
<Row gutter={24} className="host-metrics-container">
{queries.map((query, idx) => (
<Col span={12} key={hostWidgetInfo[idx].title}>
<Typography.Text>{hostWidgetInfo[idx].title}</Typography.Text>
<Card bordered className="host-metrics-card" ref={graphRef}>
{renderCardContent(query, idx)}
</Card>
</Col>
))}
</Row>
</>
);
}
export default Metrics;

View File

@@ -0,0 +1,66 @@
.host-processes {
gap: 24px;
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
margin: 0 auto;
box-sizing: border-box;
.infra-container-card-container {
display: flex;
flex-direction: column;
gap: 24px;
}
.dev-status-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.infra-container-card {
display: flex;
flex-direction: column;
justify-content: center;
}
.infra-container-card-text {
font-size: var(--font-size-sm);
color: var(--text-vanilla-400);
line-height: 20px;
letter-spacing: -0.07px;
width: 400px;
font-family: 'Inter';
margin-top: 12px;
font-weight: 300;
}
.infra-container-working-msg {
display: flex;
width: 400px;
padding: 12px;
align-items: flex-start;
gap: 12px;
border-radius: 4px;
background: rgba(171, 189, 255, 0.04);
.ant-space {
align-items: flex-start;
}
}
.infra-container-contact-support-btn {
display: flex;
align-items: center;
justify-content: center;
margin: auto;
}
}
.lightMode {
.infra-container-card-text {
color: var(--text-ink-200);
}
}

View File

@@ -0,0 +1,43 @@
import './Processes.styles.scss';
import { Space, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import WaitlistFragment from '../WaitlistFragment/WaitlistFragment';
const { Text } = Typography;
function Processes(): JSX.Element {
const { t } = useTranslation(['infraMonitoring']);
return (
<Space direction="vertical" className="host-processes" size={24}>
<div className="infra-container-card-container">
<div className="dev-status-container">
<div className="infra-container-card">
<img
src="/Icons/infraContainers.svg"
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('processes_visualization_message')}
</Text>
</div>
<div className="infra-container-working-msg">
<Space>
<img src="/Icons/broom.svg" alt="broom" width={24} height={24} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
</div>
</div>
<WaitlistFragment entityType="processes" />
</div>
</Space>
);
}
export default Processes;

View File

@@ -0,0 +1,15 @@
.wait-list-container {
display: flex;
flex-direction: column;
gap: 8px;
.wait-list-text {
font-weight: 300;
}
.join-waitlist-btn {
width: 160px;
border-radius: 2px;
background: var(--slate-500);
}
}

View File

@@ -0,0 +1,75 @@
import './WaitListFragment.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useNotifications } from 'hooks/useNotifications';
import { CheckCircle2, HandPlatter } from 'lucide-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
export default function WaitlistFragment({
entityType,
}: {
entityType: string;
}): JSX.Element {
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { t } = useTranslation(['infraMonitoring']);
const { notifications } = useNotifications();
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const handleJoinWaitlist = (): void => {
if (!user || !user.email) return;
setIsSubmitting(true);
logEvent('Infra Monitoring: Get Early Access Clicked', {
entity_type: entityType,
userEmail: user.email,
})
.then(() => {
notifications.success({
message: t('waitlist_success_message'),
});
setIsSubmitting(false);
setIsSuccess(true);
setTimeout(() => {
setIsSuccess(false);
}, 4000);
})
.catch((error) => {
console.error('Error logging event:', error);
});
};
return (
<div className="wait-list-container">
<Typography.Text className="wait-list-text">
{t('waitlist_message')}
</Typography.Text>
<Button
className="periscope-btn join-waitlist-btn"
type="default"
loading={isSubmitting}
icon={
isSuccess ? (
<CheckCircle2 size={16} color={Color.BG_FOREST_500} />
) : (
<HandPlatter size={16} />
)
}
onClick={handleJoinWaitlist}
>
Get early access
</Button>
</div>
);
}

View File

@@ -0,0 +1,15 @@
export enum VIEWS {
METRICS = 'metrics',
LOGS = 'logs',
TRACES = 'traces',
CONTAINERS = 'containers',
PROCESSES = 'processes',
}
export const VIEW_TYPES = {
METRICS: VIEWS.METRICS,
LOGS: VIEWS.LOGS,
TRACES: VIEWS.TRACES,
CONTAINERS: VIEWS.CONTAINERS,
PROCESSES: VIEWS.PROCESSES,
};

View File

@@ -0,0 +1,3 @@
import HostMetricsDetails from './HostMetricsDetails';
export default HostMetricsDetails;

View File

@@ -8,13 +8,13 @@ import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
// utils
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useMemo, useState } from 'react';
// interfaces
import { IField } from 'types/api/logs/fields';
@@ -174,12 +174,20 @@ function ListLogView({
[selectedFields],
);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const timestampValue = useMemo(
() =>
typeof flattenLogData.timestamp === 'string'
? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'),
[flattenLogData.timestamp],
? formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp,
'YYYY-MM-DD HH:mm:ss.SSS',
)
: formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
),
[flattenLogData.timestamp, formatTimezoneAdjustedTimestamp],
);
const logType = getLogIndicatorType(logData);

View File

@@ -17,6 +17,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log Message',
resources_string: {},
attributesString: {},
scope_string: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
@@ -40,6 +41,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log Message',
resources_string: {},
attributesString: {},
scope_string: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
@@ -62,6 +64,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log Message',
resources_string: {},
attributesString: {},
scope_string: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
@@ -83,6 +86,7 @@ describe('getLogIndicatorType', () => {
body: 'Sample log',
resources_string: {},
attributesString: {},
scope_string: {},
attributes_string: {
log_level: 'INFO' as never,
},
@@ -112,6 +116,7 @@ describe('getLogIndicatorTypeForTable', () => {
attributesString: {},
attributes_string: {},
attributesInt: {},
scope_string: {},
attributesFloat: {},
severity_text: 'WARN',
};
@@ -130,6 +135,7 @@ describe('getLogIndicatorTypeForTable', () => {
severity_number: 0,
body: 'Sample log message',
resources_string: {},
scope_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
@@ -166,6 +172,7 @@ describe('logIndicatorBySeverityNumber', () => {
body: 'Sample log Message',
resources_string: {},
attributesString: {},
scope_string: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},

View File

@@ -6,7 +6,6 @@ import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
@@ -14,6 +13,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { isEmpty, isNumber, isUndefined } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import {
KeyboardEvent,
MouseEvent,
@@ -89,16 +89,24 @@ function RawLogView({
attributesText += ' | ';
}
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const text = useMemo(() => {
const date =
typeof data.timestamp === 'string'
? dayjs(data.timestamp)
: dayjs(data.timestamp / 1e6);
? formatTimezoneAdjustedTimestamp(data.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS')
: formatTimezoneAdjustedTimestamp(
data.timestamp / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
);
return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${
data.body
}`;
}, [data.timestamp, data.body, attributesText]);
return `${date} | ${attributesText} ${data.body}`;
}, [
data.timestamp,
data.body,
attributesText,
formatTimezoneAdjustedTimestamp,
]);
const handleClickExpand = useCallback(() => {
if (activeContextLog || isReadOnly) return;
@@ -173,6 +181,7 @@ function RawLogView({
<LogStateIndicator type={logType} fontSize={fontSize} />
<RawLogContent
className="raw-log-content"
$isReadOnly={isReadOnly}
$isActiveLog={isActiveLog}
$isDarkMode={isDarkMode}

View File

@@ -5,10 +5,10 @@ import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import cx from 'classnames';
import { unescapeString } from 'container/LogDetailedView/utils';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { useMemo } from 'react';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
@@ -44,6 +44,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
logs,
]);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id')
@@ -81,8 +83,11 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
: formatTimezoneAdjustedTimestamp(
field / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
);
return {
children: (
<div className="table-timestamp">
@@ -125,7 +130,15 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
},
...(appendTo === 'end' ? fieldColumns : []),
];
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
}, [
fields,
isListViewPanel,
appendTo,
isDarkMode,
linesPerRow,
fontSize,
formatTimezoneAdjustedTimestamp,
]);
return { columns, dataSource: flattenLogData };
};

View File

@@ -3,7 +3,7 @@
position: absolute;
right: -2px;
margin: 6px 0;
width: 160px;
width: 240px;
border-radius: 4px;
@@ -24,7 +24,7 @@
.back-btn {
display: flex;
align-items: center;
gap: 6px;
gap: 4px;
padding: 12px;
border: none !important;
box-shadow: none !important;
@@ -32,14 +32,16 @@
.icon {
flex-shrink: 0;
}
.text {
color: var(--bg-vanilla-400);
color: var(--bg-slate-50);
font-family: Inter;
font-size: 13px;
font-size: 11px;
font-style: normal;
font-weight: 400;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: 0.14px;
text-transform: uppercase;
}
}
@@ -252,6 +254,75 @@
}
}
.add-new-column-container {
display: flex;
flex-direction: column;
.add-new-column-header {
display: flex;
flex-direction: column;
padding: 8px;
gap: 8px;
.back-icon {
cursor: pointer;
}
.title {
display: flex;
gap: 4px;
align-items: center;
color: var(--bg-slate-50);
text-transform: uppercase;
font-size: 11px;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
}
}
.add-new-column-content {
display: flex;
flex-direction: column;
padding-bottom: 16px;
min-height: 240px;
max-height: 400px;
.loading-container {
padding: 8px;
}
.column-format-new-options {
overflow-y: auto;
overflow-x: hidden;
.column-name {
padding: 4px 8px;
border-radius: 1px;
color: var(--bg-vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
&.selected {
background-color: var(--bg-ink-200);
cursor: pointer;
}
}
&::-webkit-scrollbar {
height: 1rem;
width: 0.2rem;
}
}
}
}
.selected-item-content-container {
.add-new-column-header {
padding: 8px;
@@ -314,6 +385,22 @@
cursor: pointer;
&.default-column {
color: var(--bg-vanilla-400, #c0c1c3);
cursor: not-allowed;
}
&.no-columns-selected {
color: var(--bg-slate-100);
font-size: 12px;
cursor: not-allowed;
}
&.add-new-column-btn {
color: var(--bg-vanilla-400, #c0c1c3);
cursor: pointer;
}
.name {
flex: 1;
overflow: hidden;
@@ -428,6 +515,30 @@
}
}
.add-new-column-container {
.add-new-column-header {
.title {
color: var(--bg-ink-100);
}
}
.add-new-column-content {
.column-format-new-options {
.column-name {
color: var(--bg-ink-400);
&.selected {
background-color: var(--bg-vanilla-400);
}
}
}
.loading-container {
color: var(--bg-ink-400);
}
}
}
.font-size-container {
.title {
color: var(--bg-ink-100);

View File

@@ -3,13 +3,14 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './LogsFormatOptionsMenu.styles.scss';
import { Button, Divider, Input, InputNumber, Tooltip, Typography } from 'antd';
import { Button, Input, InputNumber, Tooltip, Typography } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Check, ChevronLeft, ChevronRight, Minus, Plus, X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
interface LogsFormatOptionsMenuProps {
title: string;
@@ -35,7 +36,13 @@ export default function LogsFormatOptionsMenu({
false,
);
const [addNewColumn, setAddNewColumn] = useState(false);
const [showAddNewColumnContainer, setShowAddNewColumnContainer] = useState(
false,
);
const [selectedValue, setSelectedValue] = useState<string | null>(null);
const listRef = useRef<HTMLDivElement>(null);
const initialMouseEnterRef = useRef<boolean>(false);
const onChange = useCallback(
(key: LogViewMode) => {
@@ -49,7 +56,7 @@ export default function LogsFormatOptionsMenu({
const handleMenuItemClick = (key: LogViewMode): void => {
setSelectedItem(key);
onChange(key);
setAddNewColumn(false);
setShowAddNewColumnContainer(false);
};
const incrementMaxLinesPerRow = (): void => {
@@ -75,7 +82,8 @@ export default function LogsFormatOptionsMenu({
}, 300);
const handleToggleAddNewColumn = (): void => {
setAddNewColumn(!addNewColumn);
addColumn?.onSearch?.('');
setShowAddNewColumnContainer(!showAddNewColumnContainer);
};
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
@@ -100,9 +108,106 @@ export default function LogsFormatOptionsMenu({
}
}, [fontSizeValue]);
function handleColumnSelection(
currentIndex: number,
optionsData: DefaultOptionType[],
): void {
const currentItem = optionsData[currentIndex];
const itemLength = optionsData.length;
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(selectedValue, {
label: currentItem.label,
disabled: false,
});
// if the last element is selected then select the previous one
if (currentIndex === itemLength - 1) {
// there should be more than 1 element in the list
if (currentIndex - 1 >= 0) {
const prevValue = optionsData[currentIndex - 1]?.value || null;
setSelectedValue(prevValue as string | null);
} else {
// if there is only one element then just select and do nothing
setSelectedValue(null);
}
} else {
// selecting any random element from the list except the last one
const nextIndex = currentIndex + 1;
const nextValue = optionsData[nextIndex]?.value || null;
setSelectedValue(nextValue as string | null);
}
}
}
const handleKeyDown = (e: KeyboardEvent): void => {
if (!selectedValue) return;
const optionsData = addColumn?.options || [];
const currentIndex = optionsData.findIndex(
(item) => item?.value === selectedValue,
);
const itemLength = optionsData.length;
switch (e.key) {
case 'ArrowUp': {
const newValue = optionsData[Math.max(0, currentIndex - 1)]?.value;
setSelectedValue(newValue as string | null);
e.preventDefault();
break;
}
case 'ArrowDown': {
const newValue =
optionsData[Math.min(itemLength - 1, currentIndex + 1)]?.value;
setSelectedValue(newValue as string | null);
e.preventDefault();
break;
}
case 'Enter':
e.preventDefault();
handleColumnSelection(currentIndex, optionsData);
break;
default:
break;
}
};
useEffect(() => {
// Scroll the selected item into view
const listNode = listRef.current;
if (listNode && selectedValue) {
const optionsData = addColumn?.options || [];
const currentIndex = optionsData.findIndex(
(item) => item?.value === selectedValue,
);
const itemNode = listNode.children[currentIndex] as HTMLElement;
if (itemNode) {
itemNode.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}
}
}, [selectedValue]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return (): void => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [selectedValue]);
return (
<div
className={cx('nested-menu-container', addNewColumn ? 'active' : '')}
className={cx(
'nested-menu-container',
showAddNewColumnContainer ? 'active' : '',
)}
onClick={(event): void => {
// this is to restrict click events to propogate to parent
event.stopPropagation();
@@ -158,8 +263,72 @@ export default function LogsFormatOptionsMenu({
</Button>
</div>
</div>
) : (
<>
) : null}
{showAddNewColumnContainer && (
<div className="add-new-column-container">
<div className="add-new-column-header">
<div className="title">
<div className="periscope-btn ghost" onClick={handleToggleAddNewColumn}>
<ChevronLeft
size={14}
className="back-icon"
onClick={handleToggleAddNewColumn}
/>
</div>
Add New Column
</div>
<Input
tabIndex={0}
type="text"
autoFocus
onFocus={addColumn?.onFocus}
onChange={handleSearchValueChange}
placeholder="Search..."
/>
</div>
<div className="add-new-column-content">
{addColumn?.isFetching && (
<div className="loading-container"> Loading ... </div>
)}
<div className="column-format-new-options" ref={listRef}>
{addColumn?.options?.map(({ label, value }, index) => (
<div
className={cx('column-name', value === selectedValue && 'selected')}
key={value}
onMouseEnter={(): void => {
if (!initialMouseEnterRef.current) {
setSelectedValue(value as string | null);
}
initialMouseEnterRef.current = true;
}}
onMouseMove={(): void => {
// this is added to handle the mouse move explicit event and not the re-rendered on mouse enter event
setSelectedValue(value as string | null);
}}
onClick={(eve): void => {
eve.stopPropagation();
handleColumnSelection(index, addColumn?.options || []);
}}
>
<div className="name">
<Tooltip placement="left" title={label}>
{label}
</Tooltip>
</div>
</div>
))}
</div>
</div>
</div>
)}
{!isFontSizeOptionsOpen && !showAddNewColumnContainer && (
<div>
<div className="font-size-container">
<div className="title">Font Size</div>
<Button
@@ -230,29 +399,10 @@ export default function LogsFormatOptionsMenu({
</>
<div className="selected-item-content-container active">
{!addNewColumn && <div className="horizontal-line" />}
{addNewColumn && (
<div className="add-new-column-header">
<div className="title">
{' '}
columns
<X size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
<Input
tabIndex={0}
type="text"
autoFocus
onFocus={addColumn?.onFocus}
onChange={handleSearchValueChange}
placeholder="Search..."
/>
</div>
)}
{!showAddNewColumnContainer && <div className="horizontal-line" />}
<div className="item-content">
{!addNewColumn && (
{!showAddNewColumnContainer && (
<div className="title">
columns
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
@@ -274,48 +424,17 @@ export default function LogsFormatOptionsMenu({
/>
</div>
))}
</div>
{addColumn?.isFetching && (
<div className="loading-container"> Loading ... </div>
)}
{addNewColumn &&
addColumn &&
addColumn.value.length > 0 &&
addColumn.options &&
addColumn?.options?.length > 0 && (
<Divider className="column-divider" />
{addColumn && addColumn?.value?.length === 0 && (
<div className="column-name no-columns-selected">
No columns selected
</div>
)}
{addNewColumn && (
<div className="column-format-new-options">
{addColumn?.options?.map(({ label, value }) => (
<div
className="column-name"
key={value}
onClick={(eve): void => {
eve.stopPropagation();
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(value, { label, disabled: false });
}
}}
>
<div className="name">
<Tooltip placement="left" title={label}>
{label}
</Tooltip>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</>
)}
</>
</div>
)}
</div>
);

View File

@@ -8,6 +8,7 @@
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
.left-action {
display: flex;

View File

@@ -396,23 +396,22 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
return (
<div className="checkbox-filter">
<section className="filter-header-checkbox">
<section
className="filter-header-checkbox"
onClick={(): void => {
if (isOpen) {
setIsOpen(false);
setVisibleItemsCount(10);
} else {
setIsOpen(true);
}
}}
>
<section className="left-action">
{isOpen ? (
<ChevronDown
size={13}
cursor="pointer"
onClick={(): void => {
setIsOpen(false);
setVisibleItemsCount(10);
}}
/>
<ChevronDown size={13} cursor="pointer" />
) : (
<ChevronRight
size={13}
onClick={(): void => setIsOpen(true)}
cursor="pointer"
/>
<ChevronRight size={13} cursor="pointer" />
)}
<Typography.Text className="title">{filter.title}</Typography.Text>
</section>
@@ -420,7 +419,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
{isOpen && (
<Typography.Text
className="clear-all"
onClick={handleClearFilterAttribute}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
handleClearFilterAttribute();
}}
>
Clear All
</Typography.Text>

View File

@@ -1,11 +1,13 @@
import { Typography } from 'antd';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
import getFormattedDate from 'lib/getFormatedDate';
import { useTimezone } from 'providers/Timezone';
function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const time = new Date(CreatedOrUpdateTime);
const date = getFormattedDate(time);
const timeString = `${date} ${convertDateToAmAndPm(time)}`;
const timeString = formatTimezoneAdjustedTimestamp(
time,
'MM/DD/YYYY hh:mm:ss A (UTC Z)',
);
return <Typography>{timeString}</Typography>;
}

View File

@@ -11,7 +11,7 @@ function RouteTab({
}: RouteTabProps & TabsProps): JSX.Element {
const onChange = (activeRoute: string): void => {
if (onChangeHandler) {
onChangeHandler();
onChangeHandler(activeRoute);
}
const selectedRoute = routes.find((e) => e.key === activeRoute);

View File

@@ -11,6 +11,6 @@ export type TabRoutes = {
export interface RouteTabProps {
routes: TabRoutes[];
activeKey: TabsProps['activeKey'];
onChangeHandler?: VoidFunction;
onChangeHandler?: (key: string) => void;
history: History<unknown>;
}

View File

@@ -21,4 +21,5 @@ export enum LOCALSTORAGE {
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
}

View File

@@ -37,4 +37,8 @@ export enum QueryParams {
partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery',
ruleType = 'ruleType',
configDetail = 'configDetail',
getStartedSource = 'getStartedSource',
getStartedSourceService = 'getStartedSourceService',
mqServiceView = 'mqServiceView',
}

View File

@@ -18,4 +18,7 @@ export const REACT_QUERY_KEY = {
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
};

View File

@@ -55,10 +55,12 @@ const ROUTES = {
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',
WORKSPACE_LOCKED: '/workspace-locked',
WORKSPACE_SUSPENDED: '/workspace-suspended',
SHORTCUTS: '/shortcuts',
INTEGRATIONS: '/integrations',
MESSAGING_QUEUES: '/messaging-queues',
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
} as const;
export default ROUTES;

View File

@@ -0,0 +1,3 @@
export const TimezonePickerShortcuts = {
CloseTimezonePicker: 'escape',
};

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