Compare commits

...

107 Commits

Author SHA1 Message Date
nityanandagohain
c99a112dc7 fix: set default only if not present 2024-03-30 20:02:03 +05:30
nityanandagohain
e4d9c4e239 fix: error check 2024-03-30 19:39:52 +05:30
nityanandagohain
781732f25a Merge remote-tracking branch 'origin/develop' into issue_4777 2024-03-30 19:22:20 +05:30
nityanandagohain
77e55a0ec9 feat: allow query restrictions for log queries 2024-03-30 18:41:53 +05:30
Nityananda Gohain
a34c59762b feat: allow characters in attribute names (#4775) 2024-03-30 17:57:01 +05:30
Nityananda Gohain
397da5857f fix: enrich all queries with non materialized attributes (#4772) 2024-03-30 08:55:46 +05:30
Vikrant Gupta
43ceb052d8 feat: do not retry query range API's with i/o timeout error (#4768)
* feat: do not retry query range API's with i/o timeout error

* feat: do not retry query range API's with i/o timeout error
2024-03-29 16:00:22 +05:30
Yunus M
6eced60bf5 feat: update time range selection flows to handle relative and absolu… (#4742)
* feat: update time range selection flows to handle relative and absolute times

* fix: lint error

* fix: lint error

* feat: update logic to handle custom relative times on load and standardize relative time formats

* fix: type issue

* fix: handle light mode and on custom time range select

* chore: update alert frequency corresponding times

* chore: update copy URL

* feat: update styles
2024-03-29 14:53:48 +05:30
Rajat Dabade
7c2f5352d2 [Refactor]: Table Grid Formula issue. (#4758)
* refactor: change the logic to match data from another query

* refactor: updated logic

* refactor: clean up

* refactor: updated case to handle formula

* chore: nit

* refactor: isEqual instead of nested loops

* chore: added comments

* refactor: updated logic

* refactor: clean up

* refactor: updated case to handle formula

* chore: nit

* refactor: isEqual instead of nested loops
2024-03-29 14:41:16 +05:30
Vikrant Gupta
e6e377beff fix: billing graph page crash (#4764) 2024-03-29 11:08:33 +05:30
Prashant Shahi
6da9de6591 Merge pull request #4588 from SigNoz/chore/send-language-service-as-list
chore: send language and service name events as list
2024-03-28 22:21:55 +05:45
Vishal Sharma
7549aee656 Merge branch 'develop' into chore/send-language-service-as-list 2024-03-28 21:54:16 +05:30
Vishal Sharma
da4a6266c5 feat: add events API (#4761) 2024-03-28 21:43:41 +05:30
Vishal Sharma
6ac938f2a6 Merge branch 'develop' into chore/send-language-service-as-list 2024-03-28 21:40:57 +05:30
Raj Kamal Singh
990fc83269 Feat/integrations v0 mongo and nginx (#4763)
* feat: flesh out pre-requisites for collecting mongodb logs and metrics

* chore: remove stale pipelines in bundled integrations

* chore: clean up 'collect metrics' step for mongodb

* feat: add instructions for collecting and parsing mongodb logs

* feat: add metrics and logs attributes to mongodb data collected list

* feat: nginx logs collection instructions and some other cleanup

* feat: add list of parsed log attributes to data collected list for nginx

* chore: do not run pipeline population integration test if no built-in integration has a pipeline
2024-03-28 19:57:07 +05:30
Yunus M
5d5ff47d5e fix: update devtool property to eval-source-map (#4760) 2024-03-28 16:58:35 +05:30
Yunus M
9f30bba9a8 feat: add support to pin attributes in logs details view (#4692)
* feat: add support to pin attributes in logs details view

* feat: add safety checks

* feat: update styles

* feat: update styles

* feat: move json parsing in try catch block
2024-03-28 16:55:59 +05:30
Yunus M
6014bb76b6 feat: support drag select in chart - alerts page (#4618)
* feat: support drag select in chart - alerts page

* feat: handle back navigation after drag select
2024-03-28 16:51:29 +05:30
Vikrant Gupta
e25b54f86a fix: 404 resource not found issues (#4757) 2024-03-28 16:46:16 +05:30
Vikrant Gupta
5959963b9d fix: [SIG-575]: no data in new trace explorer page specific scenario (#4748)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2024-03-28 16:34:09 +05:30
Srikanth Chekuri
31b1d58a70 chore: fix alerting options (#4752) 2024-03-27 20:25:18 +05:30
Raj Kamal Singh
0ac9f6f663 Feat: QS: redis integration v0: instructions for collecting and parsing logs (#4753)
* chore: minor cleanups to postgres integration instructions

* chore: update instructions for connecting redis integration

* feat: add instructions for collecting redis logs

* chore: flesh out prerequisites for connecting redis integration

* chore: add list of metrics collected for redis
2024-03-27 20:03:27 +05:30
Yunus M
a30b75a2a8 feat: show environments in a separate dropdown (#4717)
* feat: show environments in a separate dropdown
2024-03-27 18:46:05 +05:30
SagarRajput-7
dbd4363ff8 feat: [SIG-573]: Fixed billing page issues (#4744)
* feat: [SIG-573]: Fixed billing page issues

* feat: [SIG-573]: Fixed jest test case
2024-03-27 11:55:28 +05:30
SagarRajput-7
ad1b01f225 feat: [SIG-566]: Added message to alert user about their past due - subscription status (#4724)
* feat: [SIG-566]: Added message to alert user about their past due - subscription status

* feat: [SIG-566]: Added message string to billings.json

* feat: [SIG-566]: Added strings to billings.json

* feat: [SIG-566]: updated test cases

* feat: [SIG-566]: updated message text

* feat: [SIG-566]: code fix

* feat: [SIG-566]: code fix
2024-03-27 10:23:57 +05:30
Vikrant Gupta
e1679790f7 fix: log chips not forming making filtering not work (#4749)
* fix: log chips not forming making filtering not work

* fix: remove console log
2024-03-27 01:01:24 +05:30
Srikanth Chekuri
ae594061e9 chore: fix query-service logging (#4696) 2024-03-27 00:07:29 +05:30
Vikrant Gupta
9e02147d4c fix: [SIG-574]: support __ in the groupBy clause (#4747) 2024-03-26 23:54:31 +05:30
Tan Wei Been
2b3d1c8ee5 [Fix]: Using exported dashboards as input to dashboard provisioning #2 (#4726)
* fix(be,fe): upsert dashboard on provision, export with uuid from frontend

* chore(fe): formatting in dashboard description

* fix: miss out while merging

---------

Co-authored-by: Håvard <haavard.markhus@inmeta.no>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
Co-authored-by: Haavasma <61970295+Haavasma@users.noreply.github.com>
2024-03-26 17:09:59 +05:30
Rajat Dabade
4c91dbcff0 Explorer Toolbar maximised and minimised (#4721) 2024-03-26 17:09:13 +05:30
Srikanth Chekuri
83f68f13db feat: add ability to customize alert frequency (#4697) 2024-03-26 12:40:53 +05:30
Vibhu Pandey
994814864c fix: send 403 on wrong password entry during change password operation (#4733) 2024-03-26 06:20:35 +05:30
Raj Kamal Singh
f24135f5b0 Feat: QS: postgres integration: instructions for collecting and parsing logs (#4738)
* chore: offer metrics config instructions for signoz cloud only

* chore: some more cleanups

* chore: get log collection instructions started

* feat: flesh out log collection otel config for postgres

* chore: some cleanup

* chore: some more cleanup

* chore: some more cleanup
2024-03-23 11:39:28 +05:30
Vikrant Gupta
5745727031 fix: [SIG-565]: design feedback for integrations (#4723)
* fix: [SIG-565]: design feedback for integrations

* feat: added dotted line in the test connection modal

* feat: handle the URL change for integration details page to support back navigation

* feat: added ghost loading states

* feat: added margin for details header

* feat: added margin for details header

* feat: increase the list sizes to 20

* fix: handle icons

* fix: remove unused classes
2024-03-22 14:59:43 +05:30
Vikrant Gupta
ae0d685b29 feat: [SIG-572]: allow number of lines changing in the logs list view (#4737)
* feat: [SIG-572]: allow number of lines changing in the logs list view

* feat: [SIG-572]: allow number of lines changing in the logs list view

* feat: added options to change row values in table view

* fix: build issues
2024-03-22 13:40:55 +05:30
Vikrant Gupta
f34a49e19c fix: [SIG-570]: handle case where - being present in the key (#4735) 2024-03-22 13:40:43 +05:30
Vikrant Gupta
9e557a0ebe feat: [SIG-571]: added support for has and nhas operator for json filter (#4736)
* feat: [SIG-571]: added support for has and nhas operator for json filter

* fix: address review comments

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2024-03-22 13:39:47 +05:30
Vikrant Gupta
0df3c26f04 feat: implement download logs feature for logs explorer new design (#4728)
* feat: implement download logs feature for logs explorer new design

* feat: address review comments

* feat: added timestamp and body to the start

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2024-03-22 13:28:38 +05:30
SagarRajput-7
0df86454ce fix: [SIG-567]: prevented stage-&-run API on legend change (#4720)
* fix: prevented stage-&-run API on legend change

* fix: code refactor

---------

Co-authored-by: Sagar Rajput <sagarrajput@192.168.1.2>
2024-03-21 16:31:59 +05:30
Vishal Sharma
63f0ae1c7c chore: update events (#4725)
* chore: update events

* chore: disable TELEMETRY_EVENT_QUERY_RANGE_API for saas

* chore: don't use mustCompile as it can cause panics
2024-03-20 19:59:28 +05:30
CheetoDa
d9f232683d Merge pull request #4688 from SigNoz/php-onboarding-docs
feat:php flow
2024-03-20 09:06:34 +05:30
CheetoDa
ad9d77d33f feat:php flow 2024-03-19 17:04:11 +05:30
CheetoDa
5a8479f4e9 feat:php flow 2024-03-19 17:04:11 +05:30
Raj Kamal Singh
f4e94c0ad1 chore: update postgres config instructions to work for both signoz cloud and self-hosted (#4718) 2024-03-19 12:28:22 +05:30
Raj Kamal Singh
6f3183823f Feat: postgres integration v0 (#4704)
* chore: update annotations for pre blocks in configuration instructions

* chore: update list of collected metrics for postgres integration

* chore: change non-string units to string in metrics collected list

* chore: some cleanups for postgres config instructions

* chore: some cleanup to metrics connection status resource labels

* chore: remove stub pipeline in postgres integration - no interesting log parsing to be done
2024-03-18 18:20:12 +05:30
Raj Kamal Singh
01bb39da6a chore: some cleanups in plumbing for integration connection status (#4716) 2024-03-18 15:22:31 +05:30
Raj Kamal Singh
43f9830e8d Feat: integrations v0 metrics connection status (#4715)
* chore: add test expectations for integration metrics connection status

* chore: reorg logs connection status calculation for parallelization

* chore: add interface for reader.GetMetricLastReceivedTsMillis

* chore: add plumbing for calculating integration metrics connection status

* chore: impl and test mocks for reader.GetMetricReceivedLatest

* chore: wrap things up and get test passing

* chore: some cleanup

* chore: some more cleanup

* chore: use prom metric names for integration connection test
2024-03-18 10:01:53 +05:30
Yunus M
4c2174958f chore: remove share invite link message (#4691) 2024-03-15 13:38:43 +05:30
Yunus M
07747e73d6 fix: context filter input overflow issue, min height for logs list view (#4710) 2024-03-15 13:25:06 +05:30
Vikrant Gupta
60946b5e9d feat: remove disabled in case of dashboard locked (#4709) 2024-03-15 12:28:03 +05:30
SagarRajput-7
0365fa5421 feat: handled inactive tab handling by removing the display flex override (#4708)
Co-authored-by: Sagar Rajput <sagarrajput@192.168.1.2>
2024-03-15 12:19:07 +05:30
Vikrant Gupta
cf22039562 Revert "Explorer Toolbar maximised and minimised (#4656)" (#4705)
This reverts commit aadb962b6c.
2024-03-15 01:26:31 +05:30
Vikrant Gupta
2a62982885 feat: support case insensitive operators (#4379) 2024-03-14 13:33:35 +05:30
Vikrant Gupta
1e1624ed4c fix: [GH-3932]: do not retry API's in case of 4XX status code (#4376)
* fix: [GH-3932]: do not retry API's in case of 400 status code

* feat: do not retry 4XX response status
2024-03-14 12:07:47 +05:30
SagarRajput-7
6b3af78873 Merge pull request #4689 from SagarRajput-7/billing-graph
feat: added isFetching condition and changed series color
2024-03-13 17:58:48 +05:30
Sagar Rajput
6adeef7e70 feat: added isFetching condition and changed series color 2024-03-13 17:50:17 +05:30
Rajat Dabade
44dc55c5ac FE: Design update for log context tab (#4601)
* refactor: initial setup for context view

* refactor: updated design for log context view

* refactor: updated comments and remove commented code

* refactor: updated comments

* refactor: handle hight issue

* refactor: initial setup for context view

* refactor: updated design for log context view

* refactor: updated comments and remove commented code

* refactor: updated comments

* refactor: handle hight issue

* refactor: added api version

* refactor: height set to parent height and remove unnessarry code

* refactor: removed commented code

---------

Co-authored-by: Yunus M <myounis.ar@live.com>
2024-03-13 17:37:48 +05:30
Srikanth Chekuri
3c419677e1 feat: add support for alerting on absent metric (#3245) 2024-03-13 17:37:32 +05:30
Rajat Dabade
aadb962b6c Explorer Toolbar maximised and minimised (#4656)
* refactor: done log explorer minimising save view toolbar

* refactor: local storage visibility support and done with traces toolbar

* refactor: added toolbar and removed commented code

* chore: css updates

* refactor: removed the background hightlighted code for droppable area

* refactor: merge conflict resolve and updated variable name
2024-03-13 17:28:09 +05:30
SagarRajput-7
c6080ca02e feat: [SIG-557]: added Billing usage graph - daily and weekly (#4686)
* feat: added Billing usage graph - daily and weekly

* feat: removed mocked response

* feat: removed weekly chart and fixed data transformations

* feat: added loading states

* feat: moved function to util file

* feat: fixed review comments

* feat: fixed JEST test case

* feat: test fix - commit

* feat: test fix - commit

* feat: test fix - commit

* feat: edited title conditionally

* feat: edited tooltip content

* feat: removed time from tooltip content and skeleton for cycleInfo Alert

---------

Co-authored-by: Sagar Rajput <sagarrajput@192.168.1.2>
2024-03-13 14:30:49 +05:30
Vishal Sharma
506448fe61 chore: dashboard info update (#4684) 2024-03-13 01:47:51 +05:30
Srikanth Chekuri
a42176599f chore: add day wise usage breakdown (#4648) 2024-03-12 22:45:31 +05:30
Srikanth Chekuri
adef0a4138 chore: add LogCommentEnricher middleware (#4681) 2024-03-12 18:39:28 +05:30
Srikanth Chekuri
c9816cce18 fix: use same time window as the logs/traces query (#4682) 2024-03-12 17:30:01 +05:30
Srikanth Chekuri
c6c2b9d809 chore: limit number of top level operations in services list (#4666) 2024-03-12 17:22:48 +05:30
Rajat Dabade
d9b379ae51 refactor: onblur convert to tag (#4662)
* refactor: onblur convert to tag

* refactor: on blur log body contains

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-03-12 14:34:10 +05:30
Rajat Dabade
dd2afe19f6 [Fix]: full view bar chart (#4667) 2024-03-12 14:22:27 +05:30
Rajat Dabade
0326a4d42a [Refactor]: Height for log explorer list panel (#4657)
* refactor: added fullview and fix height issue for logs

* refactor: hide full view button on full view mode

* refactor: removed fullview for logs

* refactor: remove max with for body
2024-03-12 12:20:45 +05:30
Srikanth Chekuri
b4d12966f3 fix: interpret missing point as zero value in formula evaluation (#4668) 2024-03-11 23:47:19 +05:30
Raj Kamal Singh
5a2d729ba9 Feat: qs autopopulate installed integration dashboards (#4680)
* chore: add test for dashboards for installed integrations

* feat: include dashboards for installed integrations in API response

* chore: add test expectation for getting installed integration dashboard by id

* feat: add support for retrieving installed integration dashboards by id

* chore: add dashboard id validation for integrations
2024-03-11 20:06:59 +05:30
Yunus M
666916fae2 fix: handle defaults for apm and aws (#4678) 2024-03-11 16:59:04 +05:30
Vishal Sharma
4b4008642d chore: extract dashboard/alert query info and send event (#4665)
* chore: extract dashboard/alert query info and send event

* chore: add totalDashboardsWithPanelAndName attribute in event
2024-03-11 16:45:06 +05:30
Srikanth Chekuri
7c2007faa3 fix: remove early return for services call in usage explorer (#4669) 2024-03-11 15:20:59 +05:30
Vikrant Gupta
6b87118fc6 feat: [SIG-546]: user with viewer roles can only view saved views (#4663)
* feat: [SIG-543]: Users with VIEWER access can create/edit/delete views for logs and traces

* feat: [SIG-543]: remove extra code

* feat: [SIG-543]: role changes in the save views toolbar

* feat: [SIG-543]: role changes in the save views toolbar

* feat: remove the save feature / dashboard / alert feature for viewer roles

* feat: remove the save feature / dashboard / alert feature for viewer roles

* fix: address review comments
2024-03-11 14:49:10 +05:30
Vikrant Gupta
49aba4fb1c feat: [SIG-543]: add time selection in the custom date selection (#4658)
* feat: [SIG-543]: inital commit

* feat: [SIG-543]: refactor date time modal to separate component

* feat: [SIG-543]: refactor date time modal to separate component

* feat: add back the time support according to the older designs in the date time picker

* fix: custom time picker minor UI fixes
2024-03-11 14:39:17 +05:30
Raj Kamal Singh
9ace374855 Feat: QS: Log Pipelines for installed integrations (#4674)
* chore: refactor: inject sqlx.DB into opamp.initDB instead of DB file name

* chore: reorganize test utils a little

* chore: add test validating pipelines for installed integrations show up in pipelines list

* chore: get basic integration pipelines testcase passing

* chore: reconcile experimental changes with latest state of develop

* chore: add integration test for reordering of pipelines

* chore: marker for integration pipelines using Id

* chore: hookup propagation of installed integration pipelines by opamp

* chore: add util for mapping slices

* chore: add support for reordering integration pipelines

* chore: exclude user saved integration pipelines if no longer installed

* chore: flesh out rest of intgeration pipelines scenarios

* chore: handle scenario when an integration is installed before any pipelines exist

* chore: notify agentConf of update after uninstalling an integration

* chore: some minor cleanup

* chore: some more cleanup

* chore: update ee server for changed controllers

* chore: some more cleanup

* chore: change builtin integration id prefix to avoid using colons that break yaml

* chore: update builtin integration id in test
2024-03-11 14:15:11 +05:30
Vikrant Gupta
a4d5774ae3 fix: update the md file for integrations config (#4677) 2024-03-11 11:48:36 +05:30
Nityananda Gohain
d0d10daa44 feat: support for timeshift in logs (#4607)
* feat: support for timeshift in logs

* fix: post process the timeshift function result

* fix: start and end times adjusted

* fix: only apply functions instead of entire post process

* fix: unnecessary error handling removed

* fix: apply functions for all sources

* feat: test added for timeshift

* fix: comments corrected

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-03-08 21:12:53 +05:30
Rajat Dabade
e519539468 Graph visibility state management. (#4632)
* refactor: change the state handling of graph visibility

* refactor: removed commented code
2024-03-08 14:11:38 +05:30
Vikrant Gupta
7051831539 fix: [SIG-542]: handle special characters in servicename (#4649)
* fix: [SIG-542]: handle special characters in servicename

* feat: handle . operator in the URL

* chore: left files to update
2024-03-08 00:08:12 +05:30
Raj Kamal Singh
c842e68288 chore: minor integrations UI cleanups (#4661) 2024-03-07 19:57:14 +05:30
Raj Kamal Singh
a295bf2fb6 Feat: QS: structure for built in integrations (#4655)
* feat: get builtin integrations started with nginx

* feat: get started with embedding and parsing of builtin integrations

* chore: add icons for nginx and redis integrations

* chore: stash current state of work

* chore: remove all yaml annotations since moved to JSON assets for bundled integrations

* chore: add file uri hydration in integration spec

* chore: refactor file uri hydration logic

* chore: add support for referencing JSON files with file uri

* chore: bring in initial integration assets

* chore: hookup builtin integrations and get all tests passing

* chore: update icons for postgres and mongo and some cleanup

* chore: some more cleanup

---------

Co-authored-by: Raj Singh <raj@Rajs-MacBook-Pro.local>
2024-03-07 19:26:20 +05:30
Vikrant Gupta
4cd40391c5 feat: route to the onboarding flow when clicking the sending logs in case of no logs (#4600)
* feat: refactor onboarding flow to add path params when selecting any module

* feat: added re-route to the onboarding flow in case of no logs and no traces

* chore: remove console logs

* chore: increase type safety

* chore: updated tab names

* chore: remove development conditions

* chore: handle cloud user

* feat: handle aws monitoring cases

* fix: apm framework not getting selected

* fix: apm framework not getting selected

* fix: apm framework not getting selected
2024-03-07 14:23:28 +05:30
Vikrant Gupta
7af4ba34af chore: [SIG-526]: Improve the light theme designs for integrations UI (#4659) 2024-03-07 14:12:06 +05:30
Rajat Dabade
54c69311ed Logs Strip color according to severity_text (#4643)
* refactor: initial setup

* refactor: done with setup

* refactor: done with severity text split color

* refactor: initial setup

* refactor: done with setup

* refactor: done with severity text split color

* chore: added unit test case

* refactor
: pointed to the correct variable

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2024-03-07 12:25:00 +05:30
Vikrant Gupta
62af836554 fix: [SIG-549]: bring the ts config for pre-commit hook in sync with github CI (#4660)
* chore: dummy commit

* chore: dummy commit

* chore: dummy commit
2024-03-07 11:53:05 +05:30
Yunus M
f9b3ca01f9 feat: add clone query functionality (#4617)
* feat: add clone query functionality

* feat: update ui
2024-03-07 11:41:29 +05:30
Vikrant Gupta
0c4149225f feat: [SIG-526]: UI Integrations V0 (#4595)
* feat: integrations v0 base setup routes and components

* chore: typecheck fix

* feat: integrations landing page changes

* feat: initial header setup

* feat: integrations list page setup

* feat: integrations details content root setup

* feat: integration detail content setup

* feat: added overview tab

* feat: added data tab

* feat: handle configuration tab

* feat: add min height for the container

* feat: generate apis and hooks for usage

* feat: added remove integration modal

* feat: added remove integration modal

* feat: added remove integration modal

* feat: added test connection bars

* chore: add bottom margins

* feat: added test connection modal

* feat: add all types of test connection

* feat: add all types of test connection

* fix: address review comments

* fix: address review comments

* feat: added get all integrations API and search bar implemnetation

* feat: navigate to overview section in case of row click and configure in btn

* feat: integrate get integration details api

* feat: handle integration details page gracefully

* feat: integrate uninstall API and the connection states

* feat: add install integration API call

* feat: added api error handling

* feat: handle error states for list and details api

* feat: handle the logs and metrics columns

* feat: add TODOs for pending tasks

* feat: comment from side nav

* feat: added support for custom tags in react markdown

* chore: revert the temporary change for merge

* feat: integrate the status api calls and polling logic

* chore: add markdown components and correct the polling issue

* chore: handle light mode

* chore: remove integrations from sideNav

* fix: address review comments

* fix: address review comments
2024-03-06 22:25:02 +05:30
Raj Kamal Singh
7136ecc2fe chore: accept connection test lookback seconds as request param (#4650)
Also removes connectionStatus from integration details.
2024-03-06 11:06:04 +05:30
Vikrant Gupta
0c14145ef9 fix: [SIG-532]: Copy Log Link Functionality for new designs (#4644)
* fix: [SIG-532]: timeRange not updating correctly for copy log link

* fix: [SIG-532]: use virtuoso props to scroll to some intiial position rather than API hit

* fix: added styles for highlighted colors

* fix: handle colors for copy log link

* fix: update colors for copy log lines
2024-03-05 21:31:15 +05:30
Srikanth Chekuri
6618b47123 chore: bump github.com/SigNoz/prometheus to v1.9.79-0.1 (#4651) 2024-03-05 21:04:35 +05:30
Raj Kamal Singh
ab5285dee6 Feat: qs api integration connection status (#4628)
* chore: add integration attribs for connection tests and status

* chore: add connection status to integration details response

* chore: update integration lifecycle test to check for connection status too

* feat: add GetIntegrationConnectionTests to integrations manager and controller

* chore: add tests for querying integration connection status

* feat: add http API support for integration connection status

* chore: some cleanups

* chore: use PostableRule for integration alerts

* chore: some more cleanup
2024-03-05 15:23:56 +05:30
Vikrant Gupta
fdd7e022e9 fix: offset added hinders the timestamps for the traces (#4642)
* fix: offset added hinders the timestamps for the traces

* fix: offset added hinders the timestamps for the traces
2024-03-04 18:47:38 +05:30
Rajat Dabade
90d7f0200a FE: updated routes and modal for create alert css (#4602)
* refactor: updated routes and modal for create alert css

* refactor: button color changed

* refactor: redirect old routes to new routes
2024-03-04 17:42:17 +05:30
Vikrant Gupta
2713e186d3 fix: dashboard variables reset on tab visibility change (#4619) 2024-03-04 13:54:55 +05:30
Raj Kamal Singh
ffdb4cfff0 chore: update version for clickhouse mock dependency (#4640) 2024-03-04 12:00:05 +05:30
Srikanth Chekuri
b3b7522250 chore: update APM metrics to use v4 query range (#4638) 2024-03-04 10:15:43 +05:30
Raj Kamal Singh
0870030d1c Feat: qs integrations http api (#4622)
* chore: add http api test for signoz integrations

* chore: add controller for integrations

* chore: add http API handlers for integrations API

* chore: hook up integrations API in new servers

* chore: add remaining fields in Integration DTO

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-03-02 10:11:51 +05:30
Raj Kamal Singh
3fece44aef chore: update dependency github.com/srikanthccv/ClickHouse-go-mock from v0.4.0 -> v0.6.0 (#4637) 2024-03-01 15:55:15 +05:30
Yunus M
e5de35a769 fix: default version to v4 if version param not present in url params (#4636) 2024-03-01 15:37:55 +05:30
Yunus M
44ff1517d1 fix: update date picker styles (#4635) 2024-03-01 15:20:21 +05:30
Srikanth Chekuri
d77389abe3 feat: add support for email alert channel (#4599) 2024-03-01 15:05:28 +05:30
Yunus M
1a62a13aea chore: update query builder to support spatial aggregations and functions (#4569) 2024-03-01 14:51:50 +05:30
Rajat Dabade
97fdba0fae Update panel type on selecting new panel on dashboards. (#4634) 2024-03-01 13:04:53 +05:30
Vishal Sharma
5c2a9e8362 Merge branch 'develop' into chore/send-language-service-as-list 2024-02-29 15:58:24 +05:30
Nityananda Gohain
1aaafa4638 feat: use attribute if present for json query (#4458)
* feat: use attribute if present for json query

* fix: refractor test cases
2024-02-29 15:32:37 +05:30
makeavish
aad840da59 chore: send language and service name events as list 2024-02-23 14:08:17 +05:30
479 changed files with 21537 additions and 3344 deletions

View File

@@ -133,7 +133,7 @@ services:
# - ./data/clickhouse-3/:/var/lib/clickhouse/
alertmanager:
image: signoz/alertmanager:0.23.4
image: signoz/alertmanager:0.23.5
volumes:
- ./data/alertmanager:/data
command:

View File

@@ -54,7 +54,7 @@ services:
alertmanager:
container_name: signoz-alertmanager
image: signoz/alertmanager:0.23.4
image: signoz/alertmanager:0.23.5
volumes:
- ./data/alertmanager:/data
depends_on:

View File

@@ -149,7 +149,7 @@ services:
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.4}
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
container_name: signoz-alertmanager
volumes:
- ./data/alertmanager:/data

View File

@@ -10,6 +10,7 @@ import (
"go.signoz.io/signoz/ee/query-service/license"
"go.signoz.io/signoz/ee/query-service/usage"
baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/integrations"
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/cache"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
@@ -31,6 +32,7 @@ type APIHandlerOptions struct {
UsageManager *usage.Manager
FeatureFlags baseint.FeatureLookup
LicenseManager *license.Manager
IntegrationsController *integrations.Controller
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
Cache cache.Cache
// Querier Influx Interval
@@ -56,6 +58,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
AppDao: opts.AppDao,
RuleManager: opts.RulesManager,
FeatureFlags: opts.FeatureFlags,
IntegrationsController: opts.IntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
Cache: opts.Cache,
FluxInterval: opts.FluxInterval,

View File

@@ -74,7 +74,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
requestBody, err := io.ReadAll(r.Body)
if err != nil {
zap.S().Errorf("received no input in api\n", err)
zap.L().Error("received no input in api", zap.Error(err))
RespondError(w, model.BadRequest(err), nil)
return
}
@@ -82,7 +82,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(requestBody, &req)
if err != nil {
zap.S().Errorf("received invalid user registration request", zap.Error(err))
zap.L().Error("received invalid user registration request", zap.Error(err))
RespondError(w, model.BadRequest(fmt.Errorf("failed to register user")), nil)
return
}
@@ -90,13 +90,13 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
// get invite object
invite, err := baseauth.ValidateInvite(ctx, req)
if err != nil {
zap.S().Errorf("failed to validate invite token", err)
zap.L().Error("failed to validate invite token", zap.Error(err))
RespondError(w, model.BadRequest(err), nil)
return
}
if invite == nil {
zap.S().Errorf("failed to validate invite token: it is either empty or invalid", err)
zap.L().Error("failed to validate invite token: it is either empty or invalid", zap.Error(err))
RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil)
return
}
@@ -104,7 +104,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
// get auth domain from email domain
domain, apierr := ah.AppDao().GetDomainByEmail(ctx, invite.Email)
if apierr != nil {
zap.S().Errorf("failed to get domain from email", apierr)
zap.L().Error("failed to get domain from email", zap.Error(apierr))
RespondError(w, model.InternalError(basemodel.ErrSignupFailed{}), nil)
}
@@ -205,24 +205,24 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
ctx := context.Background()
if !ah.CheckFeature(model.SSO) {
zap.S().Errorf("[receiveGoogleAuth] sso requested but feature unavailable %s in org domain %s", model.SSO)
zap.L().Error("[receiveGoogleAuth] sso requested but feature unavailable in org domain")
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return
}
q := r.URL.Query()
if errType := q.Get("error"); errType != "" {
zap.S().Errorf("[receiveGoogleAuth] failed to login with google auth", q.Get("error_description"))
zap.L().Error("[receiveGoogleAuth] failed to login with google auth", zap.String("error", errType), zap.String("error_description", q.Get("error_description")))
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "failed to login through SSO "), http.StatusMovedPermanently)
return
}
relayState := q.Get("state")
zap.S().Debug("[receiveGoogleAuth] relay state received", zap.String("state", relayState))
zap.L().Debug("[receiveGoogleAuth] relay state received", zap.String("state", relayState))
parsedState, err := url.Parse(relayState)
if err != nil || relayState == "" {
zap.S().Errorf("[receiveGoogleAuth] failed to process response - invalid response from IDP", err, r)
zap.L().Error("[receiveGoogleAuth] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
handleSsoError(w, r, redirectUri)
return
}
@@ -244,14 +244,14 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
identity, err := callbackHandler.HandleCallback(r)
if err != nil {
zap.S().Errorf("[receiveGoogleAuth] failed to process HandleCallback ", domain.String(), zap.Error(err))
zap.L().Error("[receiveGoogleAuth] failed to process HandleCallback ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
return
}
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email)
if err != nil {
zap.S().Errorf("[receiveGoogleAuth] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err))
zap.L().Error("[receiveGoogleAuth] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
return
}
@@ -266,14 +266,14 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
if !ah.CheckFeature(model.SSO) {
zap.S().Errorf("[receiveSAML] sso requested but feature unavailable %s in org domain %s", model.SSO)
zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
return
}
err := r.ParseForm()
if err != nil {
zap.S().Errorf("[receiveSAML] failed to process response - invalid response from IDP", err, r)
zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
handleSsoError(w, r, redirectUri)
return
}
@@ -281,11 +281,11 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
// the relay state is sent when a login request is submitted to
// Idp.
relayState := r.FormValue("RelayState")
zap.S().Debug("[receiveML] relay state", zap.String("relayState", relayState))
zap.L().Debug("[receiveML] relay state", zap.String("relayState", relayState))
parsedState, err := url.Parse(relayState)
if err != nil || relayState == "" {
zap.S().Errorf("[receiveSAML] failed to process response - invalid response from IDP", err, r)
zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
handleSsoError(w, r, redirectUri)
return
}
@@ -302,34 +302,34 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
sp, err := domain.PrepareSamlRequest(parsedState)
if err != nil {
zap.S().Errorf("[receiveSAML] failed to prepare saml request for domain (%s): %v", domain.String(), err)
zap.L().Error("[receiveSAML] failed to prepare saml request for domain", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
return
}
assertionInfo, err := sp.RetrieveAssertionInfo(r.FormValue("SAMLResponse"))
if err != nil {
zap.S().Errorf("[receiveSAML] failed to retrieve assertion info from saml response for organization (%s): %v", domain.String(), err)
zap.L().Error("[receiveSAML] failed to retrieve assertion info from saml response", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
return
}
if assertionInfo.WarningInfo.InvalidTime {
zap.S().Errorf("[receiveSAML] expired saml response for organization (%s): %v", domain.String(), err)
zap.L().Error("[receiveSAML] expired saml response", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
return
}
email := assertionInfo.NameID
if email == "" {
zap.S().Errorf("[receiveSAML] invalid email in the SSO response (%s)", domain.String())
zap.L().Error("[receiveSAML] invalid email in the SSO response", zap.String("domain", domain.String()))
handleSsoError(w, r, redirectUri)
return
}
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email)
if err != nil {
zap.S().Errorf("[receiveSAML] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err))
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
handleSsoError(w, r, redirectUri)
return
}

View File

@@ -12,6 +12,20 @@ import (
"go.uber.org/zap"
)
type DayWiseBreakdown struct {
Type string `json:"type"`
Breakdown []DayWiseData `json:"breakdown"`
}
type DayWiseData struct {
Timestamp int64 `json:"timestamp"`
Count float64 `json:"count"`
Size float64 `json:"size"`
UnitPrice float64 `json:"unitPrice"`
Quantity float64 `json:"quantity"`
Total float64 `json:"total"`
}
type tierBreakdown struct {
UnitPrice float64 `json:"unitPrice"`
Quantity float64 `json:"quantity"`
@@ -21,9 +35,10 @@ type tierBreakdown struct {
}
type usageResponse struct {
Type string `json:"type"`
Unit string `json:"unit"`
Tiers []tierBreakdown `json:"tiers"`
Type string `json:"type"`
Unit string `json:"unit"`
Tiers []tierBreakdown `json:"tiers"`
DayWiseBreakdown DayWiseBreakdown `json:"dayWiseBreakdown"`
}
type details struct {
@@ -176,7 +191,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
url := fmt.Sprintf("%s/trial?licenseKey=%s", constants.LicenseSignozIo, currentActiveLicenseKey)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
zap.S().Error("Error while creating request for trial details", err)
zap.L().Error("Error while creating request for trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details
// to avoid blocking the UI
ah.Respond(w, resp)
@@ -185,7 +200,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey)
trialResp, err := hClient.Do(req)
if err != nil {
zap.S().Error("Error while fetching trial details", err)
zap.L().Error("Error while fetching trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details
// to avoid incorrectly blocking the UI
ah.Respond(w, resp)
@@ -196,7 +211,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
trialRespBody, err := io.ReadAll(trialResp.Body)
if err != nil || trialResp.StatusCode != http.StatusOK {
zap.S().Error("Error while fetching trial details", err)
zap.L().Error("Error while fetching trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details
// to avoid incorrectly blocking the UI
ah.Respond(w, resp)
@@ -207,7 +222,7 @@ func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
var trialRespData model.SubscriptionServerResp
if err := json.Unmarshal(trialRespBody, &trialRespData); err != nil {
zap.S().Error("Error while decoding trial details", err)
zap.L().Error("Error while decoding trial details", zap.Error(err))
// If there is an error in fetching trial details, we will still return the license details
// to avoid incorrectly blocking the UI
ah.Respond(w, resp)

View File

@@ -18,14 +18,14 @@ import (
func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) {
if !ah.CheckFeature(basemodel.CustomMetricsFunction) {
zap.S().Info("CustomMetricsFunction feature is not enabled in this plan")
zap.L().Info("CustomMetricsFunction feature is not enabled in this plan")
ah.APIHandler.QueryRangeMetricsV2(w, r)
return
}
metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r)
if apiErrorObj != nil {
zap.S().Errorf(apiErrorObj.Err.Error())
zap.L().Error("Error in parsing metric query params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil)
return
}

View File

@@ -43,8 +43,8 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
return
}
pat := model.PAT{
Name: req.Name,
Role: req.Role,
Name: req.Name,
Role: req.Role,
ExpiresAt: req.ExpiresInDays,
}
err = validatePATRequest(pat)
@@ -65,7 +65,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
pat.ExpiresAt = time.Now().Unix() + (pat.ExpiresAt * 24 * 60 * 60)
}
zap.S().Debugf("Got Create PAT request: %+v", pat)
zap.L().Info("Got Create PAT request", zap.Any("pat", pat))
var apierr basemodel.BaseApiError
if pat, apierr = ah.AppDao().CreatePAT(ctx, pat); apierr != nil {
RespondError(w, apierr, nil)
@@ -115,7 +115,7 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
req.UpdatedByUserID = user.Id
id := mux.Vars(r)["id"]
req.UpdatedAt = time.Now().Unix()
zap.S().Debugf("Got Update PAT request: %+v", req)
zap.L().Info("Got Update PAT request", zap.Any("pat", req))
var apierr basemodel.BaseApiError
if apierr = ah.AppDao().UpdatePAT(ctx, req, id); apierr != nil {
RespondError(w, apierr, nil)
@@ -135,7 +135,7 @@ func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
}, nil)
return
}
zap.S().Infof("Get PATs for user: %+v", user.Id)
zap.L().Info("Get PATs for user", zap.String("user_id", user.Id))
pats, apierr := ah.AppDao().ListPATs(ctx)
if apierr != nil {
RespondError(w, apierr, nil)
@@ -156,7 +156,7 @@ func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
return
}
zap.S().Debugf("Revoke PAT with id: %+v", id)
zap.L().Info("Revoke PAT with id", zap.String("id", id))
if apierr := ah.AppDao().RevokePAT(ctx, id, user.Id); apierr != nil {
RespondError(w, apierr, nil)
return

View File

@@ -15,7 +15,7 @@ import (
func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
if !ah.CheckFeature(basemodel.SmartTraceDetail) {
zap.S().Info("SmartTraceDetail feature is not enabled in this plan")
zap.L().Info("SmartTraceDetail feature is not enabled in this plan")
ah.APIHandler.SearchTraces(w, r)
return
}
@@ -26,7 +26,7 @@ func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
}
spanLimit, err := strconv.Atoi(constants.SpanLimitStr)
if err != nil {
zap.S().Error("Error during strconv.Atoi() on SPAN_LIMIT env variable: ", err)
zap.L().Error("Error during strconv.Atoi() on SPAN_LIMIT env variable", zap.Error(err))
return
}
result, err := ah.opts.DataConnector.SearchTraces(r.Context(), traceId, spanId, levelUpInt, levelDownInt, spanLimit, db.SmartTraceAlgorithm)

View File

@@ -22,7 +22,7 @@ import (
func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) {
defer utils.Elapsed("GetMetricResult")()
zap.S().Infof("Executing metric result query: %s", query)
zap.L().Info("Executing metric result query: ", zap.String("query", query))
var hash string
// If getSubTreeSpans function is used in the clickhouse query
@@ -38,9 +38,8 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
}
rows, err := r.conn.Query(ctx, query)
zap.S().Debug(query)
if err != nil {
zap.S().Debug("Error in processing query: ", err)
zap.L().Error("Error in processing query", zap.Error(err))
return nil, "", fmt.Errorf("error in processing query")
}
@@ -117,7 +116,7 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())
}
default:
zap.S().Errorf("invalid var found in metric builder query result", v, colName)
zap.L().Error("invalid var found in metric builder query result", zap.Any("var", v), zap.String("colName", colName))
}
}
sort.Strings(groupBy)
@@ -140,7 +139,7 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
}
// err = r.conn.Exec(ctx, "DROP TEMPORARY TABLE IF EXISTS getSubTreeSpans"+hash)
// if err != nil {
// zap.S().Error("Error in dropping temporary table: ", err)
// zap.L().Error("Error in dropping temporary table: ", err)
// return nil, err
// }
if hash == "" {
@@ -152,7 +151,7 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string)
func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, query string, hash string) (string, string, error) {
zap.S().Debugf("Executing getSubTreeSpans function")
zap.L().Debug("Executing getSubTreeSpans function")
// str1 := `select fromUnixTimestamp64Milli(intDiv( toUnixTimestamp64Milli ( timestamp ), 100) * 100) AS interval, toFloat64(count()) as count from (select timestamp, spanId, parentSpanId, durationNano from getSubTreeSpans(select * from signoz_traces.signoz_index_v2 where serviceName='frontend' and name='/driver.DriverService/FindNearest' and traceID='00000000000000004b0a863cb5ed7681') where name='FindDriverIDs' group by interval order by interval asc;`
@@ -162,28 +161,28 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
err := r.conn.Exec(ctx, "DROP TABLE IF EXISTS getSubTreeSpans"+hash)
if err != nil {
zap.S().Error("Error in dropping temporary table: ", err)
zap.L().Error("Error in dropping temporary table", zap.Error(err))
return query, hash, err
}
// Create temporary table to store the getSubTreeSpans() results
zap.S().Debugf("Creating temporary table getSubTreeSpans%s", hash)
zap.L().Debug("Creating temporary table getSubTreeSpans", zap.String("hash", hash))
err = r.conn.Exec(ctx, "CREATE TABLE IF NOT EXISTS "+"getSubTreeSpans"+hash+" (timestamp DateTime64(9) CODEC(DoubleDelta, LZ4), traceID FixedString(32) CODEC(ZSTD(1)), spanID String CODEC(ZSTD(1)), parentSpanID String CODEC(ZSTD(1)), rootSpanID String CODEC(ZSTD(1)), serviceName LowCardinality(String) CODEC(ZSTD(1)), name LowCardinality(String) CODEC(ZSTD(1)), rootName LowCardinality(String) CODEC(ZSTD(1)), durationNano UInt64 CODEC(T64, ZSTD(1)), kind Int8 CODEC(T64, ZSTD(1)), tagMap Map(LowCardinality(String), String) CODEC(ZSTD(1)), events Array(String) CODEC(ZSTD(2))) ENGINE = MergeTree() ORDER BY (timestamp)")
if err != nil {
zap.S().Error("Error in creating temporary table: ", err)
zap.L().Error("Error in creating temporary table", zap.Error(err))
return query, hash, err
}
var getSpansSubQueryDBResponses []model.GetSpansSubQueryDBResponse
getSpansSubQuery := subtreeInput
// Execute the subTree query
zap.S().Debugf("Executing subTree query: %s", getSpansSubQuery)
zap.L().Debug("Executing subTree query", zap.String("query", getSpansSubQuery))
err = r.conn.Select(ctx, &getSpansSubQueryDBResponses, getSpansSubQuery)
// zap.S().Info(getSpansSubQuery)
// zap.L().Info(getSpansSubQuery)
if err != nil {
zap.S().Debug("Error in processing sql query: ", err)
zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, fmt.Errorf("Error in processing sql query")
}
@@ -196,16 +195,16 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
if len(getSpansSubQueryDBResponses) == 0 {
return query, hash, fmt.Errorf("No spans found for the given query")
}
zap.S().Debugf("Executing query to fetch all the spans from the same TraceID: %s", modelQuery)
zap.L().Debug("Executing query to fetch all the spans from the same TraceID: ", zap.String("modelQuery", modelQuery))
err = r.conn.Select(ctx, &searchScanResponses, modelQuery, getSpansSubQueryDBResponses[0].TraceID)
if err != nil {
zap.S().Debug("Error in processing sql query: ", err)
zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, fmt.Errorf("Error in processing sql query")
}
// Process model to fetch the spans
zap.S().Debugf("Processing model to fetch the spans")
zap.L().Debug("Processing model to fetch the spans")
searchSpanResponses := []basemodel.SearchSpanResponseItem{}
for _, item := range searchScanResponses {
var jsonItem basemodel.SearchSpanResponseItem
@@ -218,17 +217,17 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
}
// Build the subtree and store all the subtree spans in temporary table getSubTreeSpans+hash
// Use map to store pointer to the spans to avoid duplicates and save memory
zap.S().Debugf("Building the subtree to store all the subtree spans in temporary table getSubTreeSpans%s", hash)
zap.L().Debug("Building the subtree to store all the subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash))
treeSearchResponse, err := getSubTreeAlgorithm(searchSpanResponses, getSpansSubQueryDBResponses)
if err != nil {
zap.S().Error("Error in getSubTreeAlgorithm function: ", err)
zap.L().Error("Error in getSubTreeAlgorithm function", zap.Error(err))
return query, hash, err
}
zap.S().Debugf("Preparing batch to store subtree spans in temporary table getSubTreeSpans%s", hash)
zap.L().Debug("Preparing batch to store subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash))
statement, err := r.conn.PrepareBatch(context.Background(), fmt.Sprintf("INSERT INTO getSubTreeSpans"+hash))
if err != nil {
zap.S().Error("Error in preparing batch statement: ", err)
zap.L().Error("Error in preparing batch statement", zap.Error(err))
return query, hash, err
}
for _, span := range treeSearchResponse {
@@ -251,14 +250,14 @@ func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, qu
span.Events,
)
if err != nil {
zap.S().Debug("Error in processing sql query: ", err)
zap.L().Error("Error in processing sql query", zap.Error(err))
return query, hash, err
}
}
zap.S().Debugf("Inserting the subtree spans in temporary table getSubTreeSpans%s", hash)
zap.L().Debug("Inserting the subtree spans in temporary table getSubTreeSpans", zap.String("hash", hash))
err = statement.Send()
if err != nil {
zap.S().Error("Error in sending statement: ", err)
zap.L().Error("Error in sending statement", zap.Error(err))
return query, hash, err
}
return query, hash, nil
@@ -323,7 +322,7 @@ func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSub
spans = append(spans, span)
}
zap.S().Debug("Building Tree")
zap.L().Debug("Building Tree")
roots, err := buildSpanTrees(&spans)
if err != nil {
return nil, err
@@ -333,7 +332,7 @@ func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSub
// For each root, get the subtree spans
for _, getSpansSubQueryDBResponse := range getSpansSubQueryDBResponses {
targetSpan := &model.SpanForTraceDetails{}
// zap.S().Debug("Building tree for span id: " + getSpansSubQueryDBResponse.SpanID + " " + strconv.Itoa(i+1) + " of " + strconv.Itoa(len(getSpansSubQueryDBResponses)))
// zap.L().Debug("Building tree for span id: " + getSpansSubQueryDBResponse.SpanID + " " + strconv.Itoa(i+1) + " of " + strconv.Itoa(len(getSpansSubQueryDBResponses)))
// Search target span object in the tree
for _, root := range roots {
targetSpan, err = breadthFirstSearch(root, getSpansSubQueryDBResponse.SpanID)
@@ -341,7 +340,7 @@ func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSub
break
}
if err != nil {
zap.S().Error("Error during BreadthFirstSearch(): ", err)
zap.L().Error("Error during BreadthFirstSearch()", zap.Error(err))
return nil, err
}
}

View File

@@ -49,7 +49,7 @@ func SmartTraceAlgorithm(payload []basemodel.SearchSpanResponseItem, targetSpanI
break
}
if err != nil {
zap.S().Error("Error during BreadthFirstSearch(): ", err)
zap.L().Error("Error during BreadthFirstSearch()", zap.Error(err))
return nil, err
}
}
@@ -186,7 +186,7 @@ func buildSpanTrees(spansPtr *[]*model.SpanForTraceDetails) ([]*model.SpanForTra
// If the parent span is not found, add current span to list of roots
if parent == nil {
// zap.S().Debug("Parent Span not found parent_id: ", span.ParentID)
// zap.L().Debug("Parent Span not found parent_id: ", span.ParentID)
roots = append(roots, span)
span.ParentID = ""
continue

View File

@@ -35,6 +35,7 @@ import (
baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer"
"go.signoz.io/signoz/pkg/query-service/app/integrations"
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/app/opamp"
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
@@ -133,7 +134,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
var reader interfaces.DataConnector
storage := os.Getenv("STORAGE")
if storage == "clickhouse" {
zap.S().Info("Using ClickHouse as datastore ...")
zap.L().Info("Using ClickHouse as datastore ...")
qb := db.NewDataConnector(
localDB,
serverOptions.PromConfigPath,
@@ -171,13 +172,22 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate opamp
_, err = opAmpModel.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
_, err = opAmpModel.InitDB(localDB)
if err != nil {
return nil, err
}
integrationsController, err := integrations.NewController(localDB)
if err != nil {
return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err,
)
}
// ingestion pipelines manager
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(localDB, "sqlite")
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
localDB, "sqlite", integrationsController.GetPipelinesForInstalledIntegrations,
)
if err != nil {
return nil, err
}
@@ -233,6 +243,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
UsageManager: usageManager,
FeatureFlags: lm,
LicenseManager: lm,
IntegrationsController: integrationsController,
LogsParsingPipelineController: logParsingPipelineController,
Cache: c,
FluxInterval: fluxInterval,
@@ -278,6 +289,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
r := mux.NewRouter()
r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware)
r.Use(loggingMiddlewarePrivate)
@@ -310,6 +322,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
}
am := baseapp.NewAuthMiddleware(getUserFromRequest)
r.Use(baseapp.LogCommentEnricher)
r.Use(setTimeoutMiddleware)
r.Use(s.analyticsMiddleware)
r.Use(loggingMiddleware)
@@ -317,6 +330,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterMetricsRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
@@ -405,30 +419,33 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
signozMetricsUsed := false
signozLogsUsed := false
dataSources := []string{}
signozTracesUsed := false
if postData != nil {
if postData.CompositeQuery != nil {
data["queryType"] = postData.CompositeQuery.QueryType
data["panelType"] = postData.CompositeQuery.PanelType
signozLogsUsed, signozMetricsUsed = telemetry.GetInstance().CheckSigNozSignals(postData)
signozLogsUsed, signozMetricsUsed, signozTracesUsed = telemetry.GetInstance().CheckSigNozSignals(postData)
}
}
if signozMetricsUsed || signozLogsUsed {
if signozMetricsUsed || signozLogsUsed || signozTracesUsed {
if signozMetricsUsed {
dataSources = append(dataSources, "metrics")
telemetry.GetInstance().AddActiveMetricsUser()
}
if signozLogsUsed {
dataSources = append(dataSources, "logs")
telemetry.GetInstance().AddActiveLogsUser()
}
data["dataSources"] = dataSources
if signozTracesUsed {
telemetry.GetInstance().AddActiveTracesUser()
}
data["metricsUsed"] = signozMetricsUsed
data["logsUsed"] = signozLogsUsed
data["tracesUsed"] = signozTracesUsed
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_V3, data, userEmail, true)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
}
}
return data, true
@@ -471,7 +488,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
if _, ok := telemetry.EnabledPaths()[path]; ok {
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail, true, false)
}
}
@@ -508,7 +525,7 @@ func (s *Server) initListeners() error {
return err
}
zap.S().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort))
zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.serverOptions.HTTPHostPort))
// listen on private port to support internal services
privateHostPort := s.serverOptions.PrivateHostPort
@@ -521,7 +538,7 @@ func (s *Server) initListeners() error {
if err != nil {
return err
}
zap.S().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort))
zap.L().Info(fmt.Sprintf("Query server started listening on private port %s...", s.serverOptions.PrivateHostPort))
return nil
}
@@ -533,7 +550,7 @@ func (s *Server) Start() error {
if !s.serverOptions.DisableRules {
s.ruleManager.Start()
} else {
zap.S().Info("msg: Rules disabled as rules.disable is set to TRUE")
zap.L().Info("msg: Rules disabled as rules.disable is set to TRUE")
}
err := s.initListeners()
@@ -547,23 +564,23 @@ func (s *Server) Start() error {
}
go func() {
zap.S().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.serverOptions.HTTPHostPort))
zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.serverOptions.HTTPHostPort))
switch err := s.httpServer.Serve(s.httpConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
// normal exit, nothing to do
default:
zap.S().Error("Could not start HTTP server", zap.Error(err))
zap.L().Error("Could not start HTTP server", zap.Error(err))
}
s.unavailableChannel <- healthcheck.Unavailable
}()
go func() {
zap.S().Info("Starting pprof server", zap.String("addr", baseconst.DebugHttpPort))
zap.L().Info("Starting pprof server", zap.String("addr", baseconst.DebugHttpPort))
err = http.ListenAndServe(baseconst.DebugHttpPort, nil)
if err != nil {
zap.S().Error("Could not start pprof server", zap.Error(err))
zap.L().Error("Could not start pprof server", zap.Error(err))
}
}()
@@ -573,14 +590,14 @@ func (s *Server) Start() error {
}
go func() {
zap.S().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort))
zap.L().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort))
switch err := s.privateHTTP.Serve(s.privateConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
// normal exit, nothing to do
zap.S().Info("private http server closed")
zap.L().Info("private http server closed")
default:
zap.S().Error("Could not start private HTTP server", zap.Error(err))
zap.L().Error("Could not start private HTTP server", zap.Error(err))
}
s.unavailableChannel <- healthcheck.Unavailable
@@ -588,10 +605,10 @@ func (s *Server) Start() error {
}()
go func() {
zap.S().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint))
zap.L().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint))
err := s.opampServer.Start(baseconst.OpAmpWsEndpoint)
if err != nil {
zap.S().Info("opamp ws server failed to start", err)
zap.L().Error("opamp ws server failed to start", zap.Error(err))
s.unavailableChannel <- healthcheck.Unavailable
}
}()
@@ -667,7 +684,7 @@ func makeRulesManager(
return nil, fmt.Errorf("rule manager error: %v", err)
}
zap.S().Info("rules manager is ready")
zap.L().Info("rules manager is ready")
return manager, nil
}

View File

@@ -17,25 +17,25 @@ import (
func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel.UserPayload, error) {
patToken := r.Header.Get("SIGNOZ-API-KEY")
if len(patToken) > 0 {
zap.S().Debugf("Received a non-zero length PAT token")
zap.L().Debug("Received a non-zero length PAT token")
ctx := context.Background()
dao := apiHandler.AppDao()
pat, err := dao.GetPAT(ctx, patToken)
if err == nil && pat != nil {
zap.S().Debugf("Found valid PAT: %+v", pat)
zap.L().Debug("Found valid PAT: ", zap.Any("pat", pat))
if pat.ExpiresAt < time.Now().Unix() && pat.ExpiresAt != 0 {
zap.S().Debugf("PAT has expired: %+v", pat)
zap.L().Info("PAT has expired: ", zap.Any("pat", pat))
return nil, fmt.Errorf("PAT has expired")
}
group, apiErr := dao.GetGroupByName(ctx, pat.Role)
if apiErr != nil {
zap.S().Debugf("Error while getting group for PAT: %+v", apiErr)
zap.L().Error("Error while getting group for PAT: ", zap.Any("apiErr", apiErr))
return nil, apiErr
}
user, err := dao.GetUser(ctx, pat.UserID)
if err != nil {
zap.S().Debugf("Error while getting user for PAT: %+v", err)
zap.L().Error("Error while getting user for PAT: ", zap.Error(err))
return nil, err
}
telemetry.GetInstance().SetPatTokenUser()
@@ -48,7 +48,7 @@ func GetUserFromRequest(r *http.Request, apiHandler *api.APIHandler) (*basemodel
}, nil
}
if err != nil {
zap.S().Debugf("Error while getting user for PAT: %+v", err)
zap.L().Error("Error while getting user for PAT: ", zap.Error(err))
return nil, err
}
}

View File

@@ -22,19 +22,19 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
domain, apierr := m.GetDomainByEmail(ctx, email)
if apierr != nil {
zap.S().Errorf("failed to get domain from email", apierr)
zap.L().Error("failed to get domain from email", zap.Error(apierr))
return nil, model.InternalErrorStr("failed to get domain from email")
}
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
if err != nil {
zap.S().Errorf("failed to generate password hash when registering a user via SSO redirect", zap.Error(err))
zap.L().Error("failed to generate password hash when registering a user via SSO redirect", zap.Error(err))
return nil, model.InternalErrorStr("failed to generate password hash")
}
group, apiErr := m.GetGroupByName(ctx, baseconst.ViewerGroup)
if apiErr != nil {
zap.S().Debugf("GetGroupByName failed, err: %v\n", apiErr.Err)
zap.L().Error("GetGroupByName failed", zap.Error(apiErr))
return nil, apiErr
}
@@ -51,7 +51,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
user, apiErr = m.CreateUser(ctx, user, false)
if apiErr != nil {
zap.S().Debugf("CreateUser failed, err: %v\n", apiErr.Err)
zap.L().Error("CreateUser failed", zap.Error(apiErr))
return nil, apiErr
}
@@ -65,7 +65,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
userPayload, apierr := m.GetUserByEmail(ctx, email)
if !apierr.IsNil() {
zap.S().Errorf(" failed to get user with email received from auth provider", apierr.Error())
zap.L().Error("failed to get user with email received from auth provider", zap.String("error", apierr.Error()))
return "", model.BadRequestStr("invalid user email received from the auth provider")
}
@@ -75,7 +75,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
newUser, apiErr := m.createUserForSAMLRequest(ctx, email)
user = newUser
if apiErr != nil {
zap.S().Errorf("failed to create user with email received from auth provider: %v", apierr.Error())
zap.L().Error("failed to create user with email received from auth provider", zap.Error(apiErr))
return "", apiErr
}
} else {
@@ -84,7 +84,7 @@ func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email st
tokenStore, err := baseauth.GenerateJWTForUser(user)
if err != nil {
zap.S().Errorf("failed to generate token for SSO login user", err)
zap.L().Error("failed to generate token for SSO login user", zap.Error(err))
return "", model.InternalErrorStr("failed to generate token for the user")
}
@@ -143,8 +143,8 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
// do nothing, just skip sso
ssoAvailable = false
default:
zap.S().Errorf("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err))
return resp, model.BadRequest(err)
zap.L().Error("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err))
return resp, model.BadRequestStr(err.Error())
}
}
@@ -160,7 +160,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
if len(emailComponents) > 0 {
emailDomain = emailComponents[1]
}
zap.S().Errorf("failed to get org domain from email", zap.String("emailDomain", emailDomain), apierr.ToError())
zap.L().Error("failed to get org domain from email", zap.String("emailDomain", emailDomain), zap.Error(apierr.ToError()))
return resp, apierr
}
@@ -176,7 +176,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
escapedUrl, _ := url.QueryUnescape(sourceUrl)
siteUrl, err := url.Parse(escapedUrl)
if err != nil {
zap.S().Errorf("failed to parse referer", err)
zap.L().Error("failed to parse referer", zap.Error(err))
return resp, model.InternalError(fmt.Errorf("failed to generate login request"))
}
@@ -185,7 +185,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
resp.SsoUrl, err = orgDomain.BuildSsoUrl(siteUrl)
if err != nil {
zap.S().Errorf("failed to prepare saml request for domain", zap.String("domain", orgDomain.Name), err)
zap.L().Error("failed to prepare saml request for domain", zap.String("domain", orgDomain.Name), zap.Error(err))
return resp, model.InternalError(err)
}

View File

@@ -48,13 +48,13 @@ func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url
if domainIdStr != "" {
domainId, err := uuid.Parse(domainIdStr)
if err != nil {
zap.S().Errorf("failed to parse domainId from relay state", err)
zap.L().Error("failed to parse domainId from relay state", zap.Error(err))
return nil, fmt.Errorf("failed to parse domainId from IdP response")
}
domain, err = m.GetDomain(ctx, domainId)
if (err != nil) || domain == nil {
zap.S().Errorf("failed to find domain from domainId received in IdP response", err.Error())
zap.L().Error("failed to find domain from domainId received in IdP response", zap.Error(err))
return nil, fmt.Errorf("invalid credentials")
}
}
@@ -64,7 +64,7 @@ func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url
domainFromDB, err := m.GetDomainByName(ctx, domainNameStr)
domain = domainFromDB
if (err != nil) || domain == nil {
zap.S().Errorf("failed to find domain from domainName received in IdP response", err.Error())
zap.L().Error("failed to find domain from domainName received in IdP response", zap.Error(err))
return nil, fmt.Errorf("invalid credentials")
}
}
@@ -132,7 +132,7 @@ func (m *modelDao) ListDomains(ctx context.Context, orgId string) ([]model.OrgDo
for _, s := range stored {
domain := model.OrgDomain{Id: s.Id, Name: s.Name, OrgId: s.OrgId}
if err := domain.LoadConfig(s.Data); err != nil {
zap.S().Errorf("ListDomains() failed", zap.Error(err))
zap.L().Error("ListDomains() failed", zap.Error(err))
}
domains = append(domains, domain)
}
@@ -153,7 +153,7 @@ func (m *modelDao) CreateDomain(ctx context.Context, domain *model.OrgDomain) ba
configJson, err := json.Marshal(domain)
if err != nil {
zap.S().Errorf("failed to unmarshal domain config", zap.Error(err))
zap.L().Error("failed to unmarshal domain config", zap.Error(err))
return model.InternalError(fmt.Errorf("domain creation failed"))
}
@@ -167,7 +167,7 @@ func (m *modelDao) CreateDomain(ctx context.Context, domain *model.OrgDomain) ba
time.Now().Unix())
if err != nil {
zap.S().Errorf("failed to insert domain in db", zap.Error(err))
zap.L().Error("failed to insert domain in db", zap.Error(err))
return model.InternalError(fmt.Errorf("domain creation failed"))
}
@@ -178,13 +178,13 @@ func (m *modelDao) CreateDomain(ctx context.Context, domain *model.OrgDomain) ba
func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) basemodel.BaseApiError {
if domain.Id == uuid.Nil {
zap.S().Errorf("domain update failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
zap.L().Error("domain update failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
return model.InternalError(fmt.Errorf("domain update failed"))
}
configJson, err := json.Marshal(domain)
if err != nil {
zap.S().Errorf("domain update failed", zap.Error(err))
zap.L().Error("domain update failed", zap.Error(err))
return model.InternalError(fmt.Errorf("domain update failed"))
}
@@ -195,7 +195,7 @@ func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) ba
domain.Id)
if err != nil {
zap.S().Errorf("domain update failed", zap.Error(err))
zap.L().Error("domain update failed", zap.Error(err))
return model.InternalError(fmt.Errorf("domain update failed"))
}
@@ -206,7 +206,7 @@ func (m *modelDao) UpdateDomain(ctx context.Context, domain *model.OrgDomain) ba
func (m *modelDao) DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError {
if id == uuid.Nil {
zap.S().Errorf("domain delete failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
zap.L().Error("domain delete failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
return model.InternalError(fmt.Errorf("domain delete failed"))
}
@@ -215,7 +215,7 @@ func (m *modelDao) DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.Bas
id)
if err != nil {
zap.S().Errorf("domain delete failed", zap.Error(err))
zap.L().Error("domain delete failed", zap.Error(err))
return model.InternalError(fmt.Errorf("domain delete failed"))
}

View File

@@ -26,12 +26,12 @@ func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basem
p.Revoked,
)
if err != nil {
zap.S().Errorf("Failed to insert PAT in db, err: %v", zap.Error(err))
zap.L().Error("Failed to insert PAT in db, err: %v", zap.Error(err))
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
}
id, err := result.LastInsertId()
if err != nil {
zap.S().Errorf("Failed to get last inserted id, err: %v", zap.Error(err))
zap.L().Error("Failed to get last inserted id, err: %v", zap.Error(err))
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
}
p.Id = strconv.Itoa(int(id))
@@ -62,7 +62,7 @@ func (m *modelDao) UpdatePAT(ctx context.Context, p model.PAT, id string) basemo
p.UpdatedByUserID,
id)
if err != nil {
zap.S().Errorf("Failed to update PAT in db, err: %v", zap.Error(err))
zap.L().Error("Failed to update PAT in db, err: %v", zap.Error(err))
return model.InternalError(fmt.Errorf("PAT update failed"))
}
return nil
@@ -74,7 +74,7 @@ func (m *modelDao) UpdatePATLastUsed(ctx context.Context, token string, lastUsed
lastUsed,
token)
if err != nil {
zap.S().Errorf("Failed to update PAT last used in db, err: %v", zap.Error(err))
zap.L().Error("Failed to update PAT last used in db, err: %v", zap.Error(err))
return model.InternalError(fmt.Errorf("PAT last used update failed"))
}
return nil
@@ -84,7 +84,7 @@ func (m *modelDao) ListPATs(ctx context.Context) ([]model.PAT, basemodel.BaseApi
pats := []model.PAT{}
if err := m.DB().Select(&pats, "SELECT * FROM personal_access_tokens WHERE revoked=false ORDER by updated_at DESC;"); err != nil {
zap.S().Errorf("Failed to fetch PATs err: %v", zap.Error(err))
zap.L().Error("Failed to fetch PATs err: %v", zap.Error(err))
return nil, model.InternalError(fmt.Errorf("failed to fetch PATs"))
}
for i := range pats {
@@ -129,7 +129,7 @@ func (m *modelDao) RevokePAT(ctx context.Context, id string, userID string) base
"UPDATE personal_access_tokens SET revoked=true, updated_by_user_id = $1, updated_at=$2 WHERE id=$3",
userID, updatedAt, id)
if err != nil {
zap.S().Errorf("Failed to revoke PAT in db, err: %v", zap.Error(err))
zap.L().Error("Failed to revoke PAT in db, err: %v", zap.Error(err))
return model.InternalError(fmt.Errorf("PAT revoke failed"))
}
return nil

View File

@@ -47,13 +47,13 @@ func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError)
httpResponse, err := http.Post(C.Prefix+"/licenses/activate", APPLICATION_JSON, bytes.NewBuffer(reqString))
if err != nil {
zap.S().Errorf("failed to connect to license.signoz.io", err)
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"))
}
httpBody, err := io.ReadAll(httpResponse.Body)
if err != nil {
zap.S().Errorf("failed to read activation response from license.signoz.io", err)
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"))
}
@@ -63,7 +63,7 @@ func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError)
result := ActivationResult{}
err = json.Unmarshal(httpBody, &result)
if err != nil {
zap.S().Errorf("failed to marshal activation response from license.signoz.io", err)
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"))
}

View File

@@ -97,7 +97,7 @@ func (r *Repo) InsertLicense(ctx context.Context, l *model.License) error {
l.ValidationMessage)
if err != nil {
zap.S().Errorf("error in inserting license data: ", zap.Error(err))
zap.L().Error("error in inserting license data: ", zap.Error(err))
return fmt.Errorf("failed to insert license in db: %v", err)
}
@@ -121,7 +121,7 @@ func (r *Repo) UpdatePlanDetails(ctx context.Context,
_, err := r.db.ExecContext(ctx, query, planDetails, time.Now(), key)
if err != nil {
zap.S().Errorf("error in updating license: ", zap.Error(err))
zap.L().Error("error in updating license: ", zap.Error(err))
return fmt.Errorf("failed to update license in db: %v", err)
}

View File

@@ -100,7 +100,7 @@ func (lm *Manager) SetActive(l *model.License) {
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.S().Panicf("Couldn't activate features: %v", err)
zap.L().Panic("Couldn't activate features", zap.Error(err))
}
if !lm.validatorRunning {
// we want to make sure only one validator runs,
@@ -125,13 +125,13 @@ func (lm *Manager) LoadActiveLicense() error {
if active != nil {
lm.SetActive(active)
} else {
zap.S().Info("No active license found, defaulting to basic plan")
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
lm.activeFeatures = model.BasicPlan
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.S().Error("Couldn't initialize features: ", err)
zap.L().Error("Couldn't initialize features", zap.Error(err))
return err
}
}
@@ -191,7 +191,7 @@ func (lm *Manager) Validator(ctx context.Context) {
// Validate validates the current active license
func (lm *Manager) Validate(ctx context.Context) (reterr error) {
zap.S().Info("License validation started")
zap.L().Info("License validation started")
if lm.activeLicense == nil {
return nil
}
@@ -201,12 +201,12 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
lm.lastValidated = time.Now().Unix()
if reterr != nil {
zap.S().Errorf("License validation completed with error", reterr)
zap.L().Error("License validation completed with error", zap.Error(reterr))
atomic.AddUint64(&lm.failedAttempts, 1)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
map[string]interface{}{"err": reterr.Error()}, "")
map[string]interface{}{"err": reterr.Error()}, "", true, false)
} else {
zap.S().Info("License validation completed with no errors")
zap.L().Info("License validation completed with no errors")
}
lm.mutex.Unlock()
@@ -214,7 +214,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId)
if apiError != nil {
zap.S().Errorf("failed to validate license", apiError)
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
return apiError.Err
}
@@ -235,7 +235,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
}
if err := l.ParsePlan(); err != nil {
zap.S().Errorf("failed to parse updated license", zap.Error(err))
zap.L().Error("failed to parse updated license", zap.Error(err))
return err
}
@@ -245,7 +245,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
if err != nil {
// unexpected db write issue but we can let the user continue
// and wait for update to work in next cycle.
zap.S().Errorf("failed to validate license", zap.Error(err))
zap.L().Error("failed to validate license", zap.Error(err))
}
}
@@ -263,14 +263,14 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail)
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
}
}
}()
response, apiError := validate.ActivateLicense(key, "")
if apiError != nil {
zap.S().Errorf("failed to activate license", zap.Error(apiError.Err))
zap.L().Error("failed to activate license", zap.Error(apiError.Err))
return nil, apiError
}
@@ -284,14 +284,14 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
err := l.ParsePlan()
if err != nil {
zap.S().Errorf("failed to activate license", zap.Error(err))
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.S().Errorf("failed to activate license", zap.Error(err))
zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err)
}

View File

@@ -14,10 +14,10 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.signoz.io/signoz/ee/query-service/app"
"go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/constants"
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/version"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
zapotlpencoder "github.com/SigNoz/zap_otlp/zap_otlp_encoder"
zapotlpsync "github.com/SigNoz/zap_otlp/zap_otlp_sync"
@@ -27,18 +27,19 @@ import (
)
func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
config := zap.NewDevelopmentConfig()
config := zap.NewProductionConfig()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
config.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder
otlpEncoder := zapotlpencoder.NewOTLPEncoder(config.EncoderConfig)
consoleEncoder := zapcore.NewConsoleEncoder(config.EncoderConfig)
defaultLogLevel := zapcore.DebugLevel
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
config.EncoderConfig.EncodeDuration = zapcore.MillisDurationEncoder
config.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
otlpEncoder := zapotlpencoder.NewOTLPEncoder(config.EncoderConfig)
consoleEncoder := zapcore.NewJSONEncoder(config.EncoderConfig)
defaultLogLevel := zapcore.InfoLevel
res := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("query-service"),
@@ -48,14 +49,15 @@ func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel),
)
if enableQueryServiceLogOTLPExport == true {
conn, err := grpc.DialContext(ctx, constants.OTLPTarget, grpc.WithBlock(), grpc.WithInsecure(), grpc.WithTimeout(time.Second*30))
if enableQueryServiceLogOTLPExport {
ctx, _ := context.WithTimeout(ctx, time.Second*30)
conn, err := grpc.DialContext(ctx, baseconst.OTLPTarget, grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Println("failed to connect to otlp collector to export query service logs with error:", err)
log.Fatalf("failed to establish connection: %v", err)
} else {
logExportBatchSizeInt, err := strconv.Atoi(baseconst.LogExportBatchSize)
if err != nil {
logExportBatchSizeInt = 1000
logExportBatchSizeInt = 512
}
ws := zapcore.AddSync(zapotlpsync.NewOtlpSyncer(conn, zapotlpsync.Options{
BatchSize: logExportBatchSizeInt,
@@ -113,7 +115,6 @@ func main() {
zap.ReplaceGlobals(loggerMgr)
defer loggerMgr.Sync() // flushes buffer, if any
logger := loggerMgr.Sugar()
version.PrintVersion()
serverOptions := &app.ServerOptions{
@@ -137,22 +138,22 @@ func main() {
auth.JwtSecret = os.Getenv("SIGNOZ_JWT_SECRET")
if len(auth.JwtSecret) == 0 {
zap.S().Warn("No JWT secret key is specified.")
zap.L().Warn("No JWT secret key is specified.")
} else {
zap.S().Info("No JWT secret key set successfully.")
zap.L().Info("JWT secret key set successfully.")
}
server, err := app.NewServer(serverOptions)
if err != nil {
logger.Fatal("Failed to create server", zap.Error(err))
zap.L().Fatal("Failed to create server", zap.Error(err))
}
if err := server.Start(); err != nil {
logger.Fatal("Could not start servers", zap.Error(err))
zap.L().Fatal("Could not start server", zap.Error(err))
}
if err := auth.InitAuthCache(context.Background()); err != nil {
logger.Fatal("Failed to initialize auth cache", zap.Error(err))
zap.L().Fatal("Failed to initialize auth cache", zap.Error(err))
}
signalsChannel := make(chan os.Signal, 1)
@@ -161,9 +162,9 @@ func main() {
for {
select {
case status := <-server.HealthCheckStatus():
logger.Info("Received HealthCheck status: ", zap.Int("status", int(status)))
zap.L().Info("Received HealthCheck status: ", zap.Int("status", int(status)))
case <-signalsChannel:
logger.Fatal("Received OS Interrupt Signal ... ")
zap.L().Fatal("Received OS Interrupt Signal ... ")
server.Stop()
}
}

View File

@@ -9,8 +9,8 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
saml2 "github.com/russellhaering/gosaml2"
"go.signoz.io/signoz/ee/query-service/sso/saml"
"go.signoz.io/signoz/ee/query-service/sso"
"go.signoz.io/signoz/ee/query-service/sso/saml"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
@@ -24,16 +24,16 @@ const (
// OrgDomain identify org owned web domains for auth and other purposes
type OrgDomain struct {
Id uuid.UUID `json:"id"`
Name string `json:"name"`
OrgId string `json:"orgId"`
SsoEnabled bool `json:"ssoEnabled"`
SsoType SSOType `json:"ssoType"`
Id uuid.UUID `json:"id"`
Name string `json:"name"`
OrgId string `json:"orgId"`
SsoEnabled bool `json:"ssoEnabled"`
SsoType SSOType `json:"ssoType"`
SamlConfig *SamlConfig `json:"samlConfig"`
SamlConfig *SamlConfig `json:"samlConfig"`
GoogleAuthConfig *GoogleOAuthConfig `json:"googleAuthConfig"`
Org *basemodel.Organization
Org *basemodel.Organization
}
func (od *OrgDomain) String() string {
@@ -100,8 +100,8 @@ func (od *OrgDomain) GetSAMLCert() string {
return ""
}
// PrepareGoogleOAuthProvider creates GoogleProvider that is used in
// requesting OAuth and also used in processing response from google
// PrepareGoogleOAuthProvider creates GoogleProvider that is used in
// requesting OAuth and also used in processing response from google
func (od *OrgDomain) PrepareGoogleOAuthProvider(siteUrl *url.URL) (sso.OAuthCallbackProvider, error) {
if od.GoogleAuthConfig == nil {
return nil, fmt.Errorf("Google auth is not setup correctly for this domain")
@@ -137,38 +137,36 @@ func (od *OrgDomain) PrepareSamlRequest(siteUrl *url.URL) (*saml2.SAMLServicePro
}
func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
fmtDomainId := strings.Replace(od.Id.String(), "-", ":", -1)
// build redirect url from window.location sent by frontend
redirectURL := fmt.Sprintf("%s://%s%s", siteUrl.Scheme, siteUrl.Host, siteUrl.Path)
// prepare state that gets relayed back when the auth provider
// calls back our url. here we pass the app url (where signoz runs)
// and the domain Id. The domain Id helps in identifying sso config
// when the call back occurs and the app url is useful in redirecting user
// back to the right path.
// when the call back occurs and the app url is useful in redirecting user
// back to the right path.
// why do we need to pass app url? the callback typically is handled by backend
// and sometimes backend might right at a different port or is unaware of frontend
// endpoint (unless SITE_URL param is set). hence, we receive this build sso request
// along with frontend window.location and use it to relay the information through
// auth provider to the backend (HandleCallback or HandleSSO method).
// along with frontend window.location and use it to relay the information through
// auth provider to the backend (HandleCallback or HandleSSO method).
relayState := fmt.Sprintf("%s?domainId=%s", redirectURL, fmtDomainId)
switch (od.SsoType) {
switch od.SsoType {
case SAML:
sp, err := od.PrepareSamlRequest(siteUrl)
if err != nil {
return "", err
}
return sp.BuildAuthURL(relayState)
case GoogleAuth:
googleProvider, err := od.PrepareGoogleOAuthProvider(siteUrl)
if err != nil {
return "", err
@@ -176,9 +174,8 @@ func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
return googleProvider.BuildAuthURL(relayState)
default:
zap.S().Errorf("found unsupported SSO config for the org domain", zap.String("orgDomain", od.Name))
return "", fmt.Errorf("unsupported SSO config for the domain")
zap.L().Error("found unsupported SSO config for the org domain", zap.String("orgDomain", od.Name))
return "", fmt.Errorf("unsupported SSO config for the domain")
}
}

View File

@@ -90,6 +90,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelMsTeams,
Active: false,
@@ -177,6 +184,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelMsTeams,
Active: true,
@@ -264,6 +278,13 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelEmail,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.AlertChannelMsTeams,
Active: true,
@@ -279,17 +300,17 @@ var EnterprisePlan = basemodel.FeatureSet{
Route: "",
},
basemodel.Feature{
Name: Onboarding,
Active: true,
Usage: 0,
Name: Onboarding,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
Route: "",
},
basemodel.Feature{
Name: ChatSupport,
Active: true,
Usage: 0,
Name: ChatSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
Route: "",
},
}

View File

@@ -102,6 +102,6 @@ func PrepareRequest(issuer, acsUrl, audience, entity, idp, certString string) (*
IDPCertificateStore: certStore,
SPKeyStore: randomKeyStore,
}
zap.S().Debugf("SAML request:", sp)
zap.L().Debug("SAML request", zap.Any("sp", sp))
return sp, nil
}

View File

@@ -91,12 +91,12 @@ func (lm *Manager) UploadUsage() {
// check if license is present or not
license, err := lm.licenseRepo.GetActiveLicense(ctx)
if err != nil {
zap.S().Errorf("failed to get active license: %v", zap.Error(err))
zap.L().Error("failed to get active license", zap.Error(err))
return
}
if license == nil {
// we will not start the usage reporting if license is not present.
zap.S().Info("no license present, skipping usage reporting")
zap.L().Info("no license present, skipping usage reporting")
return
}
@@ -123,7 +123,7 @@ func (lm *Manager) UploadUsage() {
dbusages := []model.UsageDB{}
err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
if err != nil && !strings.Contains(err.Error(), "doesn't exist") {
zap.S().Errorf("failed to get usage from clickhouse: %v", zap.Error(err))
zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err))
return
}
for _, u := range dbusages {
@@ -133,16 +133,16 @@ func (lm *Manager) UploadUsage() {
}
if len(usages) <= 0 {
zap.S().Info("no snapshots to upload, skipping.")
zap.L().Info("no snapshots to upload, skipping.")
return
}
zap.S().Info("uploading usage data")
zap.L().Info("uploading usage data")
orgName := ""
orgNames, orgError := lm.modelDao.GetOrgs(ctx)
if orgError != nil {
zap.S().Errorf("failed to get org data: %v", zap.Error(orgError))
zap.L().Error("failed to get org data: %v", zap.Error(orgError))
}
if len(orgNames) == 1 {
orgName = orgNames[0].Name
@@ -152,14 +152,14 @@ func (lm *Manager) UploadUsage() {
for _, usage := range usages {
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
if err != nil {
zap.S().Errorf("error while decrypting usage data: %v", zap.Error(err))
zap.L().Error("error while decrypting usage data: %v", zap.Error(err))
return
}
usageData := model.Usage{}
err = json.Unmarshal(usageDataBytes, &usageData)
if err != nil {
zap.S().Errorf("error while unmarshalling usage data: %v", zap.Error(err))
zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err))
return
}
@@ -184,13 +184,13 @@ func (lm *Manager) UploadUsageWithExponentalBackOff(ctx context.Context, payload
for i := 1; i <= MaxRetries; i++ {
apiErr := licenseserver.SendUsage(ctx, payload)
if apiErr != nil && i == MaxRetries {
zap.S().Errorf("retries stopped : %v", zap.Error(apiErr))
zap.L().Error("retries stopped : %v", zap.Error(apiErr))
// not returning error here since it is captured in the failed count
return
} else if apiErr != nil {
// sleeping for exponential backoff
sleepDuration := RetryInterval * time.Duration(i)
zap.S().Errorf("failed to upload snapshot retrying after %v secs : %v", sleepDuration.Seconds(), zap.Error(apiErr.Err))
zap.L().Error("failed to upload snapshot retrying after %v secs : %v", zap.Duration("sleepDuration", sleepDuration), zap.Error(apiErr.Err))
time.Sleep(sleepDuration)
} else {
break
@@ -201,7 +201,7 @@ func (lm *Manager) UploadUsageWithExponentalBackOff(ctx context.Context, payload
func (lm *Manager) Stop() {
lm.scheduler.Stop()
zap.S().Debug("sending usage data before shutting down")
zap.L().Info("sending usage data before shutting down")
// send usage before shutting down
lm.UploadUsage()

View File

@@ -107,6 +107,7 @@
"react-virtuoso": "4.0.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"stream": "^0.0.2",
"style-loader": "1.3.0",
"styled-components": "^5.3.11",
@@ -203,6 +204,7 @@
"jest-styled-components": "^7.0.8",
"lint-staged": "^12.5.0",
"msw": "1.3.2",
"npm-run-all": "latest",
"portfinder-sync": "^0.0.2",
"prettier": "2.2.1",
"raw-loader": "4.0.2",
@@ -216,8 +218,7 @@
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.0.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2",
"npm-run-all": "latest"
"webpack-cli": "^4.9.2"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [

View File

@@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_2022_1972)" stroke="#fff" stroke-width="1.333" stroke-linecap="round" stroke-linejoin="round"><path d="M6.667 2h.006M9.333 1.333h.007M1.333 6l13.334-3.333M8 8V4.333M11.333 8H4.667a2 2 0 00-2 2v2.667a2 2 0 002 2h6.666a2 2 0 002-2V10a2 2 0 00-2-2zM6 8v3.333M10 8v3.333M2.667 11.334h10.666"/></g><defs><clipPath id="prefix__clip0_2022_1972"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g stroke="#C0C1C3" stroke-width="1.333" stroke-linecap="round"><path d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z" stroke-linejoin="round"/><path d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3M10.75 10.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z"/></g></svg>

After

Width:  |  Height:  |  Size: 644 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><g stroke="#C0C1C3" stroke-width="1.333" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4.667V3.333C2 2.6 2.6 2 3.333 2h1.334M11.333 2h1.334C13.4 2 14 2.6 14 3.333v1.334M14 11.334v1.333C14 13.4 13.4 14 12.667 14h-1.334M4.667 14H3.333C2.6 14 2 13.4 2 12.667v-1.333M8.667 4.667H5.333a.667.667 0 00-.666.666v2c0 .368.298.667.666.667h3.334a.667.667 0 00.666-.667v-2a.667.667 0 00-.666-.667zM10.667 8H7.333a.667.667 0 00-.666.667v2c0 .368.298.666.666.666h3.334a.667.667 0 00.666-.666v-2A.667.667 0 0010.667 8z"/></g></svg>

After

Width:  |  Height:  |  Size: 604 B

View File

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M23.06 17.526c-1.281.668-7.916 3.396-9.328 4.132-1.413.736-2.198.73-3.314.196C9.303 21.32 2.242 18.468.97 17.86c-.636-.303-.97-.56-.97-.802v-2.426s9.192-2.001 10.676-2.534c1.484-.532 1.999-.551 3.262-.089 1.263.463 8.814 1.826 10.062 2.283v2.391c0 .24-.288.503-.94.843z" fill="#912626"/><path d="M23.06 15.114c-1.281.668-7.916 3.396-9.329 4.132-1.412.737-2.197.73-3.313.196C9.302 18.91 2.242 16.056.97 15.45c-1.272-.608-1.298-1.027-.049-1.516 1.25-.49 8.271-3.244 9.755-3.776 1.484-.533 1.999-.552 3.262-.09 1.263.463 7.858 3.088 9.106 3.546 1.248.457 1.296.834.015 1.501z" fill="#C6302B"/><path d="M23.06 13.6c-1.281.668-7.916 3.396-9.328 4.133-1.413.736-2.198.73-3.314.196S2.242 14.543.97 13.935c-.636-.304-.97-.56-.97-.802v-2.426s9.192-2.001 10.676-2.534c1.484-.532 1.999-.551 3.262-.089C15.2 8.547 22.752 9.91 24 10.366v2.392c0 .24-.288.503-.94.843z" fill="#912626"/><path d="M23.06 11.19c-1.281.667-7.916 3.395-9.329 4.131-1.412.737-2.197.73-3.313.196-1.116-.533-8.176-3.386-9.448-3.993-1.272-.608-1.298-1.027-.049-1.516 1.25-.49 8.271-3.244 9.755-3.776 1.484-.533 1.999-.552 3.262-.09 1.263.463 7.858 3.088 9.106 3.545 1.248.458 1.296.835.015 1.502z" fill="#C6302B"/><path d="M23.06 9.53c-1.281.668-7.916 3.396-9.328 4.132-1.413.737-2.198.73-3.314.196-1.116-.533-8.176-3.386-9.448-3.993C.334 9.56 0 9.305 0 9.062V6.636s9.192-2 10.676-2.533c1.484-.533 1.999-.552 3.262-.09C15.2 4.477 22.752 5.84 24 6.297v2.392c0 .24-.288.502-.94.842z" fill="#912626"/><path d="M23.06 7.118c-1.281.668-7.916 3.396-9.329 4.132-1.412.737-2.197.73-3.313.196C9.303 10.913 2.242 8.061.97 7.453-.302 6.845-.328 6.427.921 5.937c1.25-.489 8.271-3.244 9.755-3.776 1.484-.532 1.999-.552 3.262-.089 1.263.463 7.858 3.088 9.106 3.545 1.248.457 1.296.834.015 1.501z" fill="#C6302B"/><path d="M14.933 4.758l-2.064.215-.462 1.111-.746-1.24L9.28 4.63l1.778-.641-.534-.985 1.665.651 1.569-.513-.424 1.017 1.6.6zm-2.649 5.393l-3.85-1.597 5.517-.847-1.667 2.444zM6.945 5.376c1.63 0 2.95.512 2.95 1.143 0 .632-1.32 1.144-2.95 1.144-1.629 0-2.95-.512-2.95-1.144 0-.63 1.321-1.143 2.95-1.143z" fill="#fff"/><path d="M17.371 5.062l3.266 1.29-3.263 1.29-.003-2.58z" fill="#621B1C"/><path d="M13.758 6.492l3.613-1.43.003 2.58-.354.139-3.262-1.29z" fill="#9A2928"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -37,11 +37,16 @@
"text_condition1": "Send a notification when",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_1min": "1 min",
"option_5min": "5 mins",
"option_10min": "10 mins",
"option_15min": "15 mins",
"option_30min": "30 mins",
"option_60min": "60 mins",
"option_4hours": "4 hours",
"option_3hours": "3 hours",
"option_6hours": "6 hours",
"option_12hours": "12 hours",
"option_24hours": "24 hours",
"field_threshold": "Alert Threshold",
"option_allthetimes": "all the times",
@@ -111,5 +116,8 @@
"exceptions_based_alert": "Exceptions-based Alert",
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
"field_unit": "Threshold unit",
"text_alert_on_absent": "Send a notification if data is missing for",
"text_alert_frequency": "Run alert every",
"text_for": "minutes",
"selected_query_placeholder": "Select query"
}

View File

@@ -25,5 +25,5 @@
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
"your_graph_build_with": "Your graph built with",
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
"dashboard_ok_confirm": "query will be saved. Press OK to confirm."
}

View File

@@ -14,6 +14,5 @@
"delete_domain_message": "Are you sure you want to delete this domain?",
"delete_domain": "Delete Domain",
"add_domain": "Add Domains",
"saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly",
"invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually"
"saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly"
}

View File

@@ -37,11 +37,16 @@
"text_condition1": "Send a notification when",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_1min": "1 min",
"option_5min": "5 mins",
"option_10min": "10 mins",
"option_15min": "15 mins",
"option_30min": "30 mins",
"option_60min": "60 mins",
"option_3hours": "3 hours",
"option_4hours": "4 hours",
"option_6hours": "6 hours",
"option_12hours": "12 hours",
"option_24hours": "24 hours",
"field_threshold": "Alert Threshold",
"option_allthetimes": "all the times",
@@ -111,5 +116,8 @@
"exceptions_based_alert": "Exceptions-based Alert",
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
"field_unit": "Threshold unit",
"text_alert_on_absent": "Send a notification if data is missing for",
"text_alert_frequency": "Run alert every",
"text_for": "minutes",
"selected_query_placeholder": "Select query"
}

View File

@@ -0,0 +1,14 @@
{
"days_remaining": "days remaining in your billing period.",
"billing": "Billing",
"manage_billing_and_costs": "Manage your billing information, invoices, and monitor costs.",
"enterprise_cloud": "Enterprise Cloud",
"enterprise": "Enterprise",
"card_details_recieved_and_billing_info": "We have received your card details, your billing will only start after the end of your free trial period.",
"upgrade_plan": "Upgrade Plan",
"manage_billing": "Manage Billing",
"upgrade_now_text": "Upgrade now to have uninterrupted access",
"billing_start_info": "Your billing will start only after the trial period",
"checkout_plans": "Check out features in paid plans",
"here": "here"
}

View File

@@ -23,6 +23,12 @@
"field_opsgenie_api_key": "API Key",
"field_opsgenie_description": "Description",
"placeholder_opsgenie_description": "Description",
"help_email_to": "Email address(es) to send alerts to (comma separated)",
"field_email_to": "To",
"placeholder_email_to": "To",
"help_email_html": "Send email in html format",
"field_email_html": "Email body template",
"placeholder_email_html": "Email body template",
"field_webhook_username": "User Name (optional)",
"field_webhook_password": "Password (optional)",
"field_pager_routing_key": "Routing Key",

View File

@@ -28,5 +28,5 @@
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
"your_graph_build_with": "Your graph built with",
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
"dashboard_ok_confirm": "query will be saved. Press OK to confirm."
}

View File

@@ -14,6 +14,5 @@
"delete_domain_message": "Are you sure you want to delete this domain?",
"delete_domain": "Delete Domain",
"add_domain": "Add Domains",
"saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly",
"invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually"
"saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly"
}

View File

@@ -4,6 +4,10 @@
"SERVICE_METRICS": "SigNoz | Service Metrics",
"SERVICE_MAP": "SigNoz | Service Map",
"GET_STARTED": "SigNoz | Get Started",
"GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM",
"GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs",
"GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure",
"GET_STARTED_AWS_MONITORING": "SigNoz | Get Started | AWS",
"TRACE": "SigNoz | Trace",
"TRACE_DETAIL": "SigNoz | Trace Detail",
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
@@ -40,8 +44,9 @@
"LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"SUPPORT": "SigNoz | Support",
"LOGS_SAVE_VIEWS": "SigNoz | Logs Save Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Save Views",
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts"
"SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS_INSTALLED": "SigNoz | Integrations"
}

View File

@@ -9,7 +9,7 @@ done
# create temporary tsconfig which includes only passed files
str="{
\"extends\": \"./tsconfig.json\",
\"include\": [\"src/types/global.d.ts\",\"src/typings/window.ts\", \"src/typings/chartjs-adapter-date-fns.d.ts\", \"src/typings/environment.ts\" ,\"src/container/OnboardingContainer/typings.d.ts\",$files]
\"include\": [ \"src/typings/**/*.ts\",\"src/**/*.d.ts\", \"./babel.config.js\", \"./jest.config.ts\", \"./.eslintrc.js\",\"./__mocks__\",\"./conf/default.conf\",\"./public\",\"./tests\",\"./playwright.config.ts\",\"./commitlint.config.ts\",\"./webpack.config.js\",\"./webpack.config.prod.js\",\"./jest.setup.ts\",\"./**/*.d.ts\",$files]
}"
echo $str > tsconfig.tmp

View File

@@ -190,3 +190,18 @@ export const WorkspaceBlocked = Loadable(
export const ShortcutsPage = Loadable(
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
);
export const InstalledIntegrations = Loadable(
() =>
import(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
),
);
export const IntegrationsMarketPlace = Loadable(
// eslint-disable-next-line sonarjs/no-identical-functions
() =>
import(
/* webpackChunkName: "IntegrationsMarketPlace" */ 'pages/IntegrationsModulePage'
),
);

View File

@@ -1,6 +1,4 @@
import ROUTES from 'constants/routes';
import Shortcuts from 'pages/Shortcuts/Shortcuts';
import WorkspaceBlocked from 'pages/WorkspaceLocked';
import { RouteProps } from 'react-router-dom';
import {
@@ -16,6 +14,8 @@ import {
EditRulesPage,
ErrorDetails,
IngestionSettings,
InstalledIntegrations,
IntegrationsMarketPlace,
LicensePage,
ListAllALertsPage,
LiveLogs,
@@ -35,6 +35,7 @@ import {
ServiceMetricsPage,
ServicesTablePage,
SettingsPage,
ShortcutsPage,
SignupPage,
SomethingWentWrong,
StatusPage,
@@ -45,6 +46,7 @@ import {
TracesSaveViews,
UnAuthorized,
UsageExplorerPage,
WorkspaceBlocked,
} from './pageComponents';
const routes: AppRoutes[] = [
@@ -57,7 +59,7 @@ const routes: AppRoutes[] = [
},
{
path: ROUTES.GET_STARTED,
exact: true,
exact: false,
component: Onboarding,
isPrivate: true,
key: 'GET_STARTED',
@@ -331,10 +333,24 @@ const routes: AppRoutes[] = [
{
path: ROUTES.SHORTCUTS,
exact: true,
component: Shortcuts,
component: ShortcutsPage,
isPrivate: true,
key: 'SHORTCUTS',
},
{
path: ROUTES.INTEGRATIONS_INSTALLED,
exact: true,
component: InstalledIntegrations,
isPrivate: true,
key: 'INTEGRATIONS_INSTALLED',
},
{
path: ROUTES.INTEGRATIONS_MARKETPLACE,
exact: true,
component: IntegrationsMarketPlace,
isPrivate: true,
key: 'INTEGRATIONS_MARKETPLACE',
},
];
export const SUPPORT_ROUTE: AppRoutes = {
@@ -358,6 +374,8 @@ export const oldRoutes = [
'/logs/old-logs-explorer',
'/logs-explorer',
'/logs-explorer/live',
'/logs-save-views',
'/traces-save-views',
'/settings/api-keys',
];
@@ -366,6 +384,8 @@ export const oldNewRoutesMapping: Record<string, string> = {
'/logs/old-logs-explorer': '/logs/old-logs-explorer',
'/logs-explorer': '/logs/logs-explorer',
'/logs-explorer/live': '/logs/logs-explorer/live',
'/logs-save-views': '/logs/saved-views',
'/traces-save-views': '/traces/saved-views',
'/settings/api-keys': '/settings/access-tokens',
};

View File

@@ -30,7 +30,8 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
statusCode,
payload: null,
error: errorMessage,
message: null,
message: (response.data as any)?.status,
body: JSON.stringify((response.data as any).data),
};
}

View File

@@ -0,0 +1,7 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { AllIntegrationsProps } from 'types/api/integrations/types';
export const getAllIntegrations = (): Promise<
AxiosResponse<AllIntegrationsProps>
> => axios.get(`/integrations`);

View File

@@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
GetIntegrationPayloadProps,
GetIntegrationProps,
} from 'types/api/integrations/types';
export const getIntegration = (
props: GetIntegrationPayloadProps,
): Promise<AxiosResponse<GetIntegrationProps>> =>
axios.get(`/integrations/${props.integrationId}`);

View File

@@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
GetIntegrationPayloadProps,
GetIntegrationStatusProps,
} from 'types/api/integrations/types';
export const getIntegrationStatus = (
props: GetIntegrationPayloadProps,
): Promise<AxiosResponse<GetIntegrationStatusProps>> =>
axios.get(`/integrations/${props.integrationId}/connection_status`);

View File

@@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
InstalledIntegrationsSuccessResponse,
InstallIntegrationKeyProps,
} from 'types/api/integrations/types';
const installIntegration = async (
props: InstallIntegrationKeyProps,
): Promise<
SuccessResponse<InstalledIntegrationsSuccessResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/integrations/install', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default installIntegration;

View File

@@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UninstallIntegrationProps,
UninstallIntegrationSuccessResponse,
} from 'types/api/integrations/types';
const unInstallIntegration = async (
props: UninstallIntegrationProps,
): Promise<
SuccessResponse<UninstallIntegrationSuccessResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/integrations/uninstall', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default unInstallIntegration;

View File

@@ -8,7 +8,7 @@ const listAllDomain = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(`orgs/${props.orgId}/domains`);
const response = await axios.get(`/orgs/${props.orgId}/domains`);
return {
statusCode: 200,

View File

@@ -2,6 +2,7 @@ const apiV1 = '/api/v1/';
export const apiV2 = '/api/v2/';
export const apiV3 = '/api/v3/';
export const apiV4 = '/api/v4/';
export const apiAlertManager = '/api/alertmanager';
export default apiV1;

View File

@@ -13,6 +13,7 @@ export interface UsageResponsePayloadProps {
billTotal: number;
};
discount: number;
subscriptionStatus?: string;
}
const getUsage = async (

View File

@@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createEmail';
const create = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/channels', {
name: props.name,
email_configs: [
{
send_resolved: true,
to: props.to,
html: props.html,
headers: props.headers,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default create;

View File

@@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editEmail';
const editEmail = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.put(`/channels/${props.id}`, {
name: props.name,
email_configs: [
{
send_resolved: true,
to: props.to,
html: props.html,
headers: props.headers,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default editEmail;

View File

@@ -0,0 +1,34 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createEmail';
const testEmail = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/testChannel', {
name: props.name,
email_configs: [
{
send_resolved: true,
to: props.to,
html: props.html,
headers: props.headers,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default testEmail;

View File

@@ -9,7 +9,7 @@ import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import store from 'store';
import apiV1, { apiAlertManager, apiV2, apiV3 } from './apiV1';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4 } from './apiV1';
import { Logout } from './utils';
const interceptorsResponse = (
@@ -114,6 +114,7 @@ ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
export const ApiV3Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV3}`,
});
ApiV3Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
@@ -121,6 +122,18 @@ ApiV3Instance.interceptors.response.use(
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
//
// axios V4
export const ApiV4Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV4}`,
});
ApiV4Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
);
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
//
AxiosAlertManagerInstance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,

View File

@@ -1,6 +1,7 @@
import { ApiV3Instance as axios } from 'api';
import { ApiV3Instance, ApiV4Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricRangePayloadV3,
@@ -9,10 +10,23 @@ import {
export const getMetricsQueryRange = async (
props: QueryRangePayload,
version: string,
signal: AbortSignal,
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
try {
const response = await axios.post('/query_range', props, { signal });
if (version && version === ENTITY_VERSION_V4) {
const response = await ApiV4Instance.post('/query_range', props, { signal });
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
}
const response = await ApiV3Instance.post('/query_range', props, { signal });
return {
statusCode: 200,

View File

@@ -24,7 +24,7 @@ export const getAggregateAttribute = async ({
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get(
`autocomplete/aggregate_attributes?${createQueryParams({
`/autocomplete/aggregate_attributes?${createQueryParams({
aggregateOperator,
searchText,
dataSource,

View File

@@ -25,7 +25,7 @@ export const getAggregateKeys = async ({
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get(
`autocomplete/attribute_keys?${createQueryParams({
`/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator,
searchText,
dataSource,

View File

@@ -2,4 +2,4 @@ import axios from 'api';
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> =>
axios.delete(`explorer/views/${uuid}`);
axios.delete(`/explorer/views/${uuid}`);

View File

@@ -6,4 +6,4 @@ import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = (
sourcepage: DataSource,
): Promise<AxiosResponse<AllViewsProps>> =>
axios.get(`explorer/views?sourcePage=${sourcepage}`);
axios.get(`/explorer/views?sourcePage=${sourcepage}`);

View File

@@ -8,7 +8,7 @@ export const saveView = ({
viewName,
extraData,
}: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> =>
axios.post('explorer/views', {
axios.post('/explorer/views', {
name: viewName,
sourcePage,
compositeQuery,

View File

@@ -11,7 +11,7 @@ export const updateView = ({
sourcePage,
viewKey,
}: UpdateViewProps): Promise<UpdateViewPayloadProps> =>
axios.put(`explorer/views/${viewKey}`, {
axios.put(`/explorer/views/${viewKey}`, {
name: viewName,
compositeQuery,
extraData,

View File

@@ -0,0 +1,23 @@
import { Color } from '@signozhq/design-tokens';
import { useIsDarkMode } from 'hooks/useDarkMode';
function ConfigureIcon(): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500}
strokeWidth="1.333"
strokeLinecap="round"
>
<path
d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z"
strokeLinejoin="round"
/>
<path d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3M10.75 10.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z" />
</g>
</svg>
);
}
export default ConfigureIcon;

View File

@@ -5,13 +5,14 @@ import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { Options } from 'container/TopNav/DateTimeSelection/config';
import {
FixedDurationSuggestionOptions,
Options,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { defaultTo, noop } from 'lodash-es';
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 {
@@ -33,7 +34,14 @@ interface CustomTimePickerProps {
onError: (value: boolean) => void;
selectedValue: string;
selectedTime: string;
onValidCustomDateChange: ([t1, t2]: any[]) => void;
onValidCustomDateChange: ({
time: [t1, t2],
timeStr,
}: {
time: [dayjs.Dayjs | null, dayjs.Dayjs | null];
timeStr: string;
}) => void;
onCustomTimeStatusUpdate?: (isValid: boolean) => void;
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
items: any[];
@@ -53,6 +61,7 @@ function CustomTimePicker({
open,
setOpen,
onValidCustomDateChange,
onCustomTimeStatusUpdate,
newPopover,
customDateTimeVisible,
setCustomDTPickerVisible,
@@ -85,6 +94,7 @@ function CustomTimePicker({
return Options[index].label;
}
}
for (
let index = 0;
index < RelativeDurationSuggestionOptions.length;
@@ -94,12 +104,17 @@ function CustomTimePicker({
return RelativeDurationSuggestionOptions[index].label;
}
}
for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) {
if (FixedDurationSuggestionOptions[index].value === selectedTime) {
return FixedDurationSuggestionOptions[index].label;
}
}
if (isValidTimeFormat(selectedTime)) {
return selectedTime;
}
return '';
};
@@ -115,6 +130,9 @@ function CustomTimePicker({
const handleOpenChange = (newOpen: boolean): void => {
setOpen(newOpen);
if (!newOpen) {
setCustomDTPickerVisible?.(false);
}
};
const debouncedHandleInputChange = debounce((inputValue): void => {
@@ -158,13 +176,22 @@ function CustomTimePicker({
setInputStatus('error');
onError(true);
setInputErrorMessage('Please enter time less than 6 months');
if (isFunction(onCustomTimeStatusUpdate)) {
onCustomTimeStatusUpdate(true);
}
} else {
onValidCustomDateChange([minTime, currentTime]);
onValidCustomDateChange({
time: [minTime, currentTime],
timeStr: inputValue,
});
}
} else {
setInputStatus('error');
onError(true);
setInputErrorMessage(null);
if (isFunction(onCustomTimeStatusUpdate)) {
onCustomTimeStatusUpdate(false);
}
}
}, 300);
@@ -317,4 +344,5 @@ CustomTimePicker.defaultProps = {
setCustomDTPickerVisible: noop,
onCustomDateHandler: noop,
handleGoLive: noop,
onCustomTimeStatusUpdate: noop,
};

View File

@@ -1,6 +1,6 @@
import './CustomTimePicker.styles.scss';
import { Button, DatePicker } from 'antd';
import { Button } from 'antd';
import cx from 'classnames';
import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
@@ -9,12 +9,10 @@ import {
Option,
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import RangePickerModal from './RangePickerModal';
interface CustomTimePickerPopoverContentProps {
options: any[];
@@ -40,35 +38,12 @@ function CustomTimePickerPopoverContent({
handleGoLive,
selectedTime,
}: CustomTimePickerPopoverContentProps): JSX.Element {
const { RangePicker } = DatePicker;
const { pathname } = useLocation();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const disabledDate = (current: Dayjs): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
const onPopoverClose = (visible: boolean): void => {
if (!visible) {
setCustomDTPickerVisible(false);
}
setIsOpen(visible);
};
const onModalOkHandler = (date_time: any): void => {
if (date_time?.[1]) {
onPopoverClose(false);
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
function getTimeChips(options: Option[]): JSX.Element {
return (
<div className="relative-date-time-section">
@@ -105,26 +80,32 @@ function CustomTimePickerPopoverContent({
}}
className={cx(
'date-time-options-btn',
selectedTime === option.value && 'active',
customDateTimeVisible
? option.value === 'custom' && 'active'
: selectedTime === option.value && 'active',
)}
>
{option.label}
</Button>
))}
</div>
<div className="relative-date-time">
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePicker
disabledDate={disabledDate}
allowClear
onCalendarChange={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
})}
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
/>
) : (
<div>
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>

View File

@@ -0,0 +1,4 @@
.custom-date-picker {
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,68 @@
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 { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
interface RangePickerModalProps {
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler: (
dateTimeRange: DateTimeRangeType,
lexicalContext?: LexicalContext | undefined,
) => void;
selectedTime: string;
}
function RangePickerModal(props: RangePickerModalProps): JSX.Element {
const {
setCustomDTPickerVisible,
setIsOpen,
onCustomDateHandler,
selectedTime,
} = props;
const { RangePicker } = DatePicker;
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const disabledDate = (current: Dayjs): boolean => {
const currentDay = dayjs(current);
return currentDay.isAfter(dayjs());
};
const onPopoverClose = (visible: boolean): void => {
if (!visible) {
setCustomDTPickerVisible(false);
}
setIsOpen(visible);
};
const onModalOkHandler = (date_time: any): void => {
if (date_time?.[1]) {
onPopoverClose(false);
}
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
};
return (
<div className="custom-date-picker">
<RangePicker
disabledDate={disabledDate}
allowClear
showTime
onOk={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
})}
/>
</div>
);
}
export default RangePickerModal;

View File

@@ -18,6 +18,8 @@
}
.ant-drawer-body {
display: flex;
flex-direction: column;
padding: 16px;
}

View File

@@ -1,10 +1,13 @@
.query-builder-search-wrapper {
margin-top: 10px;
height: 46px;
border: 1px solid var(--bg-slate-400);
border-bottom: none;
margin-top: 10px;
border: 1px solid var(--bg-slate-400);
border-bottom: none;
.ant-select-selector {
border: none !important;
}
}
.ant-select-selector {
border: none !important;
input {
font-size: 12px;
}
}
}

View File

@@ -9,6 +9,7 @@ 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 { useCallback, useMemo, useState } from 'react';
@@ -19,9 +20,8 @@ import { ILog } from 'types/api/logs/log';
// components
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
import { getLogIndicatorType } from '../LogStateIndicator/utils';
// styles
import {
Container,
@@ -37,12 +37,17 @@ const convert = new Convert();
interface LogFieldProps {
fieldKey: string;
fieldValue: string;
linesPerRow?: number;
}
type LogSelectedFieldProps = LogFieldProps &
type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
Pick<AddToQueryHOCProps, 'onAddToQuery'>;
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
function LogGeneralField({
fieldKey,
fieldValue,
linesPerRow = 1,
}: LogFieldProps): JSX.Element {
const html = useMemo(
() => ({
__html: convert.toHtml(dompurify.sanitize(fieldValue)),
@@ -55,7 +60,11 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
<Text ellipsis type="secondary" className="log-field-key">
{`${fieldKey} : `}
</Text>
<LogText dangerouslySetInnerHTML={html} className="log-value" />
<LogText
dangerouslySetInnerHTML={html}
className="log-value"
linesPerRow={linesPerRow > 1 ? linesPerRow : undefined}
/>
</TextContainer>
);
}
@@ -92,6 +101,7 @@ type ListLogViewProps = {
onSetActiveLog: (log: ILog) => void;
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
activeLog?: ILog | null;
linesPerRow: number;
};
function ListLogView({
@@ -100,6 +110,7 @@ function ListLogView({
onSetActiveLog,
onAddToQuery,
activeLog,
linesPerRow,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -114,6 +125,8 @@ function ListLogView({
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
const handlerClearActiveContextLog = useCallback(
(event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
@@ -149,7 +162,7 @@ function ListLogView({
[flattenLogData.timestamp],
);
const logType = logData?.attributes_string?.log_level || LogType.INFO;
const logType = getLogIndicatorType(logData);
const handleMouseEnter = (): void => {
setHasActionButtons(true);
@@ -163,6 +176,7 @@ function ListLogView({
<>
<Container
$isActiveLog={isHighlighted}
$isDarkMode={isDarkMode}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleDetailedView}
@@ -176,7 +190,11 @@ function ListLogView({
/>
<div>
<LogContainer>
<LogGeneralField fieldKey="Log" fieldValue={flattenLogData.body} />
<LogGeneralField
fieldKey="Log"
fieldValue={flattenLogData.body}
linesPerRow={linesPerRow}
/>
{flattenLogData.stream && (
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
)}
@@ -219,4 +237,8 @@ ListLogView.defaultProps = {
activeLog: null,
};
LogGeneralField.defaultProps = {
linesPerRow: 1,
};
export default ListLogView;

View File

@@ -1,23 +1,33 @@
import { Color } from '@signozhq/design-tokens';
import { Card, Typography } from 'antd';
import styled from 'styled-components';
import { getActiveLogBackground } from 'utils/logs';
interface LogTextProps {
linesPerRow?: number;
}
export const Container = styled(Card)<{
$isActiveLog: boolean;
$isDarkMode: boolean;
}>`
width: 100% !important;
margin-bottom: 0.3rem;
cursor: pointer;
.ant-card-body {
padding: 0.3rem 0.6rem;
}
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
${({ $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important`
: ''}
}
`;
export const Text = styled(Typography.Text)`
&&& {
min-width: 1.5rem;
min-width: 2.5rem;
white-space: nowrap;
}
`;
@@ -35,11 +45,19 @@ export const LogContainer = styled.div`
gap: 6px;
`;
export const LogText = styled.div`
export const LogText = styled.div<LogTextProps>`
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
${({ linesPerRow }): string =>
linesPerRow
? `-webkit-line-clamp: ${linesPerRow};
line-clamp: ${linesPerRow};
display: -webkit-box;
-webkit-box-orient: vertical;
white-space: normal; `
: 'white-space: nowrap;'};
};
`;
export const SelectedLog = styled.div`

View File

@@ -10,15 +10,27 @@
background-color: transparent;
&.INFO {
background-color: #1d212d;
background-color: var(--bg-slate-400);
}
&.WARNING {
background-color: #ffcd56;
&.WARNING, &.WARN {
background-color: var(--bg-amber-500);
}
&.ERROR {
background-color: #e5484d;
background-color: var(--bg-cherry-500);
}
&.TRACE {
background-color: var(--bg-robin-300);
}
&.DEBUG {
background-color: var(--bg-forest-500);
}
&.FATAL {
background-color: var(--bg-sakura-500);
}
}

View File

@@ -0,0 +1,45 @@
import { render } from '@testing-library/react';
import LogStateIndicator from './LogStateIndicator';
describe('LogStateIndicator', () => {
it('renders correctly with default props', () => {
const { container } = render(<LogStateIndicator type="INFO" />);
const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('log-state-indicator')).toBe(true);
expect(indicator.classList.contains('isActive')).toBe(false);
expect(container.querySelector('.line')).toBeTruthy();
expect(container.querySelector('.line')?.classList.contains('INFO')).toBe(
true,
);
});
it('renders correctly when isActive is true', () => {
const { container } = render(<LogStateIndicator type="INFO" isActive />);
const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('isActive')).toBe(true);
});
it('renders correctly with different types', () => {
const { container: containerInfo } = render(
<LogStateIndicator type="INFO" />,
);
expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe(
true,
);
const { container: containerWarning } = render(
<LogStateIndicator type="WARNING" />,
);
expect(
containerWarning.querySelector('.line')?.classList.contains('WARNING'),
).toBe(true);
const { container: containerError } = render(
<LogStateIndicator type="ERROR" />,
);
expect(
containerError.querySelector('.line')?.classList.contains('ERROR'),
).toBe(true);
});
});

View File

@@ -2,11 +2,40 @@ import './LogStateIndicator.styles.scss';
import cx from 'classnames';
export const SEVERITY_TEXT_TYPE = {
TRACE: 'TRACE',
TRACE2: 'TRACE2',
TRACE3: 'TRACE3',
TRACE4: 'TRACE4',
DEBUG: 'DEBUG',
DEBUG2: 'DEBUG2',
DEBUG3: 'DEBUG3',
DEBUG4: 'DEBUG4',
INFO: 'INFO',
INFO2: 'INFO2',
INFO3: 'INFO3',
INFO4: 'INFO4',
WARN: 'WARN',
WARN2: 'WARN2',
WARN3: 'WARN3',
WARN4: 'WARN4',
WARNING: 'WARNING',
ERROR: 'ERROR',
ERROR2: 'ERROR2',
ERROR3: 'ERROR3',
ERROR4: 'ERROR4',
FATAL: 'FATAL',
FATAL2: 'FATAL2',
FATAL3: 'FATAL3',
FATAL4: 'FATAL4',
} as const;
export const LogType = {
INFO: 'INFO',
WARNING: 'WARNING',
ERROR: 'ERROR',
};
} as const;
function LogStateIndicator({
type,
isActive,

View File

@@ -0,0 +1,89 @@
import { ILog } from 'types/api/logs/log';
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';
describe('getLogIndicatorType', () => {
it('should return severity type for valid log with severityText', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityText: 'INFO',
severityNumber: 2,
body: 'Sample log Message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'INFO',
};
expect(getLogIndicatorType(log)).toBe('INFO');
});
it('should return log level if severityText is missing', () => {
const log: ILog = {
date: '2024-02-29T12:34:58Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityNumber: 2,
body: 'Sample log',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'FATAL',
severityText: '',
};
expect(getLogIndicatorType(log)).toBe('FATAL');
});
});
describe('getLogIndicatorTypeForTable', () => {
it('should return severity type for valid log with severityText', () => {
const log = {
date: '2024-02-29T12:34:56Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severity_number: 2,
body: 'Sample log message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'WARN',
};
expect(getLogIndicatorTypeForTable(log)).toBe('WARN');
});
it('should return log level if severityText is missing', () => {
const log = {
date: '2024-02-29T12:34:56Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityNumber: 2,
body: 'Sample log message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
log_level: 'INFO',
};
expect(getLogIndicatorTypeForTable(log)).toBe('INFO');
});
});

View File

@@ -0,0 +1,57 @@
import { ILog } from 'types/api/logs/log';
import { LogType, SEVERITY_TEXT_TYPE } from './LogStateIndicator';
const getSeverityType = (severityText: string): string => {
switch (severityText) {
case SEVERITY_TEXT_TYPE.TRACE:
case SEVERITY_TEXT_TYPE.TRACE2:
case SEVERITY_TEXT_TYPE.TRACE3:
case SEVERITY_TEXT_TYPE.TRACE4:
return SEVERITY_TEXT_TYPE.TRACE;
case SEVERITY_TEXT_TYPE.DEBUG:
case SEVERITY_TEXT_TYPE.DEBUG2:
case SEVERITY_TEXT_TYPE.DEBUG3:
case SEVERITY_TEXT_TYPE.DEBUG4:
return SEVERITY_TEXT_TYPE.DEBUG;
case SEVERITY_TEXT_TYPE.INFO:
case SEVERITY_TEXT_TYPE.INFO2:
case SEVERITY_TEXT_TYPE.INFO3:
case SEVERITY_TEXT_TYPE.INFO4:
return SEVERITY_TEXT_TYPE.INFO;
case SEVERITY_TEXT_TYPE.WARN:
case SEVERITY_TEXT_TYPE.WARN2:
case SEVERITY_TEXT_TYPE.WARN3:
case SEVERITY_TEXT_TYPE.WARN4:
case SEVERITY_TEXT_TYPE.WARNING:
return SEVERITY_TEXT_TYPE.WARN;
case SEVERITY_TEXT_TYPE.ERROR:
case SEVERITY_TEXT_TYPE.ERROR2:
case SEVERITY_TEXT_TYPE.ERROR3:
case SEVERITY_TEXT_TYPE.ERROR4:
return SEVERITY_TEXT_TYPE.ERROR;
case SEVERITY_TEXT_TYPE.FATAL:
case SEVERITY_TEXT_TYPE.FATAL2:
case SEVERITY_TEXT_TYPE.FATAL3:
case SEVERITY_TEXT_TYPE.FATAL4:
return SEVERITY_TEXT_TYPE.FATAL;
default:
return SEVERITY_TEXT_TYPE.INFO;
}
};
export const getLogIndicatorType = (logData: ILog): string => {
if (logData.severity_text) {
return getSeverityType(logData.severity_text);
}
return logData.attributes_string?.log_level || LogType.INFO;
};
export const getLogIndicatorTypeForTable = (
log: Record<string, unknown>,
): string => {
if (log.severity_text) {
return getSeverityType(log.severity_text as string);
}
return (log.log_level as string) || LogType.INFO;
};

View File

@@ -23,9 +23,8 @@ import {
} from 'react';
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
import { getLogIndicatorType } from '../LogStateIndicator/utils';
// styles
import { RawLogContent, RawLogViewContainer } from './styles';
import { RawLogViewProps } from './types';
@@ -64,7 +63,7 @@ function RawLogView({
const severityText = data.severity_text ? `${data.severity_text} |` : '';
const logType = data?.attributes_string?.log_level || LogType.INFO;
const logType = getLogIndicatorType(data);
const updatedSelecedFields = useMemo(
() => selectedFields.filter((e) => e.name !== 'id'),
@@ -164,7 +163,11 @@ function RawLogView({
>
<LogStateIndicator
type={logType}
isActive={activeLog?.id === data.id || activeContextLog?.id === data.id}
isActive={
activeLog?.id === data.id ||
activeContextLog?.id === data.id ||
isActiveLog
}
/>
<RawLogContent

View File

@@ -30,6 +30,14 @@ export const RawLogViewContainer = styled(Row)<{
$isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode)
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
${({ $isHightlightedLog, $isDarkMode }): string =>
$isHightlightedLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
};
transition: background-color 2s ease-in;`
: ''}
`;
export const ExpandIconWrapper = styled(Col)`

View File

@@ -14,12 +14,12 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
lineHeight: '18px',
letterSpacing: '-0.07px',
marginBottom: '0px',
minWidth: '10rem',
};
}
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',
maxWidth: '40rem',
};
export const defaultListViewPanelStyle: CSSProperties = {

View File

@@ -23,6 +23,7 @@ export type UseTableViewProps = {
onOpenLogsContext?: (log: ILog) => void;
onClickExpand?: (log: ILog) => void;
activeLog?: ILog | null;
activeLogIndex?: number;
activeContextLog?: ILog | null;
isListViewPanel?: boolean;
} & LogsTableViewProps;

View File

@@ -7,12 +7,10 @@ import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { defaultTo } from 'lodash-es';
import { useMemo } from 'react';
import LogStateIndicator, {
LogType,
} from '../LogStateIndicator/LogStateIndicator';
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
import { getLogIndicatorTypeForTable } from '../LogStateIndicator/utils';
import {
defaultListViewPanelStyle,
defaultTableStyle,
@@ -84,7 +82,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
children: (
<div className="table-timestamp">
<LogStateIndicator
type={defaultTo(item.log_level, LogType.INFO) as string}
type={getLogIndicatorTypeForTable(item)}
isActive={
activeLog?.id === item.id || activeContextLog?.id === item.id
}

View File

@@ -27,7 +27,7 @@
line-height: 18px;
letter-spacing: 0.08em;
text-align: left;
color: var(--bg-slate-200, #52575c);
color: #52575c;
}
.menu-items {
@@ -65,7 +65,7 @@
padding: 12px;
.title {
color: var(--bg-slate-200, #52575c);
color: #52575c;
font-family: Inter;
font-size: 11px;
font-style: normal;
@@ -149,7 +149,7 @@
}
.title {
color: var(--bg-slate-200, #52575c);
color: #52575c;
font-family: Inter;
font-size: 11px;
font-style: normal;

View File

@@ -72,8 +72,6 @@ export default function LogsFormatOptionsMenu({
setAddNewColumn(!addNewColumn);
};
// console.log('optionsMenuConfig', config);
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
if (
maxLinesPerRow &&
@@ -122,38 +120,36 @@ export default function LogsFormatOptionsMenu({
{selectedItem && (
<>
{selectedItem === 'raw' && (
<>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
<>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
</>
)}
</div>
</>
<div className="selected-item-content-container active">
{!addNewColumn && <div className="horizontal-line" />}
@@ -221,8 +217,6 @@ export default function LogsFormatOptionsMenu({
className="column-name"
key={value}
onClick={(eve): void => {
console.log('coluimn name', label, value);
eve.stopPropagation();
if (addColumn && addColumn?.onSelect) {

View File

@@ -1,10 +1,12 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import ReactMarkdown from 'react-markdown';
import { CodeProps } from 'react-markdown/lib/ast-to-react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import rehypeRaw from 'rehype-raw';
import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn';
@@ -74,6 +76,10 @@ const interpolateMarkdown = (
return interpolatedContent;
};
function CustomTag({ color }: { color: string }): JSX.Element {
return <h1 style={{ color }}>This is custom element</h1>;
}
function MarkdownRenderer({
markdownContent,
variables,
@@ -85,12 +91,14 @@ function MarkdownRenderer({
return (
<ReactMarkdown
rehypePlugins={[rehypeRaw as any]}
components={{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
a: Link,
pre: Pre,
code: Code,
customtag: CustomTag,
}}
>
{interpolatedMarkdown}

View File

@@ -13,3 +13,6 @@ export const SIGNOZ_UPGRADE_PLAN_URL =
'https://upgrade.signoz.io/upgrade-from-app';
export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
export const DEFAULT_ENTITY_VERSION = 'v3';
export const ENTITY_VERSION_V4 = 'v4';

View File

@@ -16,4 +16,6 @@ export enum LOCALSTORAGE {
CHAT_SUPPORT = 'CHAT_SUPPORT',
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
}

View File

@@ -27,5 +27,7 @@ export enum QueryParams {
viewName = 'viewName',
viewKey = 'viewKey',
expandedWidgetId = 'expandedWidgetId',
integration = 'integration',
pagination = 'pagination',
relativeTime = 'relativeTime',
}

View File

@@ -36,6 +36,11 @@ import { v4 as uuid } from 'uuid';
import {
logsAggregateOperatorOptions,
metricAggregateOperatorOptions,
metricsGaugeAggregateOperatorOptions,
metricsGaugeSpaceAggregateOperatorOptions,
metricsHistogramSpaceAggregateOperatorOptions,
metricsSumAggregateOperatorOptions,
metricsSumSpaceAggregateOperatorOptions,
tracesAggregateOperatorOptions,
} from './queryBuilderOperators';
@@ -74,6 +79,18 @@ export const mapOfOperators = {
traces: tracesAggregateOperatorOptions,
};
export const metricsOperatorsByType = {
Sum: metricsSumAggregateOperatorOptions,
Gauge: metricsGaugeAggregateOperatorOptions,
};
export const metricsSpaceAggregationOperatorsByType = {
Sum: metricsSumSpaceAggregateOperatorOptions,
Gauge: metricsGaugeSpaceAggregateOperatorOptions,
Histogram: metricsHistogramSpaceAggregateOperatorOptions,
ExponentialHistogram: metricsHistogramSpaceAggregateOperatorOptions,
};
export const mapOfQueryFilters: Record<DataSource, QueryAdditionalFilter[]> = {
metrics: [
// eslint-disable-next-line sonarjs/no-duplicate-string
@@ -148,6 +165,9 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
aggregateOperator: MetricAggregateOperator.COUNT,
aggregateAttribute: initialAutocompleteData,
timeAggregation: MetricAggregateOperator.RATE,
spaceAggregation: MetricAggregateOperator.SUM,
functions: [],
filters: { items: [], op: 'AND' },
expression: createNewBuilderItemName({
existNames: [],
@@ -160,7 +180,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'sum',
reduceTo: 'avg',
};
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
@@ -268,6 +288,14 @@ export enum PANEL_TYPES {
EMPTY_WIDGET = 'EMPTY_WIDGET',
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum ATTRIBUTE_TYPES {
SUM = 'Sum',
GAUGE = 'Gauge',
HISTOGRAM = 'Histogram',
EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram',
}
export type IQueryBuilderState = 'search';
export const QUERY_BUILDER_SEARCH_VALUES = {

View File

@@ -302,3 +302,126 @@ export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
label: 'Rate_max',
},
];
export const metricsSumAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.RATE,
label: 'Rate',
},
{
value: MetricAggregateOperator.INCREASE,
label: 'Increase',
},
];
export const metricsGaugeAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.LATEST,
label: 'Latest',
},
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
{
value: MetricAggregateOperator.COUNT,
label: 'Count',
},
{
value: MetricAggregateOperator.COUNT_DISTINCT,
label: 'Count Distinct',
},
];
export const metricsSumSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
];
export const metricsGaugeSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
];
export const metricsHistogramSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.P50,
label: 'P50',
},
{
value: MetricAggregateOperator.P75,
label: 'P75',
},
{
value: MetricAggregateOperator.P90,
label: 'P90',
},
{
value: MetricAggregateOperator.P95,
label: 'P95',
},
{
value: MetricAggregateOperator.P99,
label: 'P99',
},
];
export const metricsEmptyTimeAggregateOperatorOptions: SelectOption<
string,
string
>[] = [];

View File

@@ -0,0 +1,137 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { QueryFunctionsTypes } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
export const queryFunctionOptions: SelectOption<string, string>[] = [
{
value: QueryFunctionsTypes.CUTOFF_MIN,
label: 'Cut Off Min',
},
{
value: QueryFunctionsTypes.CUTOFF_MAX,
label: 'Cut Off Max',
},
{
value: QueryFunctionsTypes.CLAMP_MIN,
label: 'Clamp Min',
},
{
value: QueryFunctionsTypes.CLAMP_MAX,
label: 'Clamp Max',
},
{
value: QueryFunctionsTypes.ABSOLUTE,
label: 'Absolute',
},
{
value: QueryFunctionsTypes.LOG_2,
label: 'Log2',
},
{
value: QueryFunctionsTypes.LOG_10,
label: 'Log10',
},
{
value: QueryFunctionsTypes.CUMULATIVE_SUM,
label: 'Cumulative Sum',
},
{
value: QueryFunctionsTypes.EWMA_3,
label: 'EWMA 3',
},
{
value: QueryFunctionsTypes.EWMA_5,
label: 'EWMA 5',
},
{
value: QueryFunctionsTypes.EWMA_7,
label: 'EWMA 7',
},
{
value: QueryFunctionsTypes.MEDIAN_3,
label: 'Median 3',
},
{
value: QueryFunctionsTypes.MEDIAN_5,
label: 'Median 5',
},
{
value: QueryFunctionsTypes.MEDIAN_7,
label: 'Median 7',
},
{
value: QueryFunctionsTypes.TIME_SHIFT,
label: 'Time Shift',
},
];
interface QueryFunctionConfigType {
[key: string]: {
showInput: boolean;
inputType?: string;
placeholder?: string;
};
}
export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
cutOffMin: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
cutOffMax: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
clampMin: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
clampMax: {
showInput: true,
inputType: 'text',
placeholder: 'Threshold',
},
absolute: {
showInput: false,
},
log2: {
showInput: false,
},
log10: {
showInput: false,
},
cumSum: {
showInput: false,
},
ewma3: {
showInput: true,
inputType: 'text',
placeholder: 'Alpha',
},
ewma5: {
showInput: true,
inputType: 'text',
placeholder: 'Alpha',
},
ewma7: {
showInput: true,
inputType: 'text',
placeholder: 'Alpha',
},
median3: {
showInput: false,
},
median5: {
showInput: false,
},
median7: {
showInput: false,
},
timeShift: {
showInput: true,
inputType: 'text',
},
};

View File

@@ -7,6 +7,11 @@ const ROUTES = {
TRACE_DETAIL: '/trace/:id',
TRACES_EXPLORER: '/traces-explorer',
GET_STARTED: '/get-started',
GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring',
GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management',
GET_STARTED_INFRASTRUCTURE_MONITORING:
'/get-started/infrastructure-monitoring',
GET_STARTED_AWS_MONITORING: '/get-started/aws-monitoring',
USAGE_EXPLORER: '/usage-explorer',
APPLICATION: '/services',
ALL_DASHBOARD: '/dashboard',
@@ -42,10 +47,13 @@ const ROUTES = {
TRACE_EXPLORER: '/trace-explorer',
BILLING: '/billing',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs-save-views',
TRACES_SAVE_VIEWS: '/traces-save-views',
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',
WORKSPACE_LOCKED: '/workspace-locked',
SHORTCUTS: '/shortcuts',
INTEGRATIONS_BASE: '/integrations',
INTEGRATIONS_INSTALLED: '/integrations/installed',
INTEGRATIONS_MARKETPLACE: '/integrations/marketplace',
} as const;
export default ROUTES;

View File

@@ -0,0 +1,17 @@
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
const userOS = getUserOperatingSystem();
export const DashboardShortcuts = {
SaveChanges: 's+meta',
DiscardChanges: 'd+meta',
};
export const DashboardShortcutsName = {
SaveChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+s`,
};
export const DashboardShortcutsDescription = {
SaveChanges: 'Save Changes',
DiscardChanges: 'Discard Changes',
};

View File

@@ -0,0 +1,17 @@
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
const userOS = getUserOperatingSystem();
export const QBShortcuts = {
StageAndRunQuery: 'enter+meta',
};
export const QBShortcutsName = {
StageAndRunQuery: `${
userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'
}+enter`,
};
export const QBShortcutsDescription = {
StageAndRunQuery: 'Stage and Run the query',
};

View File

@@ -231,7 +231,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
const pageTitle = t(routeKey);
const renderFullScreen =
pathname === ROUTES.GET_STARTED || pathname === ROUTES.WORKSPACE_LOCKED;
pathname === ROUTES.GET_STARTED ||
pathname === ROUTES.WORKSPACE_LOCKED ||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
pathname === ROUTES.GET_STARTED_AWS_MONITORING;
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);

View File

@@ -1,13 +1,29 @@
.billing-container {
padding: 16px 0;
width: 100%;
padding-top: 36px;
width: 65%;
.billing-summary {
margin: 24px 8px;
}
.billing-details {
margin: 36px 8px;
margin: 24px 0px;
.ant-table-title {
color: var(--bg-vanilla-400);
background-color: rgb(27, 28, 32);
}
.ant-table-cell {
background-color: var(--bg-ink-400);
border-color: var(--bg-slate-500);
}
.ant-table-tbody {
td {
border-color: var(--bg-slate-500);
}
}
}
.upgrade-plan-benefits {
@@ -24,6 +40,15 @@
}
}
}
.empty-graph-card {
.ant-card-body {
height: 40vh;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
@@ -34,3 +59,20 @@
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
min-width: 100% !important;
}
.lightMode {
.billing-container {
.billing-details {
.ant-table-cell {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-200);
}
.ant-table-tbody {
td {
border-color: var(--bg-vanilla-200);
}
}
}
}
}

View File

@@ -12,13 +12,36 @@ import BillingContainer from './BillingContainer';
const lisenceUrl = 'http://localhost/api/v2/licenses';
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
describe('BillingContainer', () => {
test('Component should render', async () => {
act(() => {
render(<BillingContainer />);
});
const unit = screen.getAllByText(/unit/i);
expect(unit[1]).toBeInTheDocument();
const dataInjection = screen.getByRole('columnheader', {
name: /data ingested/i,
});
@@ -32,24 +55,15 @@ describe('BillingContainer', () => {
});
expect(cost).toBeInTheDocument();
const total = screen.getByRole('cell', {
name: /total/i,
});
expect(total).toBeInTheDocument();
const manageBilling = screen.getByRole('button', {
name: /manage billing/i,
name: 'manage_billing',
});
expect(manageBilling).toBeInTheDocument();
const dollar = screen.getByRole('cell', {
name: /\$0/i,
});
const dollar = screen.getByText(/\$0/i);
expect(dollar).toBeInTheDocument();
const currentBill = screen.getByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('billing');
expect(currentBill).toBeInTheDocument();
});
@@ -61,9 +75,7 @@ describe('BillingContainer', () => {
const freeTrailText = await screen.findByText('Free Trial');
expect(freeTrailText).toBeInTheDocument();
const currentBill = await screen.findByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i);
@@ -73,18 +85,14 @@ describe('BillingContainer', () => {
);
expect(onTrail).toBeInTheDocument();
const numberOfDayRemaining = await screen.findByText(
/1 days remaining in your billing period./i,
);
const numberOfDayRemaining = await screen.findByText(/1 days_remaining/i);
expect(numberOfDayRemaining).toBeInTheDocument();
const upgradeButton = await screen.findAllByRole('button', {
name: /upgrade/i,
name: /upgrade_plan/i,
});
expect(upgradeButton[1]).toBeInTheDocument();
expect(upgradeButton.length).toBe(2);
const checkPaidPlan = await screen.findByText(
/Check out features in paid plans/i,
);
const checkPaidPlan = await screen.findByText(/checkout_plans/i);
expect(checkPaidPlan).toBeInTheDocument();
const link = screen.getByRole('link', { name: /here/i });
@@ -102,9 +110,7 @@ describe('BillingContainer', () => {
render(<BillingContainer />);
});
const currentBill = await screen.findByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findByText(/\$0/i);
@@ -116,17 +122,17 @@ describe('BillingContainer', () => {
expect(onTrail).toBeInTheDocument();
const receivedCardDetails = await screen.findByText(
/We have received your card details, your billing will only start after the end of your free trial period./i,
/card_details_recieved_and_billing_info/i,
);
expect(receivedCardDetails).toBeInTheDocument();
const manageBillingButton = await screen.findByRole('button', {
name: /manage billing/i,
name: /manage_billing/i,
});
expect(manageBillingButton).toBeInTheDocument();
const dayRemainingInBillingPeriod = await screen.findByText(
/1 days remaining in your billing period./i,
/1 days_remaining/i,
);
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
});
@@ -137,45 +143,30 @@ describe('BillingContainer', () => {
res(ctx.status(200), ctx.json(notOfTrailResponse)),
),
);
render(<BillingContainer />);
const { findByText } = render(<BillingContainer />);
const billingPeriodText = `Your current billing period is from ${getFormattedDate(
billingSuccessResponse.data.billingPeriodStart,
)} to ${getFormattedDate(billingSuccessResponse.data.billingPeriodEnd)}`;
const billingPeriod = await screen.findByRole('heading', {
name: new RegExp(billingPeriodText, 'i'),
});
const billingPeriod = await findByText(billingPeriodText);
expect(billingPeriod).toBeInTheDocument();
const currentBill = await screen.findByRole('heading', {
name: /current bill total/i,
});
const currentBill = screen.getByText('billing');
expect(currentBill).toBeInTheDocument();
const dollar0 = await screen.findAllByText(/\$1278.3/i);
expect(dollar0[0]).toBeInTheDocument();
expect(dollar0.length).toBe(2);
const dollar0 = await screen.findByText(/\$1,278.3/i);
expect(dollar0).toBeInTheDocument();
const metricsRow = await screen.findByRole('row', {
name: /metrics Million 4012 0.1 \$ 401.2/i,
name: /metrics 4012 Million 0.1 \$ 401.2/i,
});
expect(metricsRow).toBeInTheDocument();
const logRow = await screen.findByRole('row', {
name: /Logs GB 497 0.4 \$ 198.8/i,
name: /Logs 497 GB 0.4 \$ 198.8/i,
});
expect(logRow).toBeInTheDocument();
const totalBill = await screen.findByRole('cell', {
name: /\$1278/i,
});
expect(totalBill).toBeInTheDocument();
const totalBillRow = await screen.findByRole('row', {
name: /total \$1278/i,
});
expect(totalBillRow).toBeInTheDocument();
});
test('Should render corrent day remaining in billing period', async () => {
@@ -186,7 +177,7 @@ describe('BillingContainer', () => {
);
render(<BillingContainer />);
const dayRemainingInBillingPeriod = await screen.findByText(
/11 days remaining in your billing period./i,
/11 days_remaining/i,
);
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
});

View File

@@ -2,19 +2,33 @@
import './BillingContainer.styles.scss';
import { CheckCircleOutlined } from '@ant-design/icons';
import { Button, Col, Row, Skeleton, Table, Tag, Typography } from 'antd';
import { Color } from '@signozhq/design-tokens';
import {
Alert,
Button,
Card,
Col,
Flex,
Row,
Skeleton,
Table,
Tag,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/es/table';
import updateCreditCardApi from 'api/billing/checkout';
import getUsage from 'api/billing/getUsage';
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import manageCreditCardApi from 'api/billing/manage';
import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useAnalytics from 'hooks/analytics/useAnalytics';
import useAxiosError from 'hooks/useAxiosError';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { pick } from 'lodash-es';
import { isEmpty, pick } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -22,8 +36,11 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
interface DataType {
key: string;
name: string;
@@ -33,6 +50,11 @@ interface DataType {
cost: string;
}
enum SubscriptionStatus {
PastDue = 'past_due',
Active = 'active',
}
const renderSkeletonInput = (): JSX.Element => (
<Skeleton.Input
style={{ marginTop: '10px', height: '40px', width: '100%' }}
@@ -100,16 +122,19 @@ const dummyColumns: ColumnsType<DataType> = [
},
];
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function BillingContainer(): JSX.Element {
const daysRemainingStr = 'days remaining in your billing period.';
const { t } = useTranslation(['billings']);
const daysRemainingStr = t('days_remaining');
const [headerText, setHeaderText] = useState('');
const [billAmount, setBillAmount] = useState(0);
const [totalBillAmount, setTotalBillAmount] = useState(0);
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [daysRemaining, setDaysRemaining] = useState(0);
const [isFreeTrial, setIsFreeTrial] = useState(false);
const [data, setData] = useState<any[]>([]);
const billCurrency = '$';
const [apiResponse, setApiResponse] = useState<
Partial<UsageResponsePayloadProps>
>({});
const { trackEvent } = useAnalytics();
@@ -120,10 +145,15 @@ export default function BillingContainer(): JSX.Element {
const handleError = useAxiosError();
const isCloudUserVal = isCloudUser();
const processUsageData = useCallback(
(data: any): void => {
if (isEmpty(data?.payload)) {
return;
}
const {
details: { breakdown = [], total, billTotal },
details: { breakdown = [], billTotal },
billingPeriodStart,
billingPeriodEnd,
} = data?.payload || {};
@@ -141,8 +171,7 @@ export default function BillingContainer(): JSX.Element {
formattedUsageData.push({
key: `${index}${i}`,
name: i === 0 ? element?.type : '',
unit: element?.unit,
dataIngested: tier.quantity,
dataIngested: `${tier.quantity} ${element?.unit}`,
pricePerUnit: tier.unitPrice,
cost: `$ ${tier.tierCost}`,
});
@@ -152,7 +181,6 @@ export default function BillingContainer(): JSX.Element {
}
setData(formattedUsageData);
setTotalBillAmount(total);
if (!licensesData?.payload?.onTrial) {
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
@@ -165,11 +193,16 @@ export default function BillingContainer(): JSX.Element {
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
setBillAmount(billTotal);
}
setApiResponse(data?.payload || {});
},
[licensesData?.payload?.onTrial],
);
const { isLoading } = useQuery(
const isSubscriptionPastDue =
apiResponse.subscriptionStatus === SubscriptionStatus.PastDue;
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
{
queryFn: () => getUsage(activeLicense?.key || ''),
@@ -208,11 +241,6 @@ export default function BillingContainer(): JSX.Element {
key: 'name',
render: (text): JSX.Element => <div>{text}</div>,
},
{
title: 'Unit',
dataIndex: 'unit',
key: 'unit',
},
{
title: 'Data Ingested',
dataIndex: 'dataIngested',
@@ -230,24 +258,6 @@ export default function BillingContainer(): JSX.Element {
},
];
const renderSummary = (): JSX.Element => (
<Table.Summary.Row>
<Table.Summary.Cell index={0}>
<Typography.Title level={3} style={{ fontWeight: 500, margin: ' 0px' }}>
Total
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell index={1}> &nbsp; </Table.Summary.Cell>
<Table.Summary.Cell index={2}> &nbsp;</Table.Summary.Cell>
<Table.Summary.Cell index={3}> &nbsp; </Table.Summary.Cell>
<Table.Summary.Cell index={4}>
<Typography.Title level={3} style={{ fontWeight: 500, margin: ' 0px' }}>
${totalBillAmount}
</Typography.Title>
</Table.Summary.Cell>
</Table.Summary.Row>
);
const renderTableSkeleton = (): JSX.Element => (
<Table
dataSource={dummyData}
@@ -336,78 +346,121 @@ export default function BillingContainer(): JSX.Element {
updateCreditCard,
]);
const BillingUsageGraphCallback = useCallback(
() =>
!isLoading && !isFetchingBillingData ? (
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
) : (
<Card className="empty-graph-card" bordered={false}>
<Spinner size="large" tip="Loading..." height="35vh" />
</Card>
),
[apiResponse, billAmount, isLoading, isFetchingBillingData],
);
const { Text } = Typography;
const subscriptionPastDueMessage = (): JSX.Element => (
<Typography>
{`We were not able to process payments for your account. Please update your card details `}
<Text type="danger" onClick={handleBilling} style={{ cursor: 'pointer' }}>
{t('here')}
</Text>
{` if your payment information has changed. Email us at `}
<Text type="secondary">cloud-support@signoz.io</Text>
{` otherwise. Be sure to provide this information immediately to avoid interruption to your service.`}
</Typography>
);
return (
<div className="billing-container">
<Row
justify="space-between"
align="middle"
gutter={[16, 16]}
style={{
margin: 0,
}}
<Flex vertical style={{ marginBottom: 16 }}>
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
{t('billing')}
</Typography.Text>
<Typography.Text color={Color.BG_VANILLA_400}>
{t('manage_billing_and_costs')}
</Typography.Text>
</Flex>
<Card
bordered={false}
style={{ minHeight: 150, marginBottom: 16 }}
className="page-info"
>
<Col span={20}>
<Typography.Title level={4} ellipsis style={{ fontWeight: '300' }}>
{headerText}
</Typography.Title>
{licensesData?.payload?.onTrial &&
licensesData?.payload?.trialConvertedToSubscription && (
<Typography.Title
level={5}
ellipsis
style={{ fontWeight: '300', color: '#49aa19' }}
>
We have received your card details, your billing will only start after
the end of your free trial period.
</Typography.Title>
)}
</Col>
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Flex justify="space-between" align="center">
<Flex vertical>
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
{isCloudUserVal ? t('enterprise_cloud') : t('enterprise')}{' '}
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title>
{!isLoading && !isFetchingBillingData ? (
<Typography.Text style={{ fontSize: 12, color: Color.BG_VANILLA_400 }}>
{daysRemaining} {daysRemainingStr}
</Typography.Text>
) : null}
</Flex>
<Button
type="primary"
size="middle"
loading={isLoadingBilling || isLoadingManageBilling}
disabled={isLoading}
onClick={handleBilling}
>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
? 'Upgrade Plan'
: 'Manage Billing'}
? t('upgrade_plan')
: t('manage_billing')}
</Button>
</Col>
</Row>
</Flex>
<div className="billing-summary">
<Typography.Title level={4} style={{ margin: '16px 0' }}>
Current bill total
</Typography.Title>
{licensesData?.payload?.onTrial &&
licensesData?.payload?.trialConvertedToSubscription && (
<Typography.Text
ellipsis
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
>
{t('card_details_recieved_and_billing_info')}
</Typography.Text>
)}
<Typography.Title
level={3}
style={{ margin: '16px 0', display: 'flex', alignItems: 'center' }}
>
{billCurrency}
{billAmount} &nbsp;
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title>
{!isLoading && !isFetchingBillingData ? (
headerText && (
<Alert
message={headerText}
type="info"
showIcon
style={{ marginTop: 12 }}
/>
)
) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
)}
<Typography.Paragraph style={{ margin: '16px 0' }}>
{daysRemaining} {daysRemainingStr}
</Typography.Paragraph>
</div>
{isSubscriptionPastDue &&
(!isLoading && !isFetchingBillingData ? (
<Alert
message={subscriptionPastDueMessage()}
type="error"
showIcon
style={{ marginTop: 12 }}
/>
) : (
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
))}
</Card>
<BillingUsageGraphCallback />
<div className="billing-details">
{!isLoading && (
{!isLoading && !isFetchingBillingData && (
<Table
columns={columns}
dataSource={data}
pagination={false}
summary={renderSummary}
bordered={false}
/>
)}
{isLoading && renderTableSkeleton()}
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
</div>
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && (
@@ -423,16 +476,16 @@ export default function BillingContainer(): JSX.Element {
<Col span={20} className="plan-benefits">
<Typography.Text className="plan-benefit">
<CheckCircleOutlined />
Upgrade now to have uninterrupted access
{t('upgrade_now_text')}
</Typography.Text>
<Typography.Text className="plan-benefit">
<CheckCircleOutlined />
Your billing will start only after the trial period
{t('Your billing will start only after the trial period')}
</Typography.Text>
<Typography.Text className="plan-benefit">
<CheckCircleOutlined />
<span>
Check out features in paid plans &nbsp;
{t('checkout_plans')} &nbsp;
<a
href="https://signoz.io/pricing/"
style={{
@@ -441,7 +494,7 @@ export default function BillingContainer(): JSX.Element {
target="_blank"
rel="noreferrer"
>
here
{t('here')}
</a>
</span>
</Typography.Text>
@@ -453,7 +506,7 @@ export default function BillingContainer(): JSX.Element {
loading={isLoadingBilling || isLoadingManageBilling}
onClick={handleBilling}
>
Upgrade Plan
{t('upgrade_plan')}
</Button>
</Col>
</Row>

View File

@@ -0,0 +1,29 @@
.billing-graph-card {
.ant-card-body {
height: 40vh;
.uplot-graph-container {
padding: 8px;
}
}
.total-spent {
font-family: 'SF Mono' monospace;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 24px;
}
.total-spent-title {
font-size: 12px;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.48px;
color: rgba(255, 255, 255, 0.5);
}
}
.lightMode {
.total-spent-title {
color: var(--bg-ink-100);
}
}

View File

@@ -0,0 +1,201 @@
import './BillingUsageGraph.styles.scss';
import '../../../lib/uPlotLib/uPlotLib.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Card, Flex, Typography } from 'antd';
import Uplot from 'components/Uplot';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import tooltipPlugin from 'lib/uPlotLib/plugins/tooltipPlugin';
import getAxes from 'lib/uPlotLib/utils/getAxes';
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale';
import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale';
import { useMemo, useRef } from 'react';
import uPlot from 'uplot';
import {
convertDataToMetricRangePayload,
fillMissingValuesForQuantities,
} from './utils';
interface BillingUsageGraphProps {
data: any;
billAmount: number;
}
const paths = (
u: any,
seriesIdx: number,
idx0: number,
idx1: number,
extendGap: boolean,
buildClip: boolean,
): uPlot.Series.PathBuilder => {
const s = u.series[seriesIdx];
const style = s.drawStyle;
const interp = s.lineInterpolation;
const renderer = getRenderer(style, interp);
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
};
const calculateStartEndTime = (
data: any,
): { startTime: number; endTime: number } => {
const timestamps: number[] = [];
data?.details?.breakdown?.forEach((breakdown: any) => {
breakdown?.dayWiseBreakdown?.breakdown?.forEach((entry: any) => {
timestamps.push(entry?.timestamp);
});
});
const billingTime = [data?.billingPeriodStart, data?.billingPeriodEnd];
const startTime: number = Math.min(...timestamps, ...billingTime);
const endTime: number = Math.max(...timestamps, ...billingTime);
return { startTime, endTime };
};
export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
const { data, billAmount } = props;
const graphCompatibleData = useMemo(
() => convertDataToMetricRangePayload(data),
[data],
);
const chartData = getUPlotChartData(graphCompatibleData);
const graphRef = useRef<HTMLDivElement>(null);
const isDarkMode = useIsDarkMode();
const containerDimensions = useResizeObserver(graphRef);
const { startTime, endTime } = useMemo(() => calculateStartEndTime(data), [
data,
]);
const getGraphSeries = (color: string, label: string): any => ({
drawStyle: 'bars',
paths,
lineInterpolation: 'spline',
show: true,
label,
fill: color,
stroke: color,
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: color,
},
});
const uPlotSeries: any = useMemo(
() => [
{ label: 'Timestamp', stroke: 'purple' },
getGraphSeries(
'#7CEDBE',
graphCompatibleData.data.result[0]?.legend as string,
),
getGraphSeries(
'#4E74F8',
graphCompatibleData.data.result[1]?.legend as string,
),
getGraphSeries(
'#F24769',
graphCompatibleData.data.result[2]?.legend as string,
),
],
[graphCompatibleData.data.result],
);
const axesOptions = getAxes(isDarkMode, '');
const optionsForChart: uPlot.Options = useMemo(
() => ({
id: 'billing-usage-breakdown',
series: uPlotSeries,
width: containerDimensions.width,
height: containerDimensions.height - 30,
axes: [
{
...axesOptions[0],
grid: {
...axesOptions.grid,
show: false,
stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
},
},
{
...axesOptions[1],
stroke: isDarkMode ? Color.BG_SLATE_200 : Color.BG_INK_400,
},
],
scales: {
x: {
...getXAxisScale(startTime - 86400, endTime), // Minus 86400 from startTime to decrease a day to have a buffer start
},
y: {
...getYAxisScale({
series: graphCompatibleData?.data.newResult.data.result,
yAxisUnit: '',
softMax: null,
softMin: null,
}),
},
},
legend: {
show: true,
live: false,
isolate: true,
},
cursor: {
lock: false,
focus: {
prox: 1e6,
bias: 1,
},
},
focus: {
alpha: 0.3,
},
padding: [32, 32, 16, 16],
plugins: [
tooltipPlugin(
fillMissingValuesForQuantities(graphCompatibleData, chartData[0]),
'',
true,
),
],
}),
[
axesOptions,
chartData,
containerDimensions.height,
containerDimensions.width,
endTime,
graphCompatibleData,
isDarkMode,
startTime,
uPlotSeries,
],
);
const numberFormatter = new Intl.NumberFormat('en-US');
return (
<Card bordered={false} className="billing-graph-card">
<Flex justify="space-between">
<Flex vertical gap={6}>
<Typography.Text className="total-spent-title">
TOTAL SPENT
</Typography.Text>
<Typography.Text color={Color.BG_VANILLA_100} className="total-spent">
${numberFormatter.format(billAmount)}
</Typography.Text>
</Flex>
</Flex>
<div ref={graphRef} style={{ height: '100%', paddingBottom: 48 }}>
<Uplot data={chartData} options={optionsForChart} />
</div>
</Card>
);
}

View File

@@ -0,0 +1,87 @@
import { isEmpty, isNull } from 'lodash-es';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export const convertDataToMetricRangePayload = (
data: any,
): MetricRangePayloadProps => {
const emptyStateData = {
data: {
newResult: { data: { result: [], resultType: '' } },
result: [],
resultType: '',
},
};
if (isEmpty(data)) {
return emptyStateData;
}
const {
details: { breakdown = [] },
} = data || {};
if (isNull(breakdown) || breakdown.length === 0) {
return emptyStateData;
}
const payload = breakdown.map((info: any) => {
const metric = info.type;
const sortedBreakdownData = (info?.dayWiseBreakdown?.breakdown || []).sort(
(a: any, b: any) => a.timestamp - b.timestamp,
);
const values = (sortedBreakdownData || []).map((categoryInfo: any) => [
categoryInfo.timestamp,
categoryInfo.total,
]);
const queryName = info.type;
const legend = info.type;
const { unit } = info;
const quantity = sortedBreakdownData.map(
(categoryInfo: any) => categoryInfo.quantity,
);
return { metric, values, queryName, legend, quantity, unit };
});
const sortedData = payload.sort((a: any, b: any) => {
const sumA = a.values.reduce((acc: any, val: any) => acc + val[1], 0);
const avgA = a.values.length ? sumA / a.values.length : 0;
const sumB = b.values.reduce((acc: any, val: any) => acc + val[1], 0);
const avgB = b.values.length ? sumB / b.values.length : 0;
return sumA === sumB ? avgB - avgA : sumB - sumA;
});
return {
data: {
newResult: { data: { result: sortedData, resultType: '' } },
result: sortedData,
resultType: '',
},
};
};
export function fillMissingValuesForQuantities(
data: any,
timestampArray: number[],
): MetricRangePayloadProps {
const { result } = data.data;
const transformedResultArr: any[] = [];
result.forEach((item: any) => {
const timestampToQuantityMap: { [timestamp: number]: number } = {};
item.values.forEach((val: number[], index: number) => {
timestampToQuantityMap[val[0]] = item.quantity[index];
});
const quantityArray = timestampArray.map(
(timestamp: number) => timestampToQuantityMap[timestamp] ?? null,
);
transformedResultArr.push({ ...item, quantity: quantityArray });
});
return {
data: {
newResult: { data: { result: transformedResultArr, resultType: '' } },
result: transformedResultArr,
resultType: '',
},
};
}

View File

@@ -64,6 +64,16 @@ export interface OpsgenieChannel extends Channel {
priority?: string;
}
export interface EmailChannel extends Channel {
// comma separated list of email addresses to send alerts to
to: string;
// HTML body of the email notification.
html: string;
// Further headers email header key/value pairs.
// [ headers: { <string>: <tmpl_string>, ... } ]
headers: Record<string, string>;
}
export const ValidatePagerChannel = (p: PagerChannel): string => {
if (!p) {
return 'Received unexpected input for this channel, please contact your administrator ';

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