├── .changeset ├── README.md ├── config.json ├── perfect-humans-chew.md └── soft-coats-search.md ├── .env ├── .gitattributes ├── .github ├── images │ ├── architecture.png │ ├── dashboard.png │ ├── demo_local_animated.gif │ ├── logo_dark.png │ ├── logo_light.png │ ├── pattern3.png │ ├── search_splash.png │ ├── session.png │ └── trace.png └── workflows │ ├── main.yml │ ├── push.yml │ ├── pushv1.yml │ ├── release-nightly.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .kodiak.toml ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── .yarn └── releases │ ├── yarn-1.22.18.cjs │ └── yarn-4.5.1.cjs ├── .yarnrc.yml ├── CONTRIBUTING.md ├── DEPLOY.md ├── LICENSE ├── LOCAL.md ├── Makefile ├── README.md ├── docker-compose.ci.yml ├── docker-compose.dev.yml ├── docker-compose.yml ├── docker ├── clickhouse │ └── local │ │ ├── config.xml │ │ └── users.xml ├── hostmetrics │ ├── Dockerfile │ └── config.dev.yaml ├── hyperdx │ ├── Dockerfile │ ├── build.sh │ ├── clickhouseConfig.xml │ ├── entry.local.auth.sh │ ├── entry.local.base.sh │ ├── entry.local.noauth.sh │ └── entry.prod.sh ├── nginx │ ├── README.md │ └── nginx.conf └── otel-collector │ ├── Dockerfile │ ├── config.yaml │ └── supervisor_docker.yaml ├── nx.json ├── package.json ├── packages ├── api │ ├── .Dockerignore │ ├── .env.development │ ├── .env.test │ ├── .eslintignore │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── Dockerfile │ ├── docs │ │ └── auto_provision │ │ │ └── AUTO_PROVISION.md │ ├── jest.config.js │ ├── jest.setup.ts │ ├── migrate-mongo-config.ts │ ├── migrations │ │ ├── ch │ │ │ ├── 000001_add_is_delta_n_is_monotonic_fields_to_metric_stream_table.down.sql │ │ │ └── 000001_add_is_delta_n_is_monotonic_fields_to_metric_stream_table.up.sql │ │ └── mongo │ │ │ └── 20231130053610-add_accessKey_field_to_user_collection.ts │ ├── package.json │ ├── scripts │ │ └── generate-api-docs.ts │ ├── src │ │ ├── api-app.ts │ │ ├── clickhouse │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── renderChartConfig.test.ts.snap │ │ │ │ ├── clickhouse.test.ts │ │ │ │ └── renderChartConfig.test.ts │ │ │ └── index.ts │ │ ├── config.ts │ │ ├── controllers │ │ │ ├── __tests__ │ │ │ │ └── team.test.ts │ │ │ ├── alerts.ts │ │ │ ├── connection.ts │ │ │ ├── dashboard.ts │ │ │ ├── savedSearch.ts │ │ │ ├── sources.ts │ │ │ ├── team.ts │ │ │ └── user.ts │ │ ├── fixtures.ts │ │ ├── index.ts │ │ ├── middleware │ │ │ ├── auth.ts │ │ │ ├── cors.ts │ │ │ ├── error.ts │ │ │ └── validation.ts │ │ ├── models │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── alert.ts │ │ │ ├── alertHistory.ts │ │ │ ├── connection.ts │ │ │ ├── dashboard.ts │ │ │ ├── index.ts │ │ │ ├── savedSearch.ts │ │ │ ├── source.ts │ │ │ ├── team.ts │ │ │ ├── teamInvite.ts │ │ │ ├── user.ts │ │ │ └── webhook.ts │ │ ├── opamp │ │ │ ├── README.md │ │ │ ├── app.ts │ │ │ ├── controllers │ │ │ │ └── opampController.ts │ │ │ ├── models │ │ │ │ └── agent.ts │ │ │ ├── proto │ │ │ │ ├── anyvalue.proto │ │ │ │ └── opamp.proto │ │ │ ├── services │ │ │ │ └── agentService.ts │ │ │ └── utils │ │ │ │ └── protobuf.ts │ │ ├── routers │ │ │ ├── api │ │ │ │ ├── __tests__ │ │ │ │ │ ├── alerts.test.ts │ │ │ │ │ ├── dashboard.test.ts │ │ │ │ │ ├── savedSearch.test.ts │ │ │ │ │ ├── sources.test.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── webhooks.test.ts │ │ │ │ ├── alerts.ts │ │ │ │ ├── clickhouseProxy.ts │ │ │ │ ├── connections.ts │ │ │ │ ├── dashboards.ts │ │ │ │ ├── datasources.ts │ │ │ │ ├── index.ts │ │ │ │ ├── me.ts │ │ │ │ ├── root.ts │ │ │ │ ├── savedSearch.ts │ │ │ │ ├── sources.ts │ │ │ │ ├── team.ts │ │ │ │ └── webhooks.ts │ │ │ └── external-api │ │ │ │ ├── __tests__ │ │ │ │ ├── alerts.test.ts │ │ │ │ ├── dashboards.test.ts │ │ │ │ └── v2.test.ts │ │ │ │ └── v2 │ │ │ │ ├── alerts.ts │ │ │ │ ├── dashboards.ts │ │ │ │ └── index.ts │ │ ├── server.ts │ │ ├── setupDefaults.ts │ │ ├── tasks │ │ │ ├── __tests__ │ │ │ │ └── checkAlerts.test.ts │ │ │ ├── checkAlerts.ts │ │ │ ├── index.ts │ │ │ └── usageStats.ts │ │ └── utils │ │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── logParser.test.ts.snap │ │ │ ├── common.test.ts │ │ │ ├── errors.test.ts │ │ │ ├── logParser.test.ts │ │ │ └── validators.test.ts │ │ │ ├── common.ts │ │ │ ├── email.ts │ │ │ ├── errors.ts │ │ │ ├── externalApi.ts │ │ │ ├── logParser.ts │ │ │ ├── logger.ts │ │ │ ├── miner.ts │ │ │ ├── passport.ts │ │ │ ├── queue.ts │ │ │ ├── rateLimiter.ts │ │ │ ├── slack.ts │ │ │ ├── swagger.ts │ │ │ ├── validators.ts │ │ │ └── zod.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── app │ ├── .Dockerignore │ ├── .env.development │ ├── .eslintrc.js │ ├── .gitignore │ ├── .storybook │ │ ├── main.ts │ │ ├── preview-head.html │ │ ├── preview.tsx │ │ └── public │ │ │ └── mockServiceWorker.js │ ├── .stylelintignore │ ├── .stylelintrc │ ├── CHANGELOG.md │ ├── Dockerfile │ ├── global-setup.js │ ├── jest.config.js │ ├── knip.json │ ├── mdx.d.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── _error.tsx │ │ ├── alerts.tsx │ │ ├── api │ │ │ ├── [...all].ts │ │ │ └── config.ts │ │ ├── benchmark.tsx │ │ ├── chart.tsx │ │ ├── clickhouse.tsx │ │ ├── dashboards │ │ │ ├── [dashboardId].tsx │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── join-team.tsx │ │ ├── kubernetes.tsx │ │ ├── login │ │ │ └── index.tsx │ │ ├── register.tsx │ │ ├── search │ │ │ ├── [savedSearchId].tsx │ │ │ └── index.tsx │ │ ├── services.tsx │ │ ├── sessions.tsx │ │ └── team │ │ │ └── index.tsx │ ├── postcss.config.cjs │ ├── public │ │ ├── Icon32.png │ │ └── drain3-0.9.11-py3-none-any.whl │ ├── src │ │ ├── AlertsPage.tsx │ │ ├── AppNav.components.tsx │ │ ├── AppNav.tsx │ │ ├── AuthLoadingBlocker.tsx │ │ ├── AuthPage.tsx │ │ ├── AutocompleteInput.tsx │ │ ├── BenchmarkPage.tsx │ │ ├── ChartUtils.tsx │ │ ├── Checkbox.tsx │ │ ├── ClickhousePage.tsx │ │ ├── Clipboard.tsx │ │ ├── DBChartPage.tsx │ │ ├── DBDashboardPage.tsx │ │ ├── DBSearchPage.tsx │ │ ├── DBSearchPageAlertModal.tsx │ │ ├── DOMPlayer.tsx │ │ ├── DSSelect.tsx │ │ ├── GranularityPicker.tsx │ │ ├── HDXMarkdownChart.tsx │ │ ├── HDXMultiSeriesTableChart.stories.tsx │ │ ├── HDXMultiSeriesTableChart.tsx │ │ ├── HDXMultiSeriesTimeChart.tsx │ │ ├── Icon.tsx │ │ ├── InstallInstructionsModal.tsx │ │ ├── JoinTeamPage.tsx │ │ ├── KubernetesDashboardPage.tsx │ │ ├── LandingHeader.tsx │ │ ├── LandingPage.tsx │ │ ├── LogSidePanelElements.stories.tsx │ │ ├── LogSidePanelElements.tsx │ │ ├── Logo.tsx │ │ ├── NamespaceDetailsSidePanel.tsx │ │ ├── NavHoverDropdown.tsx │ │ ├── NodeDetailsSidePanel.tsx │ │ ├── OnboardingChecklist.tsx │ │ ├── PasswordCheck.tsx │ │ ├── Playbar.tsx │ │ ├── PlaybarSlider.tsx │ │ ├── PodDetailsSidePanel.tsx │ │ ├── SVGIcons.tsx │ │ ├── SearchInputV2.tsx │ │ ├── ServicesDashboardPage.tsx │ │ ├── SessionEventList.tsx │ │ ├── SessionSidePanel.tsx │ │ ├── SessionSubpanel.tsx │ │ ├── SessionsPage.tsx │ │ ├── Spotlights.tsx │ │ ├── TabBar.tsx │ │ ├── TabBarWithContent.tsx │ │ ├── TabItem.tsx │ │ ├── TeamPage.tsx │ │ ├── ThemeWrapper.tsx │ │ ├── TimelineChart.tsx │ │ ├── UserPreferencesModal.tsx │ │ ├── __tests__ │ │ │ ├── Spotlights.test.tsx │ │ │ ├── searchFilters.test.ts │ │ │ ├── timeQuery.test.tsx │ │ │ └── utils.test.ts │ │ ├── api.ts │ │ ├── clickhouse.ts │ │ ├── components │ │ │ ├── AggFnSelect.tsx │ │ │ ├── AlertPreviewChart.tsx │ │ │ ├── Alerts.tsx │ │ │ ├── ChartBox.tsx │ │ │ ├── ChartSQLPreview.tsx │ │ │ ├── ColorSwatchInput.stories.tsx │ │ │ ├── ColorSwatchInput.tsx │ │ │ ├── ConfirmDeleteMenu.tsx │ │ │ ├── ConnectionForm.tsx │ │ │ ├── ConnectionSelect.tsx │ │ │ ├── ContactSupportText.tsx │ │ │ ├── ContextSidePanel.tsx │ │ │ ├── DBColumnMultiSelect.tsx │ │ │ ├── DBDeltaChart.tsx │ │ │ ├── DBEditTimeChartForm.tsx │ │ │ ├── DBHeatmapChart.tsx │ │ │ ├── DBHistogramChart.tsx │ │ │ ├── DBInfraPanel.tsx │ │ │ ├── DBListBarChart.tsx │ │ │ ├── DBNumberChart.tsx │ │ │ ├── DBRowDataPanel.tsx │ │ │ ├── DBRowJsonViewer.test.tsx │ │ │ ├── DBRowJsonViewer.tsx │ │ │ ├── DBRowOverviewPanel.tsx │ │ │ ├── DBRowSidePanel.tsx │ │ │ ├── DBRowSidePanelHeader.tsx │ │ │ ├── DBRowTable.tsx │ │ │ ├── DBSearchPageFilters.tsx │ │ │ ├── DBSessionPanel.tsx │ │ │ ├── DBTableChart.tsx │ │ │ ├── DBTableSelect.tsx │ │ │ ├── DBTimeChart.tsx │ │ │ ├── DBTracePanel.tsx │ │ │ ├── DBTraceWaterfallChart.tsx │ │ │ ├── DatabaseSelect.tsx │ │ │ ├── DrawerUtils.tsx │ │ │ ├── ErrorBoundary.stories.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── EventTag.tsx │ │ │ ├── ExceptionSubpanel.tsx │ │ │ ├── HyperJson.module.scss │ │ │ ├── HyperJson.stories.tsx │ │ │ ├── HyperJson.tsx │ │ │ ├── Icon.tsx │ │ │ ├── InputControlled.tsx │ │ │ ├── InputLanguageSwitch.tsx │ │ │ ├── KubeComponents.tsx │ │ │ ├── KubernetesFilters.tsx │ │ │ ├── LogLevel.tsx │ │ │ ├── MetricNameSelect.tsx │ │ │ ├── NetworkPropertyPanel.tsx │ │ │ ├── NumberFormat.tsx │ │ │ ├── OnboardingModal.tsx │ │ │ ├── PageHeader.module.scss │ │ │ ├── PageHeader.tsx │ │ │ ├── PatternSidePanel.tsx │ │ │ ├── PatternTable.tsx │ │ │ ├── SQLEditor.tsx │ │ │ ├── SQLInlineEditor.tsx │ │ │ ├── SearchPageActionBar.tsx │ │ │ ├── SearchTotalCountChart.tsx │ │ │ ├── SelectControlled.tsx │ │ │ ├── ServiceDashboardDbQuerySidePanel.tsx │ │ │ ├── ServiceDashboardEndpointPerformanceChart.tsx │ │ │ ├── ServiceDashboardEndpointSidePanel.tsx │ │ │ ├── ServiceDashboardSlowestEventsTile.tsx │ │ │ ├── SourceForm.tsx │ │ │ ├── SourceSelect.tsx │ │ │ ├── SpanEventsSubpanel.tsx │ │ │ ├── StacktraceFrame.tsx │ │ │ ├── Table.module.scss │ │ │ ├── Table.tsx │ │ │ ├── Tags.module.scss │ │ │ ├── Tags.tsx │ │ │ ├── TimePicker │ │ │ │ ├── TimePicker.stories.tsx │ │ │ │ ├── TimePicker.tsx │ │ │ │ ├── __tests__ │ │ │ │ │ └── utils.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── useTimePickerForm.ts │ │ │ │ └── utils.ts │ │ │ ├── WhereLanguageControlled.tsx │ │ │ └── __tests__ │ │ │ │ ├── ConnectionForm.test.tsx │ │ │ │ ├── DBNumberChart.test.tsx │ │ │ │ ├── DBSearchPageFilters.test.tsx │ │ │ │ └── DBTraceWaterfallChart.test.tsx │ │ ├── config.ts │ │ ├── connection.ts │ │ ├── dashboard.ts │ │ ├── fixtures.ts │ │ ├── hdxMTViews.ts │ │ ├── hooks │ │ │ ├── __tests__ │ │ │ │ ├── useAutoCompleteOptions.test.tsx │ │ │ │ ├── useMetadata.test.tsx │ │ │ │ ├── useResizable.test.tsx │ │ │ │ └── useSqlSuggestions.test.tsx │ │ │ ├── useAutoCompleteOptions.tsx │ │ │ ├── useChartConfig.tsx │ │ │ ├── useDashboardRefresh.tsx │ │ │ ├── useExplainQuery.tsx │ │ │ ├── useFetchMetricResourceAttrs.tsx │ │ │ ├── useMetadata.tsx │ │ │ ├── useOffsetPaginatedQuery.tsx │ │ │ ├── usePatterns.tsx │ │ │ ├── useResizable.tsx │ │ │ ├── useRowWhere.tsx │ │ │ └── useSqlSuggestions.tsx │ │ ├── instrumentation.ts │ │ ├── layout.tsx │ │ ├── metadata.ts │ │ ├── mocks │ │ │ └── handlers.ts │ │ ├── nextra.config.tsx │ │ ├── savedSearch.ts │ │ ├── searchFilters.tsx │ │ ├── serviceDashboard.ts │ │ ├── sessions.ts │ │ ├── setupTests.tsx │ │ ├── source.ts │ │ ├── stories │ │ │ ├── AppNav.stories.tsx │ │ │ └── Button.stories.tsx │ │ ├── tableUtils.tsx │ │ ├── timeQuery.ts │ │ ├── types.ts │ │ ├── useConfirm.tsx │ │ ├── useFormatTime.tsx │ │ ├── useNextraSeoProps.ts │ │ ├── useQueryParam.tsx │ │ ├── useSourceMappedFrame.tsx │ │ ├── useUserPreferences.tsx │ │ ├── utils.ts │ │ ├── utils │ │ │ ├── alerts.ts │ │ │ └── curlGenerator.ts │ │ ├── vsc-dark-plus.ts │ │ └── zIndex.ts │ ├── styles │ │ ├── AlertsPage.module.scss │ │ ├── AppNav.module.scss │ │ ├── EndpointSubpanel.module.scss │ │ ├── HDXLineChart.module.scss │ │ ├── Home.module.css │ │ ├── LogSidePanel.module.scss │ │ ├── LogTable.module.scss │ │ ├── PlaybarSlider.module.scss │ │ ├── ResizablePanel.module.scss │ │ ├── SearchPage.module.scss │ │ ├── SessionSubpanelV2.module.scss │ │ ├── TeamPage.module.scss │ │ ├── TimelineChart.module.scss │ │ ├── app.scss │ │ ├── globals.css │ │ └── variables.scss │ ├── tsconfig.json │ └── tsconfig.test.json ├── common-utils │ ├── .eslintignore │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── renderChartConfig.test.ts.snap │ │ │ ├── clickhouse.test.ts │ │ │ ├── metadata.test.ts │ │ │ ├── queryParser.test.ts │ │ │ ├── renderChartConfig.test.ts │ │ │ ├── sqlFormatter.test.ts │ │ │ └── utils.test.ts │ │ ├── clickhouse.ts │ │ ├── metadata.ts │ │ ├── queryParser.ts │ │ ├── renderChartConfig.ts │ │ ├── sqlFormatter.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── tsup.config.ts └── go-parser │ ├── Dockerfile │ ├── LICENSE │ ├── go.mod │ ├── go.sum │ └── main.go ├── smoke-tests └── otel-collector │ ├── README.md │ ├── auto-parse-json.bats │ ├── data │ ├── auto-parse │ │ ├── default │ │ │ ├── assert_query.sql │ │ │ ├── expected.snap │ │ │ └── input.json │ │ ├── json-string │ │ │ ├── assert_query.sql │ │ │ ├── expected.snap │ │ │ └── input.json │ │ └── otel-map │ │ │ ├── assert_query.sql │ │ │ ├── expected.snap │ │ │ └── input.json │ ├── normalize-severity │ │ └── text-case │ │ │ ├── assert_query.sql │ │ │ ├── expected.snap │ │ │ └── input.json │ └── severity-inference │ │ ├── infer-debug │ │ ├── assert_query.sql │ │ ├── expected.snap │ │ └── input.json │ │ ├── infer-error │ │ ├── assert_query.sql │ │ ├── expected.snap │ │ └── input.json │ │ ├── infer-fatal │ │ ├── assert_query.sql │ │ ├── expected.snap │ │ └── input.json │ │ ├── infer-info │ │ ├── assert_query.sql │ │ ├── expected.snap │ │ └── input.json │ │ ├── infer-trace │ │ ├── assert_query.sql │ │ ├── expected.snap │ │ └── input.json │ │ ├── infer-warn │ │ ├── assert_query.sql │ │ ├── expected.snap │ │ └── input.json │ │ └── skip-infer │ │ ├── assert_query.sql │ │ ├── expected.snap │ │ └── input.json │ ├── docker-compose.yaml │ ├── normalize-severity.bats │ ├── receiver-config.yaml │ ├── setup_suite.bash │ ├── severity-inference.bats │ └── test_helpers │ ├── assertions.bash │ └── utilities.bash ├── version.sh └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by 4 | `@changesets/cli`, a build tool that works with multi-package repos, or 5 | single-package repos to help you version and publish your code. You can find the 6 | full documentation for it 7 | [in our repository](https://github.com/changesets/changesets) 8 | 9 | We have a quick list of common questions to get you started engaging with this 10 | project in 11 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 12 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [["@hyperdx/api", "@hyperdx/app"]], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "v2", 9 | "updateInternalDependencies": "patch" 10 | } 11 | -------------------------------------------------------------------------------- /.changeset/perfect-humans-chew.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@hyperdx/app": patch 3 | --- 4 | 5 | Better loading state for events patterns table 6 | -------------------------------------------------------------------------------- /.changeset/soft-coats-search.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@hyperdx/api": minor 3 | --- 4 | 5 | Bumped mongodb driver support to allow for AWS IAM authentication. This drops support for MongoDB 3.6. 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Used by docker-compose.yml 2 | HDX_IMAGE_REPO=docker.hyperdx.io 3 | IMAGE_NAME=ghcr.io/hyperdxio/hyperdx 4 | IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx 5 | LOCAL_IMAGE_NAME=ghcr.io/hyperdxio/hyperdx-local 6 | LOCAL_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-local 7 | ALL_IN_ONE_IMAGE_NAME=ghcr.io/hyperdxio/hyperdx-all-in-one 8 | ALL_IN_ONE_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-all-in-one 9 | OTEL_COLLECTOR_IMAGE_NAME=ghcr.io/hyperdxio/hyperdx-otel-collector 10 | OTEL_COLLECTOR_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-otel-collector 11 | CODE_VERSION=2.0.0 12 | IMAGE_VERSION_SUB_TAG=.0.0 13 | IMAGE_VERSION=2 14 | IMAGE_NIGHTLY_TAG=2-nightly 15 | IMAGE_LATEST_TAG=latest 16 | 17 | # Set up domain URLs 18 | HYPERDX_API_PORT=8000 #optional (should not be taken by other services) 19 | HYPERDX_APP_PORT=8080 20 | HYPERDX_APP_URL=http://localhost 21 | HYPERDX_LOG_LEVEL=debug 22 | HYPERDX_OPAMP_PORT=4320 23 | 24 | # Otel/Clickhouse config 25 | HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE=default 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/releases/** binary 2 | -------------------------------------------------------------------------------- /.github/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/.github/images/architecture.png -------------------------------------------------------------------------------- /.github/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/.github/images/dashboard.png -------------------------------------------------------------------------------- /.github/images/demo_local_animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/.github/images/demo_local_animated.gif -------------------------------------------------------------------------------- /.github/images/logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/.github/images/logo_dark.png -------------------------------------------------------------------------------- /.github/images/logo_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/.github/images/logo_light.png -------------------------------------------------------------------------------- /.github/images/pattern3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/.github/images/pattern3.png -------------------------------------------------------------------------------- /.github/images/search_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/.github/images/search_splash.png -------------------------------------------------------------------------------- /.github/images/session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/.github/images/session.png -------------------------------------------------------------------------------- /.github/images/trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/.github/images/trace.png -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push Downstream 2 | on: 3 | push: 4 | branches: [main] 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | jobs: 9 | push-downstream: 10 | timeout-minutes: 5 11 | runs-on: ubuntu-24.04 12 | steps: 13 | - uses: actions/github-script@v7 14 | env: 15 | ACTOR: ${{ github.actor }} 16 | AUTHOR: ${{ github.event.head_commit.author.name }} 17 | MESSAGE: ${{ github.event.head_commit.message }} 18 | SHA: ${{ github.sha }} 19 | with: 20 | github-token: ${{ secrets.DOWNSTREAM_TOKEN }} 21 | script: | 22 | const { ACTOR, AUTHOR, MESSAGE, SHA } = process.env; 23 | const result = await github.rest.actions.createWorkflowDispatch({ 24 | owner: '${{ secrets.DOWNSTREAM_OWNER }}', 25 | repo: '${{ secrets.DOWNSTREAM_REPO_V2 }}', 26 | workflow_id: '${{ secrets.DOWNSTREAM_WORKFLOW_ID_V2 }}', 27 | ref: 'main', 28 | inputs: { 29 | actor: ACTOR, 30 | author: AUTHOR, 31 | message: MESSAGE, 32 | sha: SHA 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /.github/workflows/pushv1.yml: -------------------------------------------------------------------------------- 1 | name: Push Downstream V1 2 | on: 3 | push: 4 | branches: [v1] 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | jobs: 9 | push-downstream: 10 | timeout-minutes: 5 11 | runs-on: ubuntu-24.04 12 | steps: 13 | - uses: actions/github-script@v7 14 | env: 15 | ACTOR: ${{ github.actor }} 16 | MESSAGE: ${{ github.event.head_commit.message }} 17 | SHA: ${{ github.sha }} 18 | with: 19 | github-token: ${{ secrets.DOWNSTREAM_TOKEN }} 20 | script: | 21 | const { ACTOR, MESSAGE, SHA } = process.env; 22 | const result = await github.rest.actions.createWorkflowDispatch({ 23 | owner: '${{ secrets.DOWNSTREAM_OWNER }}', 24 | repo: '${{ secrets.DOWNSTREAM_REPO }}', 25 | workflow_id: '${{ secrets.DOWNSTREAM_WORKFLOW_ID }}', 26 | ref: 'main', 27 | inputs: { 28 | sha: SHA, 29 | actor: ACTOR, 30 | message: MESSAGE 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # misc 2 | **/.DS_Store 3 | **/*.pem 4 | **/keys 5 | 6 | # logs 7 | **/*.log 8 | **/npm-debug.log* 9 | **/lerna-debug.log* 10 | 11 | # yarn 12 | **/yarn-debug.log* 13 | **/yarn-error.log* 14 | .yarn/* 15 | !.yarn/cache 16 | !.yarn/patches 17 | !.yarn/plugins 18 | !.yarn/releases 19 | !.yarn/sdks 20 | !.yarn/versions 21 | 22 | # dotenv environment variable files 23 | **/.env.development.local 24 | **/.env.test.local 25 | **/.env.production.local 26 | **/.env.local 27 | **/.dockerhub.env 28 | **/.ghcr.env 29 | 30 | # Next.js build output 31 | packages/app/.next 32 | packages/app/.pnp 33 | packages/app/.pnp.js 34 | packages/app/.vercel 35 | packages/app/coverage 36 | packages/app/out 37 | 38 | # OpenAPI spec 39 | packages/public/openapi.json 40 | 41 | # optional npm cache directory 42 | **/.npm 43 | 44 | # dependency directories 45 | **/node_modules 46 | 47 | # build output 48 | **/dist 49 | **/build 50 | **/tsconfig.tsbuildinfo 51 | 52 | # jest coverage report 53 | **/coverage 54 | 55 | # e2e 56 | e2e/cypress/screenshots/ 57 | e2e/cypress/videos/ 58 | e2e/cypress/results 59 | 60 | # scripts 61 | scripts/*.csv 62 | **/venv 63 | **/__pycache__/ 64 | *.py[cod] 65 | *$py.class 66 | 67 | # docker 68 | docker-compose.prod.yml 69 | .volumes 70 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.16.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | dist 3 | coverage 4 | tests 5 | .volumes 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "fluid": false, 11 | "arrowParens": "avoid", 12 | "proseWrap": "always" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vunguyentuan.vscode-css-variables", 4 | "clinyong.vscode-css-modules", 5 | "streetsidesoftware.code-spell-checker", 6 | "dbaeumer.vscode-eslint", 7 | "hossaini.bootstrap-intellisense", 8 | "stylelint.vscode-stylelint" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "always", 5 | "source.fixAll.stylelint": "always" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 9 | }, 10 | "[typescriptreact]": { 11 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 12 | }, 13 | "[scss]": { 14 | "editor.defaultFormatter": "stylelint.vscode-stylelint" 15 | }, 16 | "[css]": { 17 | "editor.defaultFormatter": "stylelint.vscode-stylelint" 18 | }, 19 | "stylelint.validate": ["css", "postcss", "scss"], 20 | "cssVariables.lookupFiles": [ 21 | "**/*.css", 22 | "**/*.scss", 23 | "**/*.sass", 24 | "**/*.less", 25 | "node_modules/@mantine/core/styles.css" 26 | ], 27 | "search.exclude": { 28 | "**/node_modules": true, 29 | "**/bower_components": true, 30 | "**/*.code-search": true, 31 | "**/*.map": true, 32 | "**/yarn.lock": true, 33 | "**/yarn-*.cjs": true 34 | }, 35 | "cSpell.words": ["micropip", "opamp", "pyimport", "pyodide"] 36 | } 37 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.5.1.cjs 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 DeploySentinel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-compose.ci.yml: -------------------------------------------------------------------------------- 1 | name: hdx-ci 2 | services: 3 | otel-collector: 4 | build: 5 | context: ./docker/otel-collector 6 | target: dev 7 | environment: 8 | CLICKHOUSE_ENDPOINT: 'tcp://ch-server:9000?dial_timeout=10s' 9 | HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE: ${HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE} 10 | HYPERDX_API_KEY: ${HYPERDX_API_KEY} 11 | HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL} 12 | volumes: 13 | - ./docker/otel-collector/config.yaml:/etc/otelcol-contrib/config.yaml 14 | ports: 15 | - '23133:13133' # health_check extension 16 | # - '24225:24225' # fluentd receiver 17 | # - '4317:4317' # OTLP gRPC receiver 18 | # - '4318:4318' # OTLP http receiver 19 | # - '8888:8888' # metrics extension 20 | networks: 21 | - internal 22 | depends_on: 23 | - ch-server 24 | ch-server: 25 | image: clickhouse/clickhouse-server:24-alpine 26 | environment: 27 | # default settings 28 | CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1 29 | volumes: 30 | - ./docker/clickhouse/local/config.xml:/etc/clickhouse-server/config.xml 31 | - ./docker/clickhouse/local/users.xml:/etc/clickhouse-server/users.xml 32 | restart: on-failure 33 | ports: 34 | - 8123:8123 # http api 35 | # - 9000:9000 # native 36 | networks: 37 | - internal 38 | db: 39 | image: mongo:5.0.14-focal 40 | command: --port 29999 41 | ports: 42 | - 29999:29999 43 | networks: 44 | - internal 45 | networks: 46 | internal: 47 | name: 'hyperdx-ci-internal-network' 48 | -------------------------------------------------------------------------------- /docker/clickhouse/local/users.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10000000000 6 | 0 7 | in_order 8 | 1 9 | 10 | 11 | 12 | 13 | 14 | 15 | default 16 | 17 | ::/0 18 | 19 | default 20 | 21 | 22 | api 23 | default 24 | 25 | ::/0 26 | 27 | default 28 | 29 | 30 | worker 31 | default 32 | 33 | ::/0 34 | 35 | default 36 | 37 | 38 | 39 | 40 | 41 | 42 | 3600 43 | 0 44 | 0 45 | 0 46 | 0 47 | 0 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docker/hostmetrics/Dockerfile: -------------------------------------------------------------------------------- 1 | ## base ############################################################################################# 2 | FROM otel/opentelemetry-collector-contrib:0.90.0 AS base 3 | 4 | 5 | ## dev ############################################################################################# 6 | FROM base as dev 7 | 8 | COPY ./config.dev.yaml /etc/otelcol-contrib/config.yaml 9 | 10 | 11 | ## prod ############################################################################################# 12 | FROM base as prod 13 | 14 | COPY ./config.dev.yaml /etc/otelcol-contrib/config.yaml 15 | -------------------------------------------------------------------------------- /docker/hostmetrics/config.dev.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | mongodb: 3 | hosts: 4 | - endpoint: db:27017 5 | collection_interval: 5s 6 | initial_delay: 1s 7 | tls: 8 | insecure: true 9 | insecure_skip_verify: true 10 | hostmetrics: 11 | collection_interval: 5s 12 | scrapers: 13 | cpu: 14 | load: 15 | memory: 16 | disk: 17 | filesystem: 18 | network: 19 | exporters: 20 | logging: 21 | loglevel: ${env:HYPERDX_LOG_LEVEL} 22 | otlphttp: 23 | endpoint: 'http://otel-collector:4318' 24 | headers: 25 | authorization: ${HYPERDX_API_KEY} 26 | compression: gzip 27 | service: 28 | telemetry: 29 | logs: 30 | level: ${env:HYPERDX_LOG_LEVEL} 31 | pipelines: 32 | metrics: 33 | receivers: [mongodb, hostmetrics] 34 | exporters: [otlphttp, logging] 35 | -------------------------------------------------------------------------------- /docker/hyperdx/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Meant to be run from the root of the repo 3 | 4 | # No Auth 5 | docker build --squash . -f ./docker/hyperdx/Dockerfile \ 6 | --build-context clickhouse=./docker/clickhouse \ 7 | --build-context otel-collector=./docker/otel-collector \ 8 | --build-context hyperdx=./docker/hyperdx \ 9 | --build-context api=./packages/api \ 10 | --build-context app=./packages/app \ 11 | --target all-in-one-noauth -t hyperdx/dev-all-in-one-noauth 12 | -------------------------------------------------------------------------------- /docker/hyperdx/entry.local.auth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set auth mode 4 | export IS_LOCAL_APP_MODE="REQUIRED_AUTH" 5 | 6 | # Source the common entry script 7 | source "/etc/local/entry.base.sh" 8 | -------------------------------------------------------------------------------- /docker/hyperdx/entry.local.noauth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set no auth mode 4 | export IS_LOCAL_APP_MODE="DANGEROUSLY_is_local_app_mode💀" 5 | 6 | # Source the common entry script 7 | source "/etc/local/entry.base.sh" 8 | -------------------------------------------------------------------------------- /docker/hyperdx/entry.prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export FRONTEND_URL="${FRONTEND_URL:-${HYPERDX_APP_URL:-http://localhost}:${HYPERDX_APP_PORT:-8080}}" 4 | export OPAMP_PORT=${HYPERDX_OPAMP_PORT:-4320} 5 | 6 | # Set to "REQUIRED_AUTH" to enforce API authentication. 7 | # ⚠️ Do not change this value !!!! 8 | export IS_LOCAL_APP_MODE="REQUIRED_AUTH" 9 | 10 | echo "" 11 | echo "Visit the HyperDX UI at $FRONTEND_URL" 12 | echo "" 13 | 14 | # Use concurrently to run both the API and App servers 15 | npx concurrently \ 16 | "--kill-others" \ 17 | "--names=API,APP,ALERT-TASK" \ 18 | "PORT=${HYPERDX_API_PORT:-8000} HYPERDX_APP_PORT=${HYPERDX_APP_PORT:-8080} node -r /app/api/node_modules/@hyperdx/node-opentelemetry/build/src/tracing /app/api/packages/api/build/index.js" \ 19 | "HYPERDX_API_PORT=${HYPERDX_API_PORT:-8000} /app/app/node_modules/.bin/next start -p ${HYPERDX_APP_PORT:-8080}" \ 20 | "node -r /app/api/node_modules/@hyperdx/node-opentelemetry/build/src/tracing /app/api/packages/api/build/tasks/index.js check-alerts" 21 | -------------------------------------------------------------------------------- /docker/nginx/README.md: -------------------------------------------------------------------------------- 1 | # Setup SSL nginx reverse proxy 2 | 3 | 1. Install mkcert [mkcert](https://github.com/FiloSottile/mkcert) 4 | 2. Exec `mkcert mydomain.local` and `mkcert -install` 5 | 3. Make sure the pem files are used in the nginx.conf file 6 | 4. Update HYPERDX_APP_URL to https://mydomain.local in the .env file 7 | 5. Update HYPERDX_APP_PORT to 443 (same as the nginx server port) in the .env file 8 | 6. Add the following to the /etc/hosts file 9 | ``` 10 | 127.0.0.1 mydomain.local 11 | ``` 12 | 7. Comment out ports mapping in the docker-compose.yml file for `app` service (so that the app is not exposed to the host) 13 | 8. Enable nginx service in the docker-compose.yml file 14 | 9. Run `docker-compose up -d` 15 | 10. Open https://mydomain.local in the browser 16 | -------------------------------------------------------------------------------- /docker/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # Main NGINX configuration 2 | user nginx; 3 | worker_processes auto; 4 | 5 | # Error log 6 | error_log /var/log/nginx/error.log warn; 7 | pid /var/run/nginx.pid; 8 | 9 | # Events block 10 | events { 11 | worker_connections 1024; 12 | } 13 | 14 | # HTTP block: Place your server block here 15 | http { 16 | include /etc/nginx/mime.types; 17 | default_type application/octet-stream; 18 | 19 | # Logging 20 | access_log /var/log/nginx/access.log; 21 | error_log /var/log/nginx/error.log; 22 | 23 | # Gzip compression 24 | gzip on; 25 | 26 | # Redirect HTTP to HTTPS 27 | server { 28 | listen 80; 29 | server_name mydomain.local www.mydomain.local; 30 | 31 | return 301 https://$host$request_uri; 32 | } 33 | 34 | # HTTPS reverse proxy 35 | server { 36 | listen 443 ssl; 37 | server_name mydomain.local www.mydomain.local; 38 | 39 | # TLS settings 40 | ssl_certificate /etc/nginx/ssl/mydomain.local.pem; 41 | ssl_certificate_key /etc/nginx/ssl/mydomain.local-key.pem; 42 | 43 | ssl_protocols TLSv1.2 TLSv1.3; 44 | ssl_ciphers HIGH:!aNULL:!MD5; 45 | 46 | location / { 47 | # Points to the HyperDX app service 48 | proxy_pass http://app:443; 49 | proxy_set_header Host $host; 50 | proxy_set_header X-Real-IP $remote_addr; 51 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 52 | proxy_set_header X-Forwarded-Proto $scheme; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docker/otel-collector/Dockerfile: -------------------------------------------------------------------------------- 1 | ## base ############################################################################################# 2 | FROM otel/opentelemetry-collector-contrib:0.126.0 AS col 3 | FROM otel/opentelemetry-collector-opampsupervisor:0.126.0 AS supervisor 4 | 5 | # From: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/aa5c3aa4c7ec174361fcaf908de8eaca72263078/cmd/opampsupervisor/Dockerfile#L18 6 | FROM alpine:latest@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c AS prep 7 | RUN apk --update add ca-certificates 8 | RUN mkdir -p /etc/otel/supervisor-data/ 9 | 10 | FROM scratch AS base 11 | 12 | ARG USER_UID=10001 13 | ARG USER_GID=10001 14 | USER ${USER_UID}:${USER_GID} 15 | 16 | COPY --from=prep /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 17 | COPY --from=prep --chmod=777 --chown=${USER_UID}:${USER_GID} /etc/otel/supervisor-data /etc/otel/supervisor-data 18 | COPY --from=supervisor --chmod=755 /usr/local/bin/opampsupervisor / 19 | COPY ./supervisor_docker.yaml /etc/otel/supervisor.yaml 20 | COPY --from=col --chmod=755 /otelcol-contrib /otelcontribcol 21 | 22 | ## dev ############################################################################################## 23 | FROM base AS dev 24 | 25 | COPY ./config.yaml /etc/otelcol-contrib/config.yaml 26 | 27 | EXPOSE 4317 4318 13133 28 | 29 | ENTRYPOINT ["/opampsupervisor"] 30 | CMD ["--config", "/etc/otel/supervisor.yaml"] 31 | 32 | ## prod ############################################################################################# 33 | FROM base AS prod 34 | 35 | COPY ./config.yaml /etc/otelcol-contrib/config.yaml 36 | 37 | EXPOSE 4317 4318 13133 38 | 39 | ENTRYPOINT ["/opampsupervisor"] 40 | CMD ["--config", "/etc/otel/supervisor.yaml"] 41 | -------------------------------------------------------------------------------- /docker/otel-collector/supervisor_docker.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/cmd/opampsupervisor/specification/README.md#supervisor-configuration 2 | server: 3 | endpoint: ${OPAMP_SERVER_URL}/v1/opamp 4 | tls: 5 | # Disable verification to test locally. 6 | # Don't do this in production. 7 | insecure_skip_verify: true 8 | # For more TLS settings see config/configtls.ClientConfig 9 | 10 | capabilities: 11 | reports_effective_config: true 12 | reports_own_metrics: true 13 | reports_own_logs: true 14 | reports_own_traces: true 15 | reports_health: true 16 | accepts_remote_config: true 17 | reports_remote_config: true 18 | 19 | agent: 20 | executable: /otelcontribcol 21 | config_files: 22 | - /etc/otelcol-contrib/config.yaml 23 | # passthrough_logs: true # enable to debug collector logs, can crash collector due to perf issues with this flag enabled 24 | 25 | storage: 26 | directory: /etc/otel/supervisor-data/ 27 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "affected": { 3 | "defaultBase": "main" 4 | }, 5 | "workspaceLayout": { 6 | "appsDir": "packages", 7 | "libsDir": "packages" 8 | }, 9 | "targetDefaults": { 10 | "build": { 11 | "dependsOn": ["^build"] 12 | } 13 | }, 14 | "tasksRunnerOptions": { 15 | "default": { 16 | "runner": "nx/tasks-runners/default", 17 | "options": { 18 | "cacheableOperations": ["build", "lint", "test"] 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperdx", 3 | "private": true, 4 | "version": "2-beta", 5 | "license": "MIT", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "devDependencies": { 10 | "@changesets/cli": "^2.26.2", 11 | "@nx/workspace": "16.8.1", 12 | "@typescript-eslint/eslint-plugin": "^8.7.0", 13 | "@typescript-eslint/parser": "^8.7.0", 14 | "concurrently": "^9.1.2", 15 | "dotenv": "^16.4.7", 16 | "dotenv-cli": "^8.0.0", 17 | "dotenv-expand": "^12.0.1", 18 | "eslint": "^8.57.0", 19 | "eslint-config-next": "13", 20 | "eslint-config-prettier": "^9.1.0", 21 | "eslint-plugin-n": "^16.4.0", 22 | "eslint-plugin-prettier": "^5.2.1", 23 | "eslint-plugin-security": "^2.1.0", 24 | "eslint-plugin-simple-import-sort": "^12.1.1", 25 | "husky": "^8.0.3", 26 | "lint-staged": "^13.1.2", 27 | "nx": "16.8.1", 28 | "prettier": "3.3.3" 29 | }, 30 | "scripts": { 31 | "setup": "yarn install && husky install", 32 | "app:dev": "npx concurrently -k -n 'API,APP,ALERTS-TASK,COMMON-UTILS' -c 'green.bold,blue.bold,yellow.bold,magenta' 'nx run @hyperdx/api:dev' 'nx run @hyperdx/app:dev' 'nx run @hyperdx/api:dev-task check-alerts' 'nx run @hyperdx/common-utils:dev'", 33 | "app:lint": "nx run @hyperdx/app:ci:lint", 34 | "dev": "docker compose -f docker-compose.dev.yml up -d && yarn app:dev && docker compose -f docker-compose.dev.yml down", 35 | "dev:down": "docker compose -f docker-compose.dev.yml down", 36 | "dev:compose": "docker compose -f docker-compose.dev.yml", 37 | "lint": "npx nx run-many -t ci:lint", 38 | "version": "make version", 39 | "release": "npx changeset tag && npx changeset publish" 40 | }, 41 | "lint-staged": { 42 | "**/*.{ts,tsx}": [ 43 | "prettier --write --ignore-unknown", 44 | "eslint --fix --quiet" 45 | ], 46 | "**/*.{mdx,json,yml}": [ 47 | "prettier --write --ignore-unknown" 48 | ] 49 | }, 50 | "packageManager": "yarn@4.5.1" 51 | } 52 | -------------------------------------------------------------------------------- /packages/api/.Dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/build -------------------------------------------------------------------------------- /packages/api/.env.test: -------------------------------------------------------------------------------- 1 | CLICKHOUSE_HOST=http://localhost:8123 2 | CLICKHOUSE_PASSWORD=api 3 | CLICKHOUSE_USER=api 4 | RUN_SCHEDULED_TASKS_EXTERNALLY=true 5 | EXPRESS_SESSION_SECRET="hyperdx is cool 👋" 6 | FRONTEND_URL=http://app:8080 7 | MONGO_URI=mongodb://localhost:29999/hyperdx-test 8 | NODE_ENV=test 9 | PORT=9000 10 | OPAMP_PORT=4320 11 | -------------------------------------------------------------------------------- /packages/api/.eslintignore: -------------------------------------------------------------------------------- 1 | keys 2 | node_modules 3 | archive 4 | migrate-mongo-config.ts 5 | jest.setup.ts 6 | -------------------------------------------------------------------------------- /packages/api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | ignorePatterns: ['migrate-mongo-config.ts'], 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint', 'prettier', 'simple-import-sort'], 6 | parserOptions: { 7 | tsconfigRootDir: __dirname, 8 | project: ['./tsconfig.json', './tsconfig.test.json'], 9 | }, 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:@typescript-eslint/eslint-recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'plugin:n/recommended', 15 | 'plugin:prettier/recommended', 16 | 'plugin:security/recommended-legacy', 17 | ], 18 | rules: { 19 | '@typescript-eslint/ban-ts-comment': 'warn', 20 | '@typescript-eslint/no-empty-interface': 'off', 21 | '@typescript-eslint/no-empty-object-type': 'warn', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | '@typescript-eslint/no-floating-promises': 'error', 24 | '@typescript-eslint/no-namespace': 'warn', 25 | '@typescript-eslint/no-unused-vars': 'warn', 26 | 'n/no-process-exit': 'warn', 27 | 'n/no-missing-import': 'off', 28 | 'n/no-unpublished-import': [ 29 | 'error', 30 | { 31 | allowModules: ['mongodb', 'supertest'], 32 | }, 33 | ], 34 | 'n/no-unsupported-features/es-syntax': [ 35 | 'error', 36 | { 37 | ignores: ['modules'], 38 | }, 39 | ], 40 | 'prettier/prettier': 'error', 41 | 'simple-import-sort/imports': 'error', 42 | 'simple-import-sort/exports': 'error', 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/api/Dockerfile: -------------------------------------------------------------------------------- 1 | ## base ############################################################################################# 2 | FROM node:22.16.0-alpine AS base 3 | 4 | WORKDIR /app 5 | 6 | COPY .yarn ./.yarn 7 | COPY .yarnrc.yml yarn.lock package.json nx.json .prettierrc .prettierignore ./ 8 | COPY ./packages/common-utils ./packages/common-utils 9 | COPY ./packages/api/jest.config.js ./packages/api/tsconfig.json ./packages/api/package.json ./packages/api/ 10 | RUN yarn install --mode=skip-build && yarn cache clean 11 | 12 | 13 | ## dev ############################################################################################# 14 | 15 | FROM base AS dev 16 | 17 | EXPOSE 8000 18 | 19 | ENTRYPOINT ["npx", "nx", "run", "@hyperdx/api:dev"] 20 | 21 | 22 | ## builder ######################################################################################### 23 | 24 | FROM base AS builder 25 | 26 | COPY ./packages/api/src ./packages/api/src 27 | RUN npx nx run-many --target=build --projects=@hyperdx/common-utils,@hyperdx/api 28 | RUN rm -rf node_modules && yarn workspaces focus @hyperdx/api --production 29 | 30 | 31 | ## prod ############################################################################################ 32 | 33 | FROM node:22.16.0-alpine AS prod 34 | 35 | ARG CODE_VERSION 36 | 37 | ENV CODE_VERSION=$CODE_VERSION 38 | 39 | ARG PORT 40 | 41 | ENV PORT=$PORT 42 | 43 | EXPOSE ${PORT} 44 | 45 | USER node 46 | 47 | WORKDIR /app 48 | 49 | COPY --chown=node:node --from=builder /app/node_modules ./node_modules 50 | COPY --chown=node:node --from=builder /app/packages/api/build ./packages/api/build 51 | COPY --chown=node:node --from=builder /app/packages/common-utils/dist ./packages/common-utils/dist 52 | COPY --chown=node:node --from=base /app/packages/common-utils/node_modules ./packages/common-utils/node_modules 53 | 54 | ENTRYPOINT ["node", "-r", "@hyperdx/node-opentelemetry/build/src/tracing", "./packages/api/build/index.js"] 55 | -------------------------------------------------------------------------------- /packages/api/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | setupFilesAfterEnv: ['/../jest.setup.ts'], 4 | setupFiles: ['dotenv/config'], 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | verbose: true, 8 | rootDir: './src', 9 | testMatch: ['**/__tests__/*.test.ts?(x)'], 10 | testTimeout: 30000, 11 | moduleNameMapper: { 12 | '@/(.*)$': '/$1', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/api/jest.setup.ts: -------------------------------------------------------------------------------- 1 | // @eslint-disable @typescript-eslint/no-var-requires 2 | jest.retryTimes(1, { logErrorsBeforeRetry: true }); 3 | -------------------------------------------------------------------------------- /packages/api/migrate-mongo-config.ts: -------------------------------------------------------------------------------- 1 | export = { 2 | mongodb: { 3 | // TODO Change (or review) the url to your MongoDB: 4 | url: 'mongodb://localhost:27017', 5 | 6 | // TODO Change this to your database name: 7 | databaseName: 'hyperdx', 8 | 9 | options: { 10 | // useNewUrlParser: true, // removes a deprecation warning when connecting 11 | // useUnifiedTopology: true, // removes a deprecating warning when connecting 12 | // connectTimeoutMS: 3600000, // increase connection timeout to 1 hour 13 | // socketTimeoutMS: 3600000, // increase socket timeout to 1 hour 14 | }, 15 | }, 16 | 17 | // The migrations dir, can be an relative or absolute path. Only edit this when really necessary. 18 | migrationsDir: 'migrations/mongo', 19 | 20 | // The mongodb collection where the applied changes are stored. Only edit this when really necessary. 21 | changelogCollectionName: 'changelog', 22 | 23 | // The file extension to create migrations and search for in migration dir 24 | migrationFileExtension: '.ts', 25 | 26 | // Enable the algorithm to create a checksum of the file contents and use that in the comparison to determine 27 | // if the file should be run. Requires that scripts are coded to be run multiple times. 28 | useFileHash: false, 29 | 30 | // Don't change this, unless you know what you're doing 31 | moduleSystem: 'commonjs', 32 | }; 33 | -------------------------------------------------------------------------------- /packages/api/migrations/ch/000001_add_is_delta_n_is_monotonic_fields_to_metric_stream_table.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE default.metric_stream DROP COLUMN is_delta; 2 | 3 | ALTER TABLE default.metric_stream DROP COLUMN is_monotonic; 4 | -------------------------------------------------------------------------------- /packages/api/migrations/ch/000001_add_is_delta_n_is_monotonic_fields_to_metric_stream_table.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE default.metric_stream ADD COLUMN is_delta Boolean CODEC(Delta, ZSTD(1)); 2 | 3 | ALTER TABLE default.metric_stream ADD COLUMN is_monotonic Boolean CODEC(Delta, ZSTD(1)); 4 | -------------------------------------------------------------------------------- /packages/api/migrations/mongo/20231130053610-add_accessKey_field_to_user_collection.ts: -------------------------------------------------------------------------------- 1 | import { Db, MongoClient } from 'mongodb'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | module.exports = { 5 | async up(db: Db, client: MongoClient) { 6 | await db 7 | .collection('users') 8 | .updateMany({}, { $set: { accessKey: uuidv4() } }); 9 | }, 10 | async down(db: Db, client: MongoClient) { 11 | await db.collection('users').updateMany({}, { $unset: { accessKey: '' } }); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/api/scripts/generate-api-docs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import swaggerJsdoc from 'swagger-jsdoc'; 4 | 5 | import { swaggerOptions } from '../src/utils/swagger'; 6 | 7 | const specs = swaggerJsdoc(swaggerOptions); 8 | const outputPath = path.resolve(__dirname, '../../public/openapi.json'); 9 | fs.mkdirSync(path.dirname(outputPath), { recursive: true }); 10 | fs.writeFileSync(outputPath, JSON.stringify(specs, null, 2)); 11 | 12 | console.log(`OpenAPI specification written to ${outputPath}`); 13 | -------------------------------------------------------------------------------- /packages/api/src/config.ts: -------------------------------------------------------------------------------- 1 | const env = process.env; 2 | 3 | // DEFAULTS 4 | const DEFAULT_APP_TYPE = 'api'; 5 | const DEFAULT_EXPRESS_SESSION = 'hyperdx is cool 👋'; 6 | const DEFAULT_FRONTEND_URL = `http://localhost:${env.HYPERDX_APP_PORT}`; 7 | 8 | export const NODE_ENV = env.NODE_ENV as string; 9 | 10 | export const APP_TYPE = (env.APP_TYPE || DEFAULT_APP_TYPE) as 11 | | 'api' 12 | | 'scheduled-task'; 13 | export const CODE_VERSION = env.CODE_VERSION ?? ''; 14 | export const EXPRESS_SESSION_SECRET = (env.EXPRESS_SESSION_SECRET || 15 | DEFAULT_EXPRESS_SESSION) as string; 16 | export const FRONTEND_URL = (env.FRONTEND_URL || 17 | DEFAULT_FRONTEND_URL) as string; 18 | export const HYPERDX_API_KEY = env.HYPERDX_API_KEY as string; 19 | export const HYPERDX_LOG_LEVEL = env.HYPERDX_LOG_LEVEL as string; 20 | export const IS_CI = NODE_ENV === 'test'; 21 | export const IS_DEV = NODE_ENV === 'development'; 22 | export const IS_PROD = NODE_ENV === 'production'; 23 | export const MINER_API_URL = env.MINER_API_URL as string; 24 | export const MONGO_URI = env.MONGO_URI; 25 | export const OTEL_SERVICE_NAME = env.OTEL_SERVICE_NAME as string; 26 | export const PORT = Number.parseInt(env.PORT as string); 27 | export const OPAMP_PORT = Number.parseInt(env.OPAMP_PORT as string); 28 | export const USAGE_STATS_ENABLED = env.USAGE_STATS_ENABLED !== 'false'; 29 | export const RUN_SCHEDULED_TASKS_EXTERNALLY = 30 | env.RUN_SCHEDULED_TASKS_EXTERNALLY === 'true'; 31 | 32 | // Only for single container local deployments, disable authentication 33 | export const IS_LOCAL_APP_MODE = 34 | env.IS_LOCAL_APP_MODE === 'DANGEROUSLY_is_local_app_mode💀'; 35 | 36 | // Only used to bootstrap empty instances 37 | export const DEFAULT_CONNECTIONS = env.DEFAULT_CONNECTIONS; 38 | export const DEFAULT_SOURCES = env.DEFAULT_SOURCES; 39 | 40 | // FOR CI ONLY 41 | export const CLICKHOUSE_HOST = env.CLICKHOUSE_HOST as string; 42 | export const CLICKHOUSE_USER = env.CLICKHOUSE_USER as string; 43 | export const CLICKHOUSE_PASSWORD = env.CLICKHOUSE_PASSWORD as string; 44 | -------------------------------------------------------------------------------- /packages/api/src/controllers/__tests__/team.test.ts: -------------------------------------------------------------------------------- 1 | import { createTeam, getTeam, getTeamByApiKey } from '@/controllers/team'; 2 | import { clearDBCollections, closeDB, connectDB } from '@/fixtures'; 3 | 4 | describe('team controller', () => { 5 | beforeAll(async () => { 6 | await connectDB(); 7 | }); 8 | 9 | afterEach(async () => { 10 | await clearDBCollections(); 11 | }); 12 | 13 | afterAll(async () => { 14 | await closeDB(); 15 | }); 16 | 17 | it('createTeam + getTeam', async () => { 18 | const team = await createTeam({ name: 'My Team' }); 19 | 20 | expect(team.name).toBe('My Team'); 21 | 22 | team.apiKey = 'apiKey'; 23 | 24 | await team.save(); 25 | 26 | expect(await getTeam(team._id)).toBeTruthy(); 27 | expect(await getTeamByApiKey('apiKey')).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/api/src/controllers/connection.ts: -------------------------------------------------------------------------------- 1 | import Connection, { IConnection } from '@/models/connection'; 2 | 3 | export function getConnections() { 4 | // Never return password back to the user 5 | // Return all connections in current tenant 6 | return Connection.find({}); 7 | } 8 | 9 | export function getConnectionById( 10 | team: string, 11 | connectionId: string, 12 | selectPassword = false, 13 | ) { 14 | return Connection.findOne({ _id: connectionId, team }).select( 15 | selectPassword ? '+password' : '', 16 | ); 17 | } 18 | 19 | export function createConnection( 20 | team: string, 21 | connection: Omit, 22 | ) { 23 | return Connection.create({ ...connection, team }); 24 | } 25 | 26 | export function updateConnection( 27 | team: string, 28 | connectionId: string, 29 | connection: Omit, 30 | ) { 31 | return Connection.findOneAndUpdate({ _id: connectionId, team }, connection, { 32 | new: true, 33 | }); 34 | } 35 | 36 | export function deleteConnection(team: string, connectionId: string) { 37 | return Connection.findOneAndDelete({ _id: connectionId, team }); 38 | } 39 | -------------------------------------------------------------------------------- /packages/api/src/controllers/savedSearch.ts: -------------------------------------------------------------------------------- 1 | import { SavedSearchSchema } from '@hyperdx/common-utils/dist/types'; 2 | import { groupBy } from 'lodash'; 3 | import { z } from 'zod'; 4 | 5 | import { deleteSavedSearchAlerts } from '@/controllers/alerts'; 6 | import Alert from '@/models/alert'; 7 | import { SavedSearch } from '@/models/savedSearch'; 8 | 9 | type SavedSearchWithoutId = Omit, 'id'>; 10 | 11 | export async function getSavedSearches(teamId: string) { 12 | const savedSearches = await SavedSearch.find({ team: teamId }); 13 | const alerts = await Alert.find( 14 | { team: teamId, savedSearch: { $exists: true, $ne: null } }, 15 | { __v: 0 }, 16 | ); 17 | 18 | const alertsBySavedSearchId = groupBy(alerts, 'savedSearch'); 19 | 20 | return savedSearches.map(savedSearch => ({ 21 | ...savedSearch.toJSON(), 22 | alerts: alertsBySavedSearchId[savedSearch._id.toString()] 23 | ?.map(alert => alert.toJSON()) 24 | .map(({ _id, ...alert }) => ({ id: _id, ...alert })), // Remap _id to id 25 | })); 26 | } 27 | 28 | export function getSavedSearch(teamId: string, savedSearchId: string) { 29 | return SavedSearch.findOne({ _id: savedSearchId, team: teamId }); 30 | } 31 | 32 | export function createSavedSearch( 33 | teamId: string, 34 | savedSearch: SavedSearchWithoutId, 35 | ) { 36 | return SavedSearch.create({ ...savedSearch, team: teamId }); 37 | } 38 | 39 | export function updateSavedSearch( 40 | teamId: string, 41 | savedSearchId: string, 42 | savedSearch: SavedSearchWithoutId, 43 | ) { 44 | return SavedSearch.findOneAndUpdate( 45 | { _id: savedSearchId, team: teamId }, 46 | { 47 | ...savedSearch, 48 | team: teamId, 49 | }, 50 | { new: true }, 51 | ); 52 | } 53 | 54 | export async function deleteSavedSearch(teamId: string, savedSearchId: string) { 55 | const savedSearch = await SavedSearch.findOneAndDelete({ 56 | _id: savedSearchId, 57 | team: teamId, 58 | }); 59 | if (savedSearch) { 60 | await deleteSavedSearchAlerts(savedSearchId, teamId); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/api/src/controllers/sources.ts: -------------------------------------------------------------------------------- 1 | import { ISource, Source } from '@/models/source'; 2 | 3 | export function getSources(team: string) { 4 | return Source.find({ team }); 5 | } 6 | 7 | export function createSource(team: string, source: Omit) { 8 | return Source.create({ ...source, team }); 9 | } 10 | 11 | export function updateSource( 12 | team: string, 13 | sourceId: string, 14 | source: Omit, 15 | ) { 16 | return Source.findOneAndUpdate({ _id: sourceId, team }, source, { 17 | new: true, 18 | }); 19 | } 20 | 21 | export function deleteSource(team: string, sourceId: string) { 22 | return Source.findOneAndDelete({ _id: sourceId, team }); 23 | } 24 | -------------------------------------------------------------------------------- /packages/api/src/controllers/user.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectId } from '@/models'; 2 | import User from '@/models/user'; 3 | 4 | export function findUserByAccessKey(accessKey: string) { 5 | return User.findOne({ accessKey }); 6 | } 7 | 8 | export function findUserById(id: string) { 9 | return User.findById(id); 10 | } 11 | 12 | export function findUserByEmail(email: string) { 13 | return User.findOne({ email }); 14 | } 15 | 16 | export async function findUserByEmailInTeam( 17 | email: string, 18 | team: string | ObjectId, 19 | ) { 20 | return User.findOne({ email, team }); 21 | } 22 | 23 | export function findUsersByTeam(team: string | ObjectId) { 24 | return User.find({ team }).sort({ createdAt: 1 }); 25 | } 26 | 27 | export function deleteTeamMember(teamId: string | ObjectId, userId: string) { 28 | return User.findOneAndDelete({ 29 | team: teamId, 30 | _id: userId, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { getHyperDXMetricReader } from '@hyperdx/node-opentelemetry/build/src/metrics'; 2 | import { HostMetrics } from '@opentelemetry/host-metrics'; 3 | import { MeterProvider, MetricReader } from '@opentelemetry/sdk-metrics'; 4 | import { serializeError } from 'serialize-error'; 5 | 6 | import * as config from '@/config'; 7 | import Server from '@/server'; 8 | import { isOperationalError } from '@/utils/errors'; 9 | import logger from '@/utils/logger'; 10 | 11 | if (config.IS_DEV) { 12 | // Start collecting host metrics 13 | const meterProvider = new MeterProvider({ 14 | // FIXME: missing selectCardinalityLimit property 15 | readers: [getHyperDXMetricReader() as unknown as MetricReader], 16 | }); 17 | const hostMetrics = new HostMetrics({ meterProvider }); 18 | hostMetrics.start(); 19 | } 20 | 21 | const server = new Server(); 22 | 23 | process.on('uncaughtException', (err: Error) => { 24 | logger.error(serializeError(err)); 25 | 26 | // FIXME: disable server restart until 27 | // we make sure all expected exceptions are handled properly 28 | if (config.IS_DEV && !isOperationalError(err)) { 29 | process.exit(1); 30 | } 31 | }); 32 | 33 | process.on('unhandledRejection', (err: any) => { 34 | // TODO: do we want to throw here ? 35 | logger.error(serializeError(err)); 36 | }); 37 | 38 | server.start().catch(e => logger.error(serializeError(e))); 39 | -------------------------------------------------------------------------------- /packages/api/src/middleware/cors.ts: -------------------------------------------------------------------------------- 1 | import cors from 'cors'; 2 | 3 | import { FRONTEND_URL } from '@/config'; 4 | 5 | export const noCors = cors(); 6 | 7 | export default cors({ credentials: true, origin: FRONTEND_URL }); 8 | -------------------------------------------------------------------------------- /packages/api/src/middleware/error.ts: -------------------------------------------------------------------------------- 1 | import { recordException } from '@hyperdx/node-opentelemetry'; 2 | import type { NextFunction, Request, Response } from 'express'; 3 | 4 | import { IS_PROD } from '@/config'; 5 | import { BaseError, isOperationalError, StatusCode } from '@/utils/errors'; 6 | 7 | // WARNING: need to keep the 4th arg for express to identify it as an error-handling middleware function 8 | export const appErrorHandler = ( 9 | err: BaseError, 10 | _: Request, 11 | res: Response, 12 | next: NextFunction, 13 | ) => { 14 | if (!IS_PROD) { 15 | console.error(err); 16 | } 17 | 18 | const userFacingErrorMessage = isOperationalError(err) 19 | ? err.name || err.message 20 | : 'Something went wrong :('; 21 | 22 | void recordException(err, { 23 | mechanism: { 24 | type: 'generic', 25 | handled: userFacingErrorMessage ? true : false, 26 | }, 27 | }); 28 | 29 | if (!res.headersSent) { 30 | res.status(err.statusCode ?? StatusCode.INTERNAL_SERVER).json({ 31 | message: userFacingErrorMessage, 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /packages/api/src/middleware/validation.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { z } from 'zod'; 3 | 4 | export function validateRequestHeaders(schema: T) { 5 | return function ( 6 | req: express.Request, 7 | res: express.Response, 8 | next: express.NextFunction, 9 | ) { 10 | const parsed = schema.safeParse(req.headers); 11 | if (!parsed.success) { 12 | return res.status(400).json({ type: 'Headers', errors: parsed.error }); 13 | } 14 | 15 | return next(); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/api/src/models/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { createTeam } from '@/controllers/team'; 2 | import { clearDBCollections, closeDB, connectDB } from '@/fixtures'; 3 | import Team from '@/models/team'; 4 | 5 | describe('team controller', () => { 6 | beforeAll(async () => { 7 | await connectDB(); 8 | }); 9 | 10 | afterEach(async () => { 11 | await clearDBCollections(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await closeDB(); 16 | }); 17 | 18 | it('does not query for non-existent properties', async () => { 19 | const team = await createTeam({ name: 'My Team' }); 20 | 21 | expect(await Team.find({ name: 'My Team' })).toHaveLength(1); 22 | expect(await Team.find({ fakeProperty: 'please' })).toHaveLength(0); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/api/src/models/alertHistory.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import ms from 'ms'; 3 | 4 | import { AlertState } from '@/models/alert'; 5 | 6 | import type { ObjectId } from '.'; 7 | 8 | export interface IAlertHistory { 9 | alert: ObjectId; 10 | counts: number; 11 | createdAt: Date; 12 | state: AlertState; 13 | lastValues: { startTime: Date; count: number }[]; 14 | } 15 | 16 | const AlertHistorySchema = new Schema({ 17 | counts: { 18 | type: Number, 19 | default: 0, 20 | }, 21 | createdAt: { 22 | type: Date, 23 | required: true, 24 | }, 25 | alert: { type: mongoose.Schema.Types.ObjectId, ref: 'Alert' }, 26 | state: { 27 | type: String, 28 | enum: Object.values(AlertState), 29 | required: true, 30 | }, 31 | lastValues: [ 32 | { 33 | startTime: { 34 | type: Date, 35 | required: true, 36 | }, 37 | count: { 38 | type: Number, 39 | required: true, 40 | }, 41 | }, 42 | ], 43 | }); 44 | 45 | AlertHistorySchema.index( 46 | { createdAt: 1 }, 47 | { expireAfterSeconds: ms('30d') / 1000 }, 48 | ); 49 | 50 | AlertHistorySchema.index({ alert: 1, createdAt: -1 }); 51 | 52 | export default mongoose.model( 53 | 'AlertHistory', 54 | AlertHistorySchema, 55 | ); 56 | -------------------------------------------------------------------------------- /packages/api/src/models/connection.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | type ObjectId = mongoose.Types.ObjectId; 5 | 6 | export interface IConnection { 7 | _id: ObjectId; 8 | host: string; 9 | name: string; 10 | password: string; 11 | username: string; 12 | team: ObjectId; 13 | } 14 | 15 | export default mongoose.model( 16 | 'Connection', 17 | new Schema( 18 | { 19 | team: { 20 | type: mongoose.Schema.Types.ObjectId, 21 | required: true, 22 | ref: 'Team', 23 | }, 24 | name: String, 25 | host: String, 26 | username: String, 27 | password: { 28 | type: String, 29 | select: false, 30 | }, 31 | }, 32 | { 33 | timestamps: true, 34 | toJSON: { virtuals: true }, 35 | }, 36 | ), 37 | ); 38 | -------------------------------------------------------------------------------- /packages/api/src/models/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { DashboardSchema } from '@hyperdx/common-utils/dist/types'; 2 | import mongoose, { Schema } from 'mongoose'; 3 | import { z } from 'zod'; 4 | 5 | import type { ObjectId } from '.'; 6 | 7 | export interface IDashboard extends z.infer { 8 | _id: ObjectId; 9 | team: ObjectId; 10 | } 11 | 12 | export default mongoose.model( 13 | 'Dashboard', 14 | new Schema( 15 | { 16 | name: { 17 | type: String, 18 | required: true, 19 | }, 20 | tiles: { type: mongoose.Schema.Types.Mixed, required: true }, 21 | team: { type: mongoose.Schema.Types.ObjectId, ref: 'Team' }, 22 | tags: { 23 | type: [String], 24 | default: [], 25 | }, 26 | }, 27 | { 28 | timestamps: true, 29 | toJSON: { getters: true }, 30 | }, 31 | ), 32 | ); 33 | -------------------------------------------------------------------------------- /packages/api/src/models/index.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | import * as config from '@/config'; 4 | import logger from '@/utils/logger'; 5 | 6 | export type ObjectId = mongoose.Types.ObjectId; 7 | 8 | // set flags 9 | mongoose.set('strictQuery', false); 10 | 11 | // Allow empty strings to be set to required fields 12 | // https://github.com/Automattic/mongoose/issues/7150 13 | // ex. query in logview can be empty 14 | mongoose.Schema.Types.String.checkRequired(v => v != null); 15 | 16 | // connection events handlers 17 | mongoose.connection.on('connected', () => { 18 | logger.info('Connection established to MongoDB'); 19 | }); 20 | 21 | mongoose.connection.on('disconnected', () => { 22 | logger.info('Lost connection to MongoDB server'); 23 | }); 24 | 25 | mongoose.connection.on('error', () => { 26 | logger.error('Could not connect to MongoDB'); 27 | }); 28 | 29 | mongoose.connection.on('reconnected', () => { 30 | logger.error('Reconnected to MongoDB'); 31 | }); 32 | 33 | mongoose.connection.on('reconnectFailed', () => { 34 | logger.error('Failed to reconnect to MongoDB'); 35 | }); 36 | 37 | export const connectDB = async () => { 38 | if (config.MONGO_URI == null) { 39 | throw new Error('MONGO_URI is not set'); 40 | } 41 | await mongoose.connect(config.MONGO_URI, { 42 | heartbeatFrequencyMS: 10000, // retry failed heartbeats 43 | maxPoolSize: 100, // 5 nodes -> max 1000 connections 44 | }); 45 | }; 46 | 47 | export const mongooseConnection = mongoose.connection; 48 | -------------------------------------------------------------------------------- /packages/api/src/models/savedSearch.ts: -------------------------------------------------------------------------------- 1 | import { SavedSearchSchema } from '@hyperdx/common-utils/dist/types'; 2 | import mongoose, { Schema } from 'mongoose'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import { z } from 'zod'; 5 | 6 | type ObjectId = mongoose.Types.ObjectId; 7 | 8 | export interface ISavedSearch 9 | extends Omit, 'source'> { 10 | _id: ObjectId; 11 | team: ObjectId; 12 | source: ObjectId; 13 | } 14 | 15 | export const SavedSearch = mongoose.model( 16 | 'SavedSearch', 17 | new Schema( 18 | { 19 | team: { 20 | type: mongoose.Schema.Types.ObjectId, 21 | required: true, 22 | ref: 'Team', 23 | }, 24 | 25 | name: String, 26 | select: String, 27 | where: String, 28 | whereLanguage: String, 29 | orderBy: String, 30 | source: { 31 | type: mongoose.Schema.Types.ObjectId, 32 | required: true, 33 | ref: 'Source', 34 | }, 35 | tags: [String], 36 | }, 37 | { 38 | toJSON: { virtuals: true }, 39 | timestamps: true, 40 | }, 41 | ), 42 | ); 43 | -------------------------------------------------------------------------------- /packages/api/src/models/team.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | type ObjectId = mongoose.Types.ObjectId; 5 | 6 | export interface ITeam { 7 | _id: ObjectId; 8 | name: string; 9 | allowedAuthMethods?: 'password'[]; 10 | apiKey: string; 11 | hookId: string; 12 | collectorAuthenticationEnforced: boolean; 13 | } 14 | 15 | export default mongoose.model( 16 | 'Team', 17 | new Schema( 18 | { 19 | name: String, 20 | allowedAuthMethods: [String], 21 | hookId: { 22 | type: String, 23 | default: function genUUID() { 24 | return uuidv4(); 25 | }, 26 | }, 27 | apiKey: { 28 | type: String, 29 | default: function genUUID() { 30 | return uuidv4(); 31 | }, 32 | }, 33 | collectorAuthenticationEnforced: { 34 | type: Boolean, 35 | default: false, 36 | }, 37 | }, 38 | { 39 | timestamps: true, 40 | }, 41 | ), 42 | ); 43 | -------------------------------------------------------------------------------- /packages/api/src/models/teamInvite.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import ms from 'ms'; 3 | 4 | export interface ITeamInvite { 5 | createdAt: Date; 6 | email: string; 7 | name?: string; 8 | teamId: string; 9 | token: string; 10 | updatedAt: Date; 11 | } 12 | 13 | const TeamInviteSchema = new Schema( 14 | { 15 | teamId: { 16 | type: Schema.Types.ObjectId, 17 | ref: 'Team', 18 | required: true, 19 | }, 20 | name: String, 21 | email: { 22 | type: String, 23 | required: true, 24 | }, 25 | token: { 26 | type: String, 27 | required: true, 28 | }, 29 | }, 30 | { 31 | timestamps: true, 32 | }, 33 | ); 34 | 35 | TeamInviteSchema.index( 36 | { createdAt: 1 }, 37 | { expireAfterSeconds: ms('30d') / 1000 }, 38 | ); 39 | 40 | TeamInviteSchema.index({ teamId: 1, email: 1 }, { unique: true }); 41 | 42 | export default mongoose.model('TeamInvite', TeamInviteSchema); 43 | -------------------------------------------------------------------------------- /packages/api/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | // @ts-ignore don't install the @types for this package, as it conflicts with mongoose 3 | import passportLocalMongoose from 'passport-local-mongoose'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | 6 | type ObjectId = mongoose.Types.ObjectId; 7 | 8 | export interface IUser { 9 | _id: ObjectId; 10 | accessKey: string; 11 | createdAt: Date; 12 | email: string; 13 | name: string; 14 | team: ObjectId; 15 | } 16 | 17 | export type UserDocument = mongoose.HydratedDocument; 18 | 19 | const UserSchema = new Schema( 20 | { 21 | name: String, 22 | email: { 23 | type: String, 24 | required: true, 25 | }, 26 | team: { type: mongoose.Schema.Types.ObjectId, ref: 'Team' }, 27 | accessKey: { 28 | type: String, 29 | default: function genUUID() { 30 | return uuidv4(); 31 | }, 32 | }, 33 | }, 34 | { 35 | timestamps: true, 36 | }, 37 | ); 38 | 39 | UserSchema.virtual('hasPasswordAuth').get(function (this: IUser) { 40 | return true; 41 | }); 42 | 43 | UserSchema.plugin(passportLocalMongoose, { 44 | usernameField: 'email', 45 | usernameLowerCase: true, 46 | usernameCaseInsensitive: true, 47 | }); 48 | 49 | UserSchema.index({ email: 1 }, { unique: true }); 50 | 51 | export default mongoose.model('User', UserSchema); 52 | -------------------------------------------------------------------------------- /packages/api/src/models/webhook.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb'; 2 | import mongoose, { Schema } from 'mongoose'; 3 | 4 | export enum WebhookService { 5 | Slack = 'slack', 6 | Generic = 'generic', 7 | } 8 | 9 | interface MongooseMap extends Map { 10 | // https://mongoosejs.com/docs/api/map.html#MongooseMap.prototype.toJSON() 11 | // Converts this map to a native JavaScript Map for JSON.stringify(). Set the flattenMaps option to convert this map to a POJO instead. 12 | // doc.myMap.toJSON() instanceof Map; // true 13 | // doc.myMap.toJSON({ flattenMaps: true }) instanceof Map; // false 14 | toJSON: (options?: { 15 | flattenMaps?: boolean; 16 | }) => { [key: string]: any } | Map; 17 | } 18 | 19 | export interface IWebhook { 20 | _id: ObjectId; 21 | createdAt: Date; 22 | name: string; 23 | service: WebhookService; 24 | team: ObjectId; 25 | updatedAt: Date; 26 | url?: string; 27 | description?: string; 28 | // reminder to serialize/convert the Mongoose model instance to a plain javascript object when using 29 | // to strip the additional properties that are related to the Mongoose internal representation -> webhook.headers.toJSON() 30 | queryParams?: MongooseMap; 31 | headers?: MongooseMap; 32 | body?: string; 33 | } 34 | 35 | const WebhookSchema = new Schema( 36 | { 37 | team: { type: Schema.Types.ObjectId, ref: 'Team' }, 38 | service: { 39 | type: String, 40 | enum: Object.values(WebhookService), 41 | required: true, 42 | }, 43 | name: { 44 | type: String, 45 | required: true, 46 | }, 47 | url: { 48 | type: String, 49 | required: false, 50 | }, 51 | description: { 52 | type: String, 53 | required: false, 54 | }, 55 | queryParams: { 56 | type: Map, 57 | of: String, 58 | required: false, 59 | }, 60 | headers: { 61 | type: Map, 62 | of: String, 63 | required: false, 64 | }, 65 | body: { 66 | type: String, 67 | required: false, 68 | }, 69 | }, 70 | { timestamps: true }, 71 | ); 72 | 73 | WebhookSchema.index({ team: 1, service: 1, name: 1 }, { unique: true }); 74 | 75 | export default mongoose.model('Webhook', WebhookSchema); 76 | -------------------------------------------------------------------------------- /packages/api/src/opamp/README.md: -------------------------------------------------------------------------------- 1 | Implements an HTTP OpAMP server that serves configurations to supervised 2 | collectors. 3 | 4 | Spec: https://github.com/open-telemetry/opamp-spec/tree/main 5 | 6 | Workflow: 7 | 8 | - Sup pings /v1/opamp with status 9 | - Server checks if configs should be updated 10 | - Return new config if current config is outdated 11 | - Config derived from team doc with ingestion api key 12 | -------------------------------------------------------------------------------- /packages/api/src/opamp/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { appErrorHandler } from '@/middleware/error'; 4 | import { opampController } from '@/opamp/controllers/opampController'; 5 | 6 | // Create Express application 7 | const app = express(); 8 | 9 | app.disable('x-powered-by'); 10 | 11 | // Special body parser setup for OpAMP 12 | app.use( 13 | '/v1/opamp', 14 | express.raw({ 15 | type: 'application/x-protobuf', 16 | limit: '10mb', 17 | }), 18 | ); 19 | 20 | // OpAMP endpoint 21 | app.post('/v1/opamp', opampController.handleOpampMessage.bind(opampController)); 22 | 23 | // Health check endpoint 24 | app.get('/health', (req, res) => { 25 | res.status(200).json({ status: 'OK' }); 26 | }); 27 | 28 | // Error handling 29 | app.use(appErrorHandler); 30 | 31 | export default app; 32 | -------------------------------------------------------------------------------- /packages/api/src/routers/api/index.ts: -------------------------------------------------------------------------------- 1 | import alertsRouter from './alerts'; 2 | import dashboardRouter from './dashboards'; 3 | import datasourceRouter from './datasources'; 4 | import meRouter from './me'; 5 | import rootRouter from './root'; 6 | import teamRouter from './team'; 7 | import webhooksRouter from './webhooks'; 8 | 9 | export default { 10 | alertsRouter, 11 | datasourceRouter, 12 | dashboardRouter, 13 | meRouter, 14 | rootRouter, 15 | teamRouter, 16 | webhooksRouter, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/api/src/routers/api/me.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { USAGE_STATS_ENABLED } from '@/config'; 4 | import { getTeam } from '@/controllers/team'; 5 | import { Api404Error } from '@/utils/errors'; 6 | 7 | const router = express.Router(); 8 | 9 | router.get('/', async (req, res, next) => { 10 | try { 11 | if (req.user == null) { 12 | throw new Api404Error('Request without user found'); 13 | } 14 | 15 | const { 16 | _id: id, 17 | accessKey, 18 | createdAt, 19 | email, 20 | name, 21 | team: teamId, 22 | } = req.user; 23 | 24 | const team = await getTeam(teamId); 25 | 26 | return res.json({ 27 | accessKey, 28 | createdAt, 29 | email, 30 | id, 31 | name, 32 | team, 33 | usageStatsEnabled: USAGE_STATS_ENABLED, 34 | }); 35 | } catch (e) { 36 | next(e); 37 | } 38 | }); 39 | 40 | export default router; 41 | -------------------------------------------------------------------------------- /packages/api/src/routers/external-api/__tests__/v2.test.ts: -------------------------------------------------------------------------------- 1 | import { getLoggedInAgent, getServer } from '../../../fixtures'; 2 | 3 | describe('external api v2', () => { 4 | const server = getServer(); 5 | 6 | beforeAll(async () => { 7 | await server.start(); 8 | }); 9 | 10 | afterEach(async () => { 11 | await server.clearDBs(); 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | afterAll(async () => { 16 | await server.stop(); 17 | }); 18 | 19 | it('GET /api/v2', async () => { 20 | const { agent, user } = await getLoggedInAgent(server); 21 | const resp = await agent 22 | .get(`/api/v2`) 23 | .set('Authorization', `Bearer ${user?.accessKey}`) 24 | .expect(200); 25 | expect(resp.body.version).toEqual('v2'); 26 | expect(resp.body.user._id).toEqual(user?._id.toString()); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/api/src/routers/external-api/v2/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { validateUserAccessKey } from '@/middleware/auth'; 4 | import alertsRouter from '@/routers/external-api/v2/alerts'; 5 | import dashboardRouter from '@/routers/external-api/v2/dashboards'; 6 | import { Api400Error, Api403Error } from '@/utils/errors'; 7 | import rateLimiter from '@/utils/rateLimiter'; 8 | 9 | const router = express.Router(); 10 | 11 | const rateLimiterKeyGenerator = (req: express.Request) => { 12 | return req.headers.authorization || req.ip; 13 | }; 14 | 15 | const defaultRateLimiter = rateLimiter({ 16 | windowMs: 60 * 1000, // 1 minute 17 | max: 100, // Limit each IP to 100 requests per `window` 18 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 19 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 20 | keyGenerator: rateLimiterKeyGenerator, 21 | }); 22 | 23 | router.get('/', validateUserAccessKey, (req, res, next) => { 24 | res.json({ 25 | version: 'v2', 26 | user: req.user?.toJSON(), 27 | }); 28 | }); 29 | 30 | router.use('/alerts', defaultRateLimiter, validateUserAccessKey, alertsRouter); 31 | 32 | router.use( 33 | '/dashboards', 34 | defaultRateLimiter, 35 | validateUserAccessKey, 36 | dashboardRouter, 37 | ); 38 | 39 | export default router; 40 | -------------------------------------------------------------------------------- /packages/api/src/utils/__tests__/common.test.ts: -------------------------------------------------------------------------------- 1 | import ms from 'ms'; 2 | 3 | import { convertMsToGranularityString } from '@/utils/common'; 4 | 5 | describe('utils/common', () => { 6 | it('convertMsToGranularityString', () => { 7 | // 30 second is the min granularity 8 | expect(convertMsToGranularityString(ms('1s'))).toEqual('30 second'); 9 | expect(convertMsToGranularityString(ms('30'))).toEqual('30 second'); 10 | expect(convertMsToGranularityString(ms('1m'))).toEqual('1 minute'); 11 | expect(convertMsToGranularityString(ms('5m'))).toEqual('5 minute'); 12 | expect(convertMsToGranularityString(ms('10m'))).toEqual('10 minute'); 13 | expect(convertMsToGranularityString(ms('15m'))).toEqual('15 minute'); 14 | expect(convertMsToGranularityString(ms('30m'))).toEqual('30 minute'); 15 | expect(convertMsToGranularityString(ms('60 minute'))).toEqual('1 hour'); 16 | expect(convertMsToGranularityString(ms('2h'))).toEqual('2 hour'); 17 | expect(convertMsToGranularityString(ms('6h'))).toEqual('6 hour'); 18 | expect(convertMsToGranularityString(ms('12h'))).toEqual('12 hour'); 19 | expect(convertMsToGranularityString(ms('1d'))).toEqual('1 day'); 20 | expect(convertMsToGranularityString(ms('2d'))).toEqual('2 day'); 21 | expect(convertMsToGranularityString(ms('7d'))).toEqual('7 day'); 22 | expect(convertMsToGranularityString(ms('30d'))).toEqual('30 day'); 23 | // 30 day is the max granularity 24 | expect(convertMsToGranularityString(ms('1y'))).toEqual('30 day'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/api/src/utils/__tests__/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { Api500Error, BaseError, isOperationalError } from '../errors'; 2 | 3 | describe('Errors utils', () => { 4 | test('BaseError class', () => { 5 | const e = new BaseError('nvim', 500, true, 'is the best editor!!!'); 6 | expect(e.name).toBe('nvim'); 7 | expect(e.statusCode).toBe(500); 8 | expect(e.isOperational).toBeTruthy(); 9 | expect(e.message).toBe('is the best editor!!!'); 10 | expect(e.stack?.includes('nvim: is the best editor')); 11 | }); 12 | 13 | test('isOperational', () => { 14 | expect( 15 | isOperationalError( 16 | new BaseError('nvim', 500, true, 'is the best editor!!!'), 17 | ), 18 | ).toBeTruthy(); 19 | expect(isOperationalError(new Api500Error('BANG'))).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/api/src/utils/__tests__/validators.test.ts: -------------------------------------------------------------------------------- 1 | import * as validators from '../validators'; 2 | 3 | describe('validators', () => { 4 | describe('validatePassword', () => { 5 | it('should return true if password is valid', () => { 6 | expect(validators.validatePassword('abcdefgh')).toBe(true); 7 | }); 8 | 9 | it('should return false if password is invalid', () => { 10 | expect(validators.validatePassword(null!)).toBe(false); 11 | expect(validators.validatePassword(undefined!)).toBe(false); 12 | expect(validators.validatePassword('')).toBe(false); 13 | expect(validators.validatePassword('1234567')).toBe(false); 14 | expect(validators.validatePassword('a'.repeat(65))).toBe(false); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/api/src/utils/email.ts: -------------------------------------------------------------------------------- 1 | export const sendAlert = ({ 2 | alertDetails, 3 | alertEvents, 4 | alertGroup, 5 | alertName, 6 | alertUrl, 7 | toEmail, 8 | }: { 9 | alertDetails: string; 10 | alertEvents: string; 11 | alertGroup?: string; 12 | alertName: string; 13 | alertUrl: string; 14 | toEmail: string; 15 | }) => { 16 | // Send alert email 17 | }; 18 | 19 | export const sendResetPasswordEmail = ({ 20 | toEmail, 21 | token, 22 | }: { 23 | toEmail: string; 24 | token: string; 25 | }) => { 26 | // Send reset password email 27 | }; 28 | -------------------------------------------------------------------------------- /packages/api/src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export enum StatusCode { 2 | BAD_REQUEST = 400, 3 | CONFLICT = 409, 4 | CONTENT_TOO_LARGE = 413, 5 | FORBIDDEN = 403, 6 | INTERNAL_SERVER = 500, 7 | NOT_FOUND = 404, 8 | OK = 200, 9 | UNAUTHORIZED = 401, 10 | } 11 | 12 | export class BaseError extends Error { 13 | name: string; 14 | 15 | statusCode: StatusCode; 16 | 17 | isOperational: boolean; 18 | 19 | constructor( 20 | name: string, 21 | statusCode: StatusCode, 22 | isOperational: boolean, 23 | description: string, 24 | ) { 25 | super(description); 26 | 27 | Object.setPrototypeOf(this, BaseError.prototype); 28 | 29 | this.name = name; 30 | this.statusCode = statusCode; 31 | this.isOperational = isOperational; 32 | } 33 | } 34 | 35 | export class Api500Error extends BaseError { 36 | constructor(name: string) { 37 | super(name, StatusCode.INTERNAL_SERVER, true, 'Internal Server Error'); 38 | } 39 | } 40 | 41 | export class Api400Error extends BaseError { 42 | constructor(name: string) { 43 | super(name, StatusCode.BAD_REQUEST, true, 'Bad Request'); 44 | } 45 | } 46 | 47 | export class Api404Error extends BaseError { 48 | constructor(name: string) { 49 | super(name, StatusCode.NOT_FOUND, true, 'Not Found'); 50 | } 51 | } 52 | 53 | export class Api401Error extends BaseError { 54 | constructor(name: string) { 55 | super(name, StatusCode.UNAUTHORIZED, true, 'Unauthorized'); 56 | } 57 | } 58 | 59 | export class Api403Error extends BaseError { 60 | constructor(name: string) { 61 | super(name, StatusCode.FORBIDDEN, true, 'Forbidden'); 62 | } 63 | } 64 | 65 | export class Api409Error extends BaseError { 66 | constructor(name: string) { 67 | super(name, StatusCode.CONFLICT, true, 'Conflict'); 68 | } 69 | } 70 | 71 | export const isOperationalError = (error: Error) => { 72 | if (error instanceof BaseError) { 73 | return error.isOperational; 74 | } 75 | return false; 76 | }; 77 | -------------------------------------------------------------------------------- /packages/api/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { getWinstonTransport } from '@hyperdx/node-opentelemetry'; 2 | import expressWinston from 'express-winston'; 3 | import winston, { addColors } from 'winston'; 4 | 5 | import { 6 | APP_TYPE, 7 | HYPERDX_API_KEY, 8 | HYPERDX_LOG_LEVEL, 9 | IS_PROD, 10 | } from '@/config'; 11 | 12 | // LOCAL DEV ONLY 13 | addColors({ 14 | error: 'bold red', 15 | warn: 'bold yellow', 16 | info: 'white', 17 | http: 'gray', 18 | verbose: 'bold magenta', 19 | debug: 'green', 20 | silly: 'cyan', 21 | }); 22 | 23 | const MAX_LEVEL = HYPERDX_LOG_LEVEL ?? 'debug'; 24 | const DEFAULT_FORMAT = winston.format.combine( 25 | winston.format.errors({ stack: true }), 26 | winston.format.json(), 27 | ); 28 | 29 | const hyperdxTransport = HYPERDX_API_KEY 30 | ? getWinstonTransport(MAX_LEVEL, { 31 | bufferSize: APP_TYPE === 'scheduled-task' ? 1 : 100, 32 | }) 33 | : null; 34 | 35 | export const expressLogger = expressWinston.logger({ 36 | level: MAX_LEVEL, 37 | format: DEFAULT_FORMAT, 38 | msg: IS_PROD 39 | ? undefined 40 | : 'HTTP {{res.statusCode}} {{req.method}} {{req.url}} {{res.responseTime}}ms', 41 | transports: [ 42 | new winston.transports.Console(), 43 | ...(hyperdxTransport ? [hyperdxTransport] : []), 44 | ], 45 | meta: IS_PROD, 46 | }); 47 | 48 | const logger = winston.createLogger({ 49 | level: MAX_LEVEL, 50 | format: DEFAULT_FORMAT, 51 | transports: [ 52 | new winston.transports.Console(), 53 | ...(hyperdxTransport ? [hyperdxTransport] : []), 54 | ], 55 | }); 56 | 57 | export default logger; 58 | -------------------------------------------------------------------------------- /packages/api/src/utils/miner.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import ms from 'ms'; 3 | 4 | import * as config from '@/config'; 5 | 6 | import logger from './logger'; 7 | 8 | const MAX_LOG_LINES = 1e4; 9 | 10 | export const getLogsPatterns = async ( 11 | teamId: string, 12 | lines: string[][], 13 | ): Promise<{ 14 | patterns: Record; 15 | result: Record; 16 | }> => { 17 | if (lines.length > MAX_LOG_LINES) { 18 | logger.error(`Too many log lines requested: ${lines.length}`); 19 | } 20 | 21 | return axios({ 22 | method: 'POST', 23 | url: `${config.MINER_API_URL}/logs`, 24 | data: { 25 | team_id: teamId, 26 | lines: lines.slice(0, MAX_LOG_LINES), 27 | }, 28 | maxContentLength: Infinity, 29 | maxBodyLength: Infinity, 30 | timeout: ms('2 minute'), 31 | }).then(response => response.data); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/api/src/utils/passport.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import { Strategy as LocalStrategy } from 'passport-local'; 3 | 4 | import { findUserById } from '@/controllers/user'; 5 | import type { UserDocument } from '@/models/user'; 6 | import User from '@/models/user'; 7 | 8 | import logger from './logger'; 9 | 10 | passport.serializeUser(function (user, done) { 11 | done(null, (user as any)._id); 12 | }); 13 | 14 | passport.deserializeUser(function (id: string, done) { 15 | findUserById(id) 16 | .then(user => { 17 | if (user == null) { 18 | return done(new Error('User not found')); 19 | } 20 | done(null, user as UserDocument); 21 | }) 22 | .catch(done); 23 | }); 24 | 25 | // Use local passport strategy via passport-local-mongoose plugin 26 | const passportLocalMongooseAuthenticate = (User as any).authenticate(); 27 | 28 | passport.use( 29 | new LocalStrategy( 30 | { 31 | usernameField: 'email', 32 | }, 33 | async function (username, password, done) { 34 | try { 35 | const { user, error } = await passportLocalMongooseAuthenticate( 36 | username, 37 | password, 38 | ); 39 | if (error) { 40 | logger.info({ 41 | message: `Login for "${username}" failed, ${error}"`, 42 | type: 'user_login', 43 | authType: 'password', 44 | }); 45 | } 46 | return done(null, user, error); 47 | } catch (err) { 48 | logger.error(`Login for "${username}" failed, error: ${err}"`); 49 | return done(err); 50 | } 51 | }, 52 | ), 53 | ); 54 | 55 | export default passport; 56 | -------------------------------------------------------------------------------- /packages/api/src/utils/queue.ts: -------------------------------------------------------------------------------- 1 | export class LimitedSizeQueue { 2 | private readonly _limit: number; 3 | 4 | private readonly _queue: T[]; 5 | 6 | constructor(limit: number) { 7 | this._limit = limit; 8 | this._queue = []; 9 | } 10 | 11 | enqueue(item: T) { 12 | this._queue.push(item); 13 | if (this._queue.length === this._limit + 1) { 14 | // randomly remove an item 15 | this._queue.splice(Math.floor(Math.random() * this._limit), 1); 16 | } 17 | } 18 | 19 | toArray() { 20 | return this._queue; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/api/src/utils/rateLimiter.ts: -------------------------------------------------------------------------------- 1 | import rateLimit, { Options } from 'express-rate-limit'; 2 | 3 | export default (config?: Partial) => { 4 | return rateLimit({ 5 | ...config, 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/api/src/utils/slack.ts: -------------------------------------------------------------------------------- 1 | import { IncomingWebhook, IncomingWebhookSendArguments } from '@slack/webhook'; 2 | 3 | export function postMessageToWebhook( 4 | webhookUrl: string, 5 | message: IncomingWebhookSendArguments, 6 | ) { 7 | const webhook = new IncomingWebhook(webhookUrl); 8 | return webhook.send({ 9 | text: message.text, 10 | blocks: message.blocks, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /packages/api/src/utils/swagger.ts: -------------------------------------------------------------------------------- 1 | import { Application, Express } from 'express'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import swaggerJsdoc from 'swagger-jsdoc'; 5 | import swaggerUi from 'swagger-ui-express'; 6 | 7 | export const swaggerOptions = { 8 | definition: { 9 | openapi: '3.0.0', 10 | info: { 11 | title: 'HyperDX External API', 12 | description: 'API for managing HyperDX alerts and dashboards', 13 | version: '2.0.0', 14 | }, 15 | servers: [ 16 | { 17 | url: 'https://api.hyperdx.io', 18 | description: 'Production API server', 19 | }, 20 | { 21 | url: '/', 22 | description: 'Current server', 23 | }, 24 | ], 25 | tags: [ 26 | { 27 | name: 'Dashboards', 28 | description: 29 | 'Endpoints for managing dashboards and their visualizations', 30 | }, 31 | { 32 | name: 'Alerts', 33 | description: 'Endpoints for managing monitoring alerts', 34 | }, 35 | ], 36 | components: { 37 | securitySchemes: { 38 | BearerAuth: { 39 | type: 'http', 40 | scheme: 'bearer', 41 | bearerFormat: 'API Key', 42 | }, 43 | }, 44 | }, 45 | security: [ 46 | { 47 | BearerAuth: [], 48 | }, 49 | ], 50 | }, 51 | apis: ['./src/routers/external-api/**/*.ts'], // Path to the API routes files 52 | }; 53 | 54 | export function setupSwagger(app: Application) { 55 | const specs = swaggerJsdoc(swaggerOptions); 56 | 57 | // Serve swagger docs 58 | app.use('/api/v2/docs', swaggerUi.serve, swaggerUi.setup(specs)); 59 | 60 | // Serve OpenAPI spec as JSON (needed for ReDoc) 61 | app.get('/api/v2/docs.json', (req, res) => { 62 | res.setHeader('Content-Type', 'application/json'); 63 | res.send(specs); 64 | }); 65 | 66 | // Optionally save the spec to a file 67 | const outputPath = path.resolve(__dirname, '../../../public/openapi.json'); 68 | fs.mkdirSync(path.dirname(outputPath), { recursive: true }); 69 | fs.writeFileSync(outputPath, JSON.stringify(specs, null, 2)); 70 | } 71 | -------------------------------------------------------------------------------- /packages/api/src/utils/validators.ts: -------------------------------------------------------------------------------- 1 | export const validatePassword = (password: string) => { 2 | if (!password || password.length < 8 || password.length > 64) { 3 | return false; 4 | } 5 | return true; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "paths": { 5 | "@/*": ["./*"] 6 | }, 7 | "allowSyntheticDefaultImports": true, 8 | "downlevelIteration": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "importHelpers": true, 12 | "lib": ["ES2022", "dom"], 13 | "module": "Node16", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": false, 17 | "noImplicitReturns": false, 18 | "noImplicitThis": false, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "outDir": "build", 22 | "skipLibCheck": false, 23 | "sourceMap": true, 24 | "strict": true, 25 | "target": "ES2022" 26 | }, 27 | "include": ["src", "migrations", "scripts"], 28 | "exclude": ["node_modules", "**/*.test.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /packages/api/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/app/.Dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/.next -------------------------------------------------------------------------------- /packages/app/.env.development: -------------------------------------------------------------------------------- 1 | HYPERDX_API_KEY="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 2 | HYPERDX_API_PORT=8000 3 | HYPERDX_APP_PORT=8080 4 | NEXT_PUBLIC_SERVER_URL="http://localhost:${HYPERDX_API_PORT}" 5 | NODE_ENV=development 6 | OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" 7 | OTEL_SERVICE_NAME="hdx-oss-dev-app" 8 | PORT=${HYPERDX_APP_PORT} 9 | NODE_OPTIONS="--max-http-header-size=131072" 10 | -------------------------------------------------------------------------------- /packages/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['simple-import-sort', '@typescript-eslint', 'prettier'], 5 | parserOptions: { 6 | tsconfigRootDir: __dirname, 7 | project: ['./tsconfig.json', './tsconfig.test.json'], 8 | }, 9 | extends: [ 10 | 'next', 11 | 'eslint:recommended', 12 | 'plugin:@typescript-eslint/eslint-recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'plugin:prettier/recommended', 15 | ], 16 | rules: { 17 | '@typescript-eslint/ban-ts-comment': 'warn', 18 | '@typescript-eslint/no-empty-function': 'warn', 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | '@typescript-eslint/no-unsafe-function-type': 'warn', 21 | '@typescript-eslint/no-unused-expressions': 'warn', 22 | '@typescript-eslint/no-unused-vars': 'warn', 23 | 'react/display-name': 'off', 24 | 'simple-import-sort/exports': 'error', 25 | 'simple-import-sort/imports': 'error', 26 | 'no-console': ['error', { allow: ['warn', 'error'] }], 27 | }, 28 | overrides: [ 29 | { 30 | files: ['**/*.js', '**/*.ts', '**/*.tsx'], 31 | rules: { 32 | 'simple-import-sort/imports': [ 33 | 'error', 34 | { 35 | groups: [ 36 | ['^react$', '^next', '^[a-z]', '^@'], 37 | ['^@/'], 38 | ['^\\.\\.(?!/?$)', '^\\.\\./?$'], 39 | ['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], 40 | ['^.+\\.s?css$'], 41 | ['^\\u0000'], 42 | ], 43 | }, 44 | ], 45 | }, 46 | }, 47 | ], 48 | }; 49 | -------------------------------------------------------------------------------- /packages/app/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Sentry 3 | .sentryclirc 4 | 5 | # Sentry 6 | next.config.original.js 7 | 8 | public/__ENV.js 9 | -------------------------------------------------------------------------------- /packages/app/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'path'; 2 | import type { StorybookConfig } from '@storybook/nextjs'; 3 | 4 | function getAbsolutePath(value: string): any { 5 | return dirname(require.resolve(join(value, 'package.json'))); 6 | } 7 | 8 | const config: StorybookConfig = { 9 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 10 | addons: [ 11 | getAbsolutePath('@storybook/addon-links'), 12 | getAbsolutePath('@storybook/addon-essentials'), 13 | getAbsolutePath('@storybook/addon-interactions'), 14 | getAbsolutePath('@storybook/addon-styling-webpack'), 15 | ], 16 | framework: { 17 | name: getAbsolutePath('@storybook/nextjs'), 18 | options: {}, 19 | }, 20 | staticDirs: ['./public'], 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /packages/app/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 5 | 10 | 14 | -------------------------------------------------------------------------------- /packages/app/.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Preview } from '@storybook/react'; 3 | import { initialize, mswLoader } from 'msw-storybook-addon'; 4 | import { QueryClient, QueryClientProvider } from 'react-query'; 5 | import { QueryParamProvider } from 'use-query-params'; 6 | import { NextAdapter } from 'next-query-params'; 7 | 8 | import '@mantine/core/styles.css'; 9 | import '@mantine/notifications/styles.css'; 10 | import '@mantine/dates/styles.css'; 11 | 12 | import '../styles/globals.css'; 13 | import '../styles/app.scss'; 14 | 15 | import { meHandler } from '../src/mocks/handlers'; 16 | import { ThemeWrapper } from '../src/ThemeWrapper'; 17 | 18 | initialize(); 19 | 20 | const queryClient = new QueryClient(); 21 | 22 | const preview: Preview = { 23 | decorators: [ 24 | Story => ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ), 33 | ], 34 | loaders: [mswLoader], 35 | parameters: { 36 | msw: { 37 | handlers: [meHandler], 38 | }, 39 | }, 40 | }; 41 | 42 | export default preview; 43 | -------------------------------------------------------------------------------- /packages/app/.stylelintignore: -------------------------------------------------------------------------------- 1 | .next 2 | .storybook 3 | node_modules 4 | coverage -------------------------------------------------------------------------------- /packages/app/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["stylelint-prettier"], 3 | "extends": ["stylelint-config-standard-scss"], 4 | "rules": { 5 | "prettier/prettier": true, 6 | "selector-class-pattern": null, 7 | "no-descending-specificity": null, 8 | "scss/at-extend-no-missing-placeholder": [ 9 | true, 10 | { 11 | "severity": "warning" 12 | } 13 | ], 14 | "scss/dollar-variable-pattern": null 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/app/global-setup.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | process.env.TZ = 'America/New_York'; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-ts', 3 | testEnvironment: 'jsdom', 4 | globalSetup: '/global-setup.js', 5 | roots: ['/src'], 6 | transform: { 7 | '^.+\\.(ts|tsx)$': 'ts-jest', 8 | }, 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 10 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 11 | transformIgnorePatterns: ['/node_modules/(?!(ky|ky-universal))'], 12 | moduleNameMapper: { 13 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy', 14 | '^@/(.*)$': '/src/$1', 15 | }, 16 | setupFilesAfterEnv: ['/src/setupTests.tsx'], 17 | // Prettier 3 not supported yet 18 | // See: https://stackoverflow.com/a/76818962 19 | prettierPath: null, 20 | globals: { 21 | // This is necessary because next.js forces { "jsx": "preserve" }, but ts-jest appears to require { "jsx": "react-jsx" } 22 | 'ts-jest': { 23 | tsconfig: { 24 | jsx: 'react-jsx', 25 | }, 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/app/knip.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": ["pages/**/*.tsx"], 3 | "ignore": [".next/**"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/app/mdx.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.mdx' { 2 | export default any; 3 | export const meta: any; 4 | } 5 | -------------------------------------------------------------------------------- /packages/app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/app/next.config.js: -------------------------------------------------------------------------------- 1 | const { configureRuntimeEnv } = require('next-runtime-env/build/configure'); 2 | const { version } = require('./package.json'); 3 | 4 | configureRuntimeEnv(); 5 | 6 | const withNextra = require('nextra')({ 7 | theme: 'nextra-theme-docs', 8 | themeConfig: './src/nextra.config.tsx', 9 | }); 10 | 11 | module.exports = { 12 | experimental: { 13 | instrumentationHook: true, 14 | }, 15 | // Ignore otel pkgs warnings 16 | // https://github.com/open-telemetry/opentelemetry-js/issues/4173#issuecomment-1822938936 17 | webpack: ( 18 | config, 19 | { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack }, 20 | ) => { 21 | if (isServer) { 22 | config.ignoreWarnings = [{ module: /opentelemetry/ }]; 23 | } 24 | return config; 25 | }, 26 | ...withNextra({ 27 | async headers() { 28 | return [ 29 | { 30 | source: '/(.*)?', // Matches all pages 31 | headers: [ 32 | { 33 | key: 'X-Frame-Options', 34 | value: 'DENY', 35 | }, 36 | ], 37 | }, 38 | ]; 39 | }, 40 | // This slows down builds by 2x for some reason... 41 | swcMinify: false, 42 | publicRuntimeConfig: { 43 | version, 44 | }, 45 | productionBrowserSourceMaps: false, 46 | ...(process.env.NEXT_OUTPUT_STANDALONE === 'true' 47 | ? { 48 | output: 'standalone', 49 | } 50 | : {}), 51 | }), 52 | }; 53 | -------------------------------------------------------------------------------- /packages/app/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/app/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: This requires `@sentry/nextjs` version 7.3.0 or higher. 3 | * 4 | * NOTE: If using this with `next` version 12.2.0 or lower, uncomment the 5 | * penultimate line in `CustomErrorComponent`. 6 | * 7 | * This page is loaded by Nextjs: 8 | * - on the server, when data-fetching methods throw or reject 9 | * - on the client, when `getInitialProps` throws or rejects 10 | * - on the client, when a React lifecycle method throws or rejects, and it's 11 | * caught by the built-in Nextjs error boundary 12 | * 13 | * See: 14 | * - https://nextjs.org/docs/basic-features/data-fetching/overview 15 | * - https://nextjs.org/docs/api-reference/data-fetching/get-initial-props 16 | * - https://reactjs.org/docs/error-boundaries.html 17 | */ 18 | 19 | import NextErrorComponent from 'next/error'; 20 | 21 | const CustomErrorComponent = (props: any) => { 22 | // If you're using a Nextjs version prior to 12.2.1, uncomment this to 23 | // compensate for https://github.com/vercel/next.js/issues/8592 24 | // Sentry.captureUnderscoreErrorException(props); 25 | 26 | return ; 27 | }; 28 | 29 | CustomErrorComponent.getInitialProps = async (contextData: any) => { 30 | // This will contain the status code of the response 31 | return NextErrorComponent.getInitialProps(contextData); 32 | }; 33 | 34 | export default CustomErrorComponent; 35 | -------------------------------------------------------------------------------- /packages/app/pages/alerts.tsx: -------------------------------------------------------------------------------- 1 | import AlertsPage from '@/AlertsPage'; 2 | 3 | export default AlertsPage; 4 | -------------------------------------------------------------------------------- /packages/app/pages/api/[...all].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware'; 3 | 4 | const DEFAULT_SERVER_URL = `http://127.0.0.1:${process.env.HYPERDX_API_PORT}`; 5 | 6 | export const config = { 7 | api: { 8 | externalResolver: true, 9 | bodyParser: false, 10 | }, 11 | }; 12 | 13 | export default (req: NextApiRequest, res: NextApiResponse) => { 14 | const proxy = createProxyMiddleware({ 15 | changeOrigin: true, 16 | // logger: console, // DEBUG 17 | pathRewrite: { '^/api': '' }, 18 | target: process.env.NEXT_PUBLIC_SERVER_URL || DEFAULT_SERVER_URL, 19 | autoRewrite: true, 20 | // ...(IS_DEV && { 21 | // logger: console, 22 | // }), 23 | }); 24 | return proxy(req, res, error => { 25 | if (error) { 26 | console.error(error); 27 | res.status(500).send('API proxy error'); 28 | return; 29 | } 30 | res.status(404).send('Not found'); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/app/pages/api/config.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | import { HDX_API_KEY, HDX_COLLECTOR_URL, HDX_SERVICE_NAME } from '@/config'; 4 | import type { NextApiConfigResponseData } from '@/types'; 5 | 6 | export default function handler( 7 | req: NextApiRequest, 8 | res: NextApiResponse, 9 | ) { 10 | res.status(200).json({ 11 | apiKey: HDX_API_KEY, 12 | collectorUrl: HDX_COLLECTOR_URL, 13 | serviceName: HDX_SERVICE_NAME, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/pages/benchmark.tsx: -------------------------------------------------------------------------------- 1 | import BenchmarkPage from '@/BenchmarkPage'; 2 | 3 | export default BenchmarkPage; 4 | -------------------------------------------------------------------------------- /packages/app/pages/chart.tsx: -------------------------------------------------------------------------------- 1 | import ChartPage from '@/DBChartPage'; 2 | 3 | export default ChartPage; 4 | -------------------------------------------------------------------------------- /packages/app/pages/clickhouse.tsx: -------------------------------------------------------------------------------- 1 | import ClickhousePage from '@/ClickhousePage'; 2 | export default ClickhousePage; 3 | -------------------------------------------------------------------------------- /packages/app/pages/dashboards/[dashboardId].tsx: -------------------------------------------------------------------------------- 1 | import DashboardPage from '@/DBDashboardPage'; 2 | 3 | export default DashboardPage; 4 | -------------------------------------------------------------------------------- /packages/app/pages/dashboards/index.tsx: -------------------------------------------------------------------------------- 1 | import DBDashboardPage from '@/DBDashboardPage'; 2 | export default DBDashboardPage; 3 | -------------------------------------------------------------------------------- /packages/app/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import LandingPage from '@/LandingPage'; 2 | export default LandingPage; 3 | -------------------------------------------------------------------------------- /packages/app/pages/join-team.tsx: -------------------------------------------------------------------------------- 1 | import JoinTeamPage from '@/JoinTeamPage'; 2 | 3 | export default JoinTeamPage; 4 | -------------------------------------------------------------------------------- /packages/app/pages/kubernetes.tsx: -------------------------------------------------------------------------------- 1 | import KubernetesDashboardPage from '@/KubernetesDashboardPage'; 2 | 3 | export default KubernetesDashboardPage; 4 | -------------------------------------------------------------------------------- /packages/app/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import AuthPage from '@/AuthPage'; 2 | 3 | export default function Login() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /packages/app/pages/register.tsx: -------------------------------------------------------------------------------- 1 | import AuthPage from '@/AuthPage'; 2 | export default function Register() { 3 | return ( 4 |
5 | 6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /packages/app/pages/search/[savedSearchId].tsx: -------------------------------------------------------------------------------- 1 | import SearchPage from '@/DBSearchPage'; 2 | 3 | export default SearchPage; 4 | -------------------------------------------------------------------------------- /packages/app/pages/search/index.tsx: -------------------------------------------------------------------------------- 1 | import SearchPage from '@/DBSearchPage'; 2 | 3 | export default SearchPage; 4 | -------------------------------------------------------------------------------- /packages/app/pages/services.tsx: -------------------------------------------------------------------------------- 1 | import ServicesDashboardPage from '@/ServicesDashboardPage'; 2 | export default ServicesDashboardPage; 3 | -------------------------------------------------------------------------------- /packages/app/pages/sessions.tsx: -------------------------------------------------------------------------------- 1 | import SessionsPage from '@/SessionsPage'; 2 | 3 | export default SessionsPage; 4 | -------------------------------------------------------------------------------- /packages/app/pages/team/index.tsx: -------------------------------------------------------------------------------- 1 | import TeamPage from '@/TeamPage'; 2 | export default TeamPage; 3 | -------------------------------------------------------------------------------- /packages/app/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-preset-mantine': {}, 4 | 'postcss-simple-vars': { 5 | variables: { 6 | 'mantine-breakpoint-xs': '36em', 7 | 'mantine-breakpoint-sm': '48em', 8 | 'mantine-breakpoint-md': '62em', 9 | 'mantine-breakpoint-lg': '75em', 10 | 'mantine-breakpoint-xl': '88em', 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/app/public/Icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/packages/app/public/Icon32.png -------------------------------------------------------------------------------- /packages/app/public/drain3-0.9.11-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperdxio/hyperdx/afe4c2da9ca35f187e655b378858c6dad7150300/packages/app/public/drain3-0.9.11-py3-none-any.whl -------------------------------------------------------------------------------- /packages/app/src/AuthLoadingBlocker.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | import api from './api'; 4 | 5 | export default function AuthLoadingBlocker() { 6 | const { data: meData } = api.useMe(); 7 | 8 | // increase number of periods rendered as a loading animation, 1 period per second 9 | const [periods, setPeriods] = useState(0); 10 | useEffect(() => { 11 | const interval = setInterval(() => { 12 | setPeriods(periods => (periods + 1) % 4); 13 | }, 1000); 14 | return () => clearInterval(interval); 15 | }, []); 16 | 17 | return ( 18 | <> 19 | {meData == null && ( 20 |
26 | Loading HyperDX 27 | {'.'.repeat(periods)} 28 |
29 | )} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/app/src/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | export default function Checkbox({ 2 | id, 3 | className, 4 | labelClassName, 5 | checked, 6 | onChange, 7 | label, 8 | disabled, 9 | title, 10 | }: { 11 | id: string; 12 | className?: string; 13 | labelClassName?: string; 14 | checked: boolean; 15 | onChange: () => void; 16 | label: React.ReactNode; 17 | disabled?: boolean; 18 | title?: string; 19 | }) { 20 | return ( 21 | 22 | 30 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/app/src/Clipboard.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import cx from 'classnames'; 3 | import { Button } from 'react-bootstrap'; 4 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 5 | 6 | export default function Clipboard({ 7 | text, 8 | className, 9 | children, 10 | }: { 11 | text: string; 12 | className?: string; 13 | children: ({ isCopied }: { isCopied: boolean }) => React.ReactNode; 14 | }) { 15 | const [isCopied, setIsCopied] = useState(false); 16 | 17 | return ( 18 | { 21 | setIsCopied(true); 22 | setTimeout(() => setIsCopied(false), 2000); 23 | }} 24 | > 25 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/app/src/DBChartPage.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import dynamic from 'next/dynamic'; 3 | import { parseAsJson, parseAsStringEnum, useQueryState } from 'nuqs'; 4 | import { SavedChartConfig } from '@hyperdx/common-utils/dist/types'; 5 | import { Box } from '@mantine/core'; 6 | 7 | import { DEFAULT_CHART_CONFIG, Granularity } from '@/ChartUtils'; 8 | import EditTimeChartForm from '@/components/DBEditTimeChartForm'; 9 | import { withAppNav } from '@/layout'; 10 | import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery'; 11 | 12 | import { useSources } from './source'; 13 | 14 | // Autocomplete can focus on column/map keys 15 | 16 | // Sampled field discovery and full field discovery 17 | 18 | // TODO: This is a hack to set the default time range 19 | const defaultTimeRange = parseTimeQuery('Past 1h', false) as [Date, Date]; 20 | 21 | function DBChartExplorerPage() { 22 | const { 23 | searchedTimeRange, 24 | displayedTimeInputValue, 25 | setDisplayedTimeInputValue, 26 | onSearch, 27 | onTimeRangeSelect, 28 | } = useNewTimeQuery({ 29 | initialDisplayValue: 'Past 1h', 30 | initialTimeRange: defaultTimeRange, 31 | // showRelativeInterval: isLive, 32 | }); 33 | 34 | const { data: sources } = useSources(); 35 | 36 | const [chartConfig, setChartConfig] = useQueryState( 37 | 'config', 38 | parseAsJson().withDefault({ 39 | ...DEFAULT_CHART_CONFIG, 40 | source: sources?.[0]?.id ?? '', 41 | }), 42 | ); 43 | 44 | return ( 45 | 46 | { 49 | setChartConfig(config); 50 | }} 51 | dateRange={searchedTimeRange} 52 | setDisplayedTimeInputValue={setDisplayedTimeInputValue} 53 | displayedTimeInputValue={displayedTimeInputValue} 54 | onTimeRangeSearch={onSearch} 55 | onTimeRangeSelect={onTimeRangeSelect} 56 | /> 57 | 58 | ); 59 | } 60 | 61 | const DBChartExplorerPageDynamic = dynamic(async () => DBChartExplorerPage, { 62 | ssr: false, 63 | }); 64 | 65 | // @ts-ignore 66 | DBChartExplorerPageDynamic.getLayout = withAppNav; 67 | 68 | export default DBChartExplorerPageDynamic; 69 | -------------------------------------------------------------------------------- /packages/app/src/DSSelect.tsx: -------------------------------------------------------------------------------- 1 | import Select from 'react-select'; 2 | 3 | export default function DSSelect< 4 | Option extends { value: string | undefined; label: React.ReactNode }, 5 | >({ 6 | options, 7 | value, 8 | onChange, 9 | disabled, 10 | }: { 11 | options: Option[]; 12 | disabled?: boolean; 13 | value: string | undefined; 14 | onChange: (value: Option['value'] | undefined) => void; 15 | }) { 16 | return ( 17 | } 42 | maxDropdownHeight={280} 43 | data={data} 44 | disabled={isTablesLoading} 45 | value={table} 46 | comboboxProps={{ withinPortal: false }} 47 | onChange={v => setTable(v ?? undefined)} 48 | onBlur={onBlur} 49 | name={name} 50 | ref={inputRef} 51 | size={size} 52 | /> 53 | ); 54 | } 55 | 56 | export function DBTableSelectControlled({ 57 | database, 58 | connectionId, 59 | ...props 60 | }: { 61 | database?: string; 62 | size?: string; 63 | connectionId: string | undefined; 64 | } & UseControllerProps) { 65 | const { 66 | field, 67 | fieldState: { invalid, isTouched, isDirty }, 68 | formState: { touchedFields, dirtyFields }, 69 | } = useController(props); 70 | 71 | return ( 72 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /packages/app/src/components/DatabaseSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useController, UseControllerProps } from 'react-hook-form'; 2 | import { Select } from '@mantine/core'; 3 | 4 | import { useDatabasesDirect } from '@/clickhouse'; 5 | 6 | type DatabaseSelectProps = { 7 | database: string | undefined; 8 | setDatabase: (db: string | undefined) => void; 9 | onBlur?: () => void; 10 | inputRef?: React.Ref; 11 | name?: string; 12 | size?: string; 13 | connectionId: string | undefined; 14 | }; 15 | 16 | export default function DatabaseSelect({ 17 | database, 18 | setDatabase, 19 | connectionId, 20 | onBlur, 21 | name, 22 | inputRef, 23 | size, 24 | }: DatabaseSelectProps) { 25 | const { data: databases, isLoading: isDatabasesLoading } = useDatabasesDirect( 26 | { connectionId: connectionId ?? '' }, 27 | { enabled: !!connectionId }, 28 | ); 29 | 30 | const data = (databases?.data || []).map((db: { name: string }) => ({ 31 | value: db.name, 32 | label: db.name, 33 | })); 34 | 35 | return ( 36 | 26 | )} 27 | /> 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/app/src/components/InputLanguageSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from '@mantine/core'; 2 | 3 | export default function InputLanguageSwitch({ 4 | language, 5 | onLanguageChange, 6 | showHotkey, 7 | }: { 8 | language: 'sql' | 'lucene'; 9 | onLanguageChange: (language: 'sql' | 'lucene') => void; 10 | showHotkey?: boolean; 11 | }) { 12 | return ( 13 | 14 | {showHotkey && ( 15 | 16 | / 17 | 18 | )} 19 | onLanguageChange('sql')} 22 | size="xs" 23 | role="button" 24 | > 25 | SQL 26 | 27 | 28 | | 29 | 30 | onLanguageChange('lucene')} 35 | > 36 | Lucene 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/app/src/components/LogLevel.tsx: -------------------------------------------------------------------------------- 1 | import { Text, TextProps } from '@mantine/core'; 2 | 3 | import { getLogLevelClass } from '@/utils'; 4 | 5 | export default function LogLevel({ 6 | level, 7 | ...props 8 | }: { level: string } & TextProps) { 9 | const levelClass = getLogLevelClass(level); 10 | 11 | return ( 12 | 23 | {level} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/app/src/components/PageHeader.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables'; 2 | 3 | .header { 4 | align-items: center; 5 | background-color: $body-bg; 6 | border-bottom: 1px solid $slate-950; 7 | color: $slate-200; 8 | display: flex; 9 | font-weight: 500; 10 | height: 60px; 11 | justify-content: space-between; 12 | line-height: 1; 13 | padding: 0 32px; 14 | position: sticky; 15 | top: 0; 16 | z-index: 100; 17 | } 18 | -------------------------------------------------------------------------------- /packages/app/src/components/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import styles from './PageHeader.module.scss'; 2 | 3 | export const PageHeader = ({ children }: { children: React.ReactNode }) => { 4 | return
{children}
; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/app/src/components/SearchPageActionBar.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Menu, Text } from '@mantine/core'; 2 | 3 | export default function SearchPageActionBar({ 4 | onClickDeleteSavedSearch, 5 | onClickRenameSavedSearch, 6 | }: { 7 | onClickDeleteSavedSearch: () => void; 8 | onClickRenameSavedSearch: () => void; 9 | }) { 10 | return ( 11 | 12 | 13 | 22 | 23 | 24 | 25 | } 27 | onClick={onClickDeleteSavedSearch} 28 | > 29 | Delete Saved Search 30 | 31 | } 33 | onClick={onClickRenameSavedSearch} 34 | > 35 | Rename Saved Search 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/app/src/components/SearchTotalCountChart.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; 3 | import { Text } from '@mantine/core'; 4 | import { keepPreviousData } from '@tanstack/react-query'; 5 | 6 | import { useTimeChartSettings } from '@/ChartUtils'; 7 | import { useQueriedChartConfig } from '@/hooks/useChartConfig'; 8 | 9 | export function useSearchTotalCount( 10 | config: ChartConfigWithDateRange, 11 | queryKeyPrefix: string, 12 | ) { 13 | // copied from DBTimeChart 14 | const { granularity } = useTimeChartSettings(config); 15 | const queriedConfig = { 16 | ...config, 17 | granularity, 18 | limit: { limit: 100000 }, 19 | }; 20 | const { 21 | data: totalCountData, 22 | isLoading, 23 | isError, 24 | } = useQueriedChartConfig(queriedConfig, { 25 | queryKey: [queryKeyPrefix, queriedConfig], 26 | staleTime: 1000 * 60 * 5, 27 | refetchOnWindowFocus: false, 28 | placeholderData: keepPreviousData, // no need to flash loading state when in live tail 29 | }); 30 | 31 | const totalCount = useMemo(() => { 32 | return totalCountData?.data?.reduce( 33 | (p: number, v: any) => p + Number.parseInt(v['count()']), 34 | 0, 35 | ); 36 | }, [totalCountData]); 37 | 38 | return { 39 | totalCount, 40 | isLoading, 41 | isError, 42 | }; 43 | } 44 | 45 | export default function SearchTotalCountChart({ 46 | config, 47 | queryKeyPrefix, 48 | }: { 49 | config: ChartConfigWithDateRange; 50 | queryKeyPrefix: string; 51 | }) { 52 | const { totalCount, isLoading, isError } = useSearchTotalCount( 53 | config, 54 | queryKeyPrefix, 55 | ); 56 | 57 | return ( 58 | 59 | {isLoading ? ( 60 | ··· Results 61 | ) : totalCount !== null && !isError ? ( 62 | `${totalCount} Results` 63 | ) : ( 64 | '0 Results' 65 | )} 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /packages/app/src/components/SelectControlled.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useController, UseControllerProps } from 'react-hook-form'; 3 | import { Select, SelectProps } from '@mantine/core'; 4 | 5 | export type SelectControlledProps = SelectProps & 6 | UseControllerProps & { 7 | onCreate?: () => void; 8 | allowDeselect?: boolean; 9 | }; 10 | 11 | export default function SelectControlled(props: SelectControlledProps) { 12 | const { field, fieldState } = useController(props); 13 | const { onCreate, allowDeselect = true, ...restProps } = props; 14 | 15 | // This is needed as mantine does not clear the select 16 | // if the value is not in the data after 17 | // if it was previously in the data (ex. data was deleted) 18 | const selected = props.data?.find(d => 19 | typeof d === 'string' 20 | ? d === field.value 21 | : 'value' in d 22 | ? d.value === field.value 23 | : true, 24 | ); 25 | 26 | const onChange = useCallback( 27 | (value: string | null) => { 28 | if (value === '_create_new_value' && onCreate != null) { 29 | onCreate(); 30 | } else if (value !== null || allowDeselect) { 31 | field.onChange(value); 32 | } 33 | }, 34 | [field, onCreate, allowDeselect], 35 | ); 36 | 37 | return ( 38 |