diff --git a/.changeset/gentle-lamps-drop.md b/.changeset/gentle-lamps-drop.md new file mode 100644 index 000000000..4c663efa2 --- /dev/null +++ b/.changeset/gentle-lamps-drop.md @@ -0,0 +1,6 @@ +--- +'@hyperdx/api': patch +'@hyperdx/app': patch +--- + +feat: Allow to update team name diff --git a/.changeset/wise-wombats-hunt.md b/.changeset/wise-wombats-hunt.md new file mode 100644 index 000000000..7e3646ee0 --- /dev/null +++ b/.changeset/wise-wombats-hunt.md @@ -0,0 +1,6 @@ +--- +'@hyperdx/app': patch +'@hyperdx/api': patch +--- + +feat: Allow to set table headers and chart line names diff --git a/.env b/.env index 865453cb1..63e296832 100644 --- a/.env +++ b/.env @@ -1,8 +1,9 @@ # Used by docker-compose.yml IMAGE_NAME=ghcr.io/hyperdxio/hyperdx +IMAGE_NAME_HDX=ghcr.hyperdx.io/hyperdxio/hyperdx LOCAL_IMAGE_NAME=ghcr.io/hyperdxio/hyperdx-local LOCAL_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-local -IMAGE_VERSION=1.10.0 +IMAGE_VERSION=1.10.1 # Set up domain URLs HYPERDX_API_PORT=8000 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7069314ca..197f5dbb9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ concurrency: jobs: lint: timeout-minutes: 8 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v2 @@ -25,14 +25,14 @@ jobs: - name: Install vector run: | mkdir -p vector - curl -sSfL --proto '=https' --tlsv1.2 https://packages.timber.io/vector/0.41.1/vector-0.41.1-x86_64-unknown-linux-musl.tar.gz | tar xzf - -C vector --strip-components=2 + curl -sSfL --proto '=https' --tlsv1.2 https://packages.timber.io/vector/0.43.1/vector-0.43.1-x86_64-unknown-linux-musl.tar.gz | tar xzf - -C vector --strip-components=2 cp ./vector/bin/vector /usr/local/bin/vector vector --version - name: Run lint + type check run: make ci-lint unit: timeout-minutes: 8 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v2 @@ -46,7 +46,7 @@ jobs: run: make ci-unit integration: timeout-minutes: 8 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v2 diff --git a/Makefile b/Makefile index 001a05f1d..91c9b2b53 100644 --- a/Makefile +++ b/Makefile @@ -68,22 +68,22 @@ dev-migrate-db: .PHONY: build-local build-local: - docker build ./docker/hostmetrics -t ${IMAGE_NAME}:${LATEST_VERSION}-hostmetrics --target prod & - docker build ./docker/ingestor -t ${IMAGE_NAME}:${LATEST_VERSION}-ingestor --target prod & - docker build ./docker/otel-collector -t ${IMAGE_NAME}:${LATEST_VERSION}-otel-collector --target prod & - docker build --build-arg CODE_VERSION=${LATEST_VERSION} . -f ./packages/miner/Dockerfile -t ${IMAGE_NAME}:${LATEST_VERSION}-miner --target prod & - docker build --build-arg CODE_VERSION=${LATEST_VERSION} . -f ./packages/go-parser/Dockerfile -t ${IMAGE_NAME}:${LATEST_VERSION}-go-parser & + docker build ./docker/hostmetrics -t ${IMAGE_NAME_HDX}:${LATEST_VERSION}-hostmetrics --target prod & + docker build ./docker/ingestor -t ${IMAGE_NAME_HDX}:${LATEST_VERSION}-ingestor --target prod & + docker build ./docker/otel-collector -t ${IMAGE_NAME_HDX}:${LATEST_VERSION}-otel-collector --target prod & + docker build --build-arg CODE_VERSION=${LATEST_VERSION} . -f ./packages/miner/Dockerfile -t ${IMAGE_NAME_HDX}:${LATEST_VERSION}-miner --target prod & + docker build --build-arg CODE_VERSION=${LATEST_VERSION} . -f ./packages/go-parser/Dockerfile -t ${IMAGE_NAME_HDX}:${LATEST_VERSION}-go-parser & docker build \ --build-arg CODE_VERSION=${LATEST_VERSION} \ --build-arg PORT=${HYPERDX_API_PORT} \ - . -f ./packages/api/Dockerfile -t ${IMAGE_NAME}:${LATEST_VERSION}-api --target prod & + . -f ./packages/api/Dockerfile -t ${IMAGE_NAME_HDX}:${LATEST_VERSION}-api --target prod & docker build \ --build-arg CODE_VERSION=${LATEST_VERSION} \ --build-arg OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT} \ --build-arg OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME} \ --build-arg PORT=${HYPERDX_APP_PORT} \ --build-arg SERVER_URL=${HYPERDX_API_URL}:${HYPERDX_API_PORT} \ - . -f ./packages/app/Dockerfile -t ${IMAGE_NAME}:${LATEST_VERSION}-app --target prod + . -f ./packages/app/Dockerfile -t ${IMAGE_NAME_HDX}:${LATEST_VERSION}-app --target prod .PHONY: version version: diff --git a/docker-compose.yml b/docker-compose.yml index a49eb0e71..1fbb45d7e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: go-parser: - image: ${IMAGE_NAME}:${IMAGE_VERSION}-go-parser + image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-go-parser container_name: hdx-oss-go-parser environment: AGGREGATOR_API_URL: 'http://aggregator:8001' @@ -16,7 +16,7 @@ services: networks: - internal miner: - image: ${IMAGE_NAME}:${IMAGE_VERSION}-miner + image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-miner container_name: hdx-oss-miner environment: HYPERDX_API_KEY: ${HYPERDX_API_KEY} @@ -30,7 +30,7 @@ services: networks: - internal hostmetrics: - image: ${IMAGE_NAME}:${IMAGE_VERSION}-hostmetrics + image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-hostmetrics container_name: hdx-oss-hostmetrics environment: HYPERDX_API_KEY: ${HYPERDX_API_KEY} @@ -40,7 +40,7 @@ services: networks: - internal ingestor: - image: ${IMAGE_NAME}:${IMAGE_VERSION}-ingestor + image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-ingestor container_name: hdx-oss-ingestor volumes: - .volumes/ingestor_data:/var/lib/vector @@ -76,7 +76,7 @@ services: networks: - internal otel-collector: - image: ${IMAGE_NAME}:${IMAGE_VERSION}-otel-collector + image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-otel-collector container_name: hdx-oss-otel-collector environment: HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL} @@ -94,7 +94,7 @@ services: networks: - internal aggregator: - image: ${IMAGE_NAME}:${IMAGE_VERSION}-api + image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-api container_name: hdx-oss-aggregator ports: - 8001:8001 @@ -118,7 +118,7 @@ services: - redis - ch-server task-check-alerts: - image: ${IMAGE_NAME}:${IMAGE_VERSION}-api + image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-api container_name: hdx-oss-task-check-alerts entrypoint: 'node' command: './build/tasks/index.js check-alerts' @@ -150,7 +150,7 @@ services: - db - redis api: - image: ${IMAGE_NAME}:${IMAGE_VERSION}-api + image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-api container_name: hdx-oss-api ports: - ${HYPERDX_API_PORT}:${HYPERDX_API_PORT} @@ -185,7 +185,7 @@ services: - db - redis app: - image: ${IMAGE_NAME}:${IMAGE_VERSION}-app + image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-app container_name: hdx-oss-app ports: - ${HYPERDX_APP_PORT}:${HYPERDX_APP_PORT} diff --git a/docker/ingestor/Dockerfile b/docker/ingestor/Dockerfile index 7fb9bca99..2b5eda305 100644 --- a/docker/ingestor/Dockerfile +++ b/docker/ingestor/Dockerfile @@ -1,5 +1,5 @@ ## base ############################################################################################# -FROM timberio/vector:0.41.1-alpine AS base +FROM timberio/vector:0.43.1-alpine AS base RUN mkdir -p /var/lib/vector VOLUME ["/var/lib/vector"] diff --git a/package.json b/package.json index dd8415d18..78e78b100 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hyperdx", "private": true, - "version": "1.10.0", + "version": "1.10.1", "license": "MIT", "workspaces": [ "packages/*" diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 065845721..aee9e1e15 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,12 @@ # @hyperdx/api +## 1.10.1 + +### Patch Changes + +- e1d8409: fix: expandToNestedObject method bug (inconsistent top-level keys) +- 9daccff: fix: should check team + service + name uniq constraint instead (adding webhook) + ## 1.10.0 ### Minor Changes diff --git a/packages/api/package.json b/packages/api/package.json index cca64f477..5dd3294d2 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@hyperdx/api", - "version": "1.10.0", + "version": "1.10.1", "license": "MIT", "private": true, "engines": { diff --git a/packages/api/src/controllers/team.ts b/packages/api/src/controllers/team.ts index 7ef947d43..e25b8a058 100644 --- a/packages/api/src/controllers/team.ts +++ b/packages/api/src/controllers/team.ts @@ -61,6 +61,10 @@ export function rotateTeamApiKey(teamId: ObjectId) { return Team.findByIdAndUpdate(teamId, { apiKey: uuidv4() }, { new: true }); } +export function setTeamName(teamId: ObjectId, name: string) { + return Team.findByIdAndUpdate(teamId, { name }, { new: true }); +} + export async function getTags(teamId: ObjectId) { const [dashboardTags, logViewTags] = await Promise.all([ Dashboard.aggregate([ diff --git a/packages/api/src/routers/api/team.ts b/packages/api/src/routers/api/team.ts index 08b9f3c09..adda9268e 100644 --- a/packages/api/src/routers/api/team.ts +++ b/packages/api/src/routers/api/team.ts @@ -6,7 +6,12 @@ import { z } from 'zod'; import { validateRequest } from 'zod-express-middleware'; import * as config from '@/config'; -import { getTags, getTeam, rotateTeamApiKey } from '@/controllers/team'; +import { + getTags, + getTeam, + rotateTeamApiKey, + setTeamName, +} from '@/controllers/team'; import { deleteTeamMember, findUserByEmail, @@ -78,6 +83,28 @@ router.patch('/apiKey', async (req, res, next) => { } }); +router.patch( + '/name', + validateRequest({ + body: z.object({ + name: z.string().min(1).max(100), + }), + }), + async (req, res, next) => { + try { + const teamId = req.user?.team; + if (teamId == null) { + throw new Error(`User ${req.user?._id} not associated with a team`); + } + const { name } = req.body; + const team = await setTeamName(teamId, name); + res.json({ name: team?.name }); + } catch (e) { + next(e); + } + }, +); + router.post( '/invitation', validateRequest({ diff --git a/packages/api/src/routers/api/webhooks.ts b/packages/api/src/routers/api/webhooks.ts index 773fa77df..a2bb4a7b0 100644 --- a/packages/api/src/routers/api/webhooks.ts +++ b/packages/api/src/routers/api/webhooks.ts @@ -58,7 +58,7 @@ router.post( } const { name, service, url, description, queryParams, headers, body } = req.body; - if (await Webhook.findOne({ team: teamId, service, url })) { + if (await Webhook.findOne({ team: teamId, service, name })) { return res.status(400).json({ message: 'Webhook already exists', }); diff --git a/packages/api/src/tasks/__tests__/checkAlerts.test.ts b/packages/api/src/tasks/__tests__/checkAlerts.test.ts index a3504c2a6..7bc92f340 100644 --- a/packages/api/src/tasks/__tests__/checkAlerts.test.ts +++ b/packages/api/src/tasks/__tests__/checkAlerts.test.ts @@ -99,6 +99,23 @@ describe('checkAlerts', () => { expect(expandToNestedObject({ 'foo.bar.baz': 'qux' })).toEqual({ foo: { bar: { baz: 'qux' } }, }); + // inconsistent top level keys + expect( + expandToNestedObject({ + foo: 'bar', + 'foo.bar': 'baz', + }), + ).toEqual({ + foo: 'bar', + }); + expect( + expandToNestedObject({ + 'foo.bar': 'baz', + foo: 'bar', + }), + ).toEqual({ + foo: 'bar', + }); // mix expect( expandToNestedObject({ diff --git a/packages/api/src/tasks/checkAlerts.ts b/packages/api/src/tasks/checkAlerts.ts index 18cb84c2a..3ce0ade83 100644 --- a/packages/api/src/tasks/checkAlerts.ts +++ b/packages/api/src/tasks/checkAlerts.ts @@ -126,11 +126,16 @@ export const expandToNestedObject = ( break; } const nestedKey = keys[i]; - if (i === keys.length - 1) { - nestedObj[nestedKey] = obj[key]; - } else { - nestedObj[nestedKey] = nestedObj[nestedKey] || {}; - nestedObj = nestedObj[nestedKey]; + try { + if (i === keys.length - 1) { + nestedObj[nestedKey] = obj[key]; + } else { + nestedObj[nestedKey] = nestedObj[nestedKey] || {}; + nestedObj = nestedObj[nestedKey]; + } + } catch (e) { + // skip the duplicate inconsistent top level keys case. ex: 'foo' and 'foo.bar' + logger.warn('expandToNestedObject', e); } } } diff --git a/packages/app/.storybook/preview-head.html b/packages/app/.storybook/preview-head.html index fb4d6a073..18f2685f7 100644 --- a/packages/app/.storybook/preview-head.html +++ b/packages/app/.storybook/preview-head.html @@ -1,13 +1,2 @@ - - - + + \ No newline at end of file diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 1e0bcc961..9ba303280 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,7 @@ # @hyperdx/app +## 1.10.1 + ## 1.10.0 ### Minor Changes diff --git a/packages/app/package.json b/packages/app/package.json index 9c95f7807..2b72ea90f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@hyperdx/app", - "version": "1.10.0", + "version": "1.10.1", "private": true, "license": "MIT", "engines": { diff --git a/packages/app/pages/_app.tsx b/packages/app/pages/_app.tsx index 29b8a6138..8fdeb1b92 100644 --- a/packages/app/pages/_app.tsx +++ b/packages/app/pages/_app.tsx @@ -95,21 +95,9 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) { return ( - + - - - + - {label} + {label} ))} diff --git a/packages/app/src/ChartUtils.tsx b/packages/app/src/ChartUtils.tsx index 8205acc72..0373dced3 100644 --- a/packages/app/src/ChartUtils.tsx +++ b/packages/app/src/ChartUtils.tsx @@ -15,8 +15,13 @@ import Checkbox from './Checkbox'; import FieldMultiSelect from './FieldMultiSelect'; import MetricTagFilterInput from './MetricTagFilterInput'; import SearchInput from './SearchInput'; -import { AggFn, ChartSeries, MetricsDataType, SourceTable } from './types'; -import { NumberFormat } from './types'; +import { + AggFn, + ChartSeries, + MetricsDataType, + NumberFormat, + SourceTable, +} from './types'; import { legacyMetricNameToNameAndDataType } from './utils'; export const SORT_ORDER = [ @@ -31,19 +36,28 @@ export const TABLES = [ { value: 'metrics' as const, label: 'Metrics' }, ]; -export const AGG_FNS = [ - { value: 'count' as const, label: 'Count of Events' }, - { value: 'sum' as const, label: 'Sum' }, - { value: 'p99' as const, label: '99th Percentile' }, - { value: 'p95' as const, label: '95th Percentile' }, - { value: 'p90' as const, label: '90th Percentile' }, - { value: 'p50' as const, label: 'Median' }, - { value: 'avg' as const, label: 'Average' }, - { value: 'max' as const, label: 'Maximum' }, - { value: 'min' as const, label: 'Minimum' }, - { value: 'count_distinct' as const, label: 'Count Distinct' }, +export const AGG_FNS: { + value: AggFn; + label: string; +}[] = [ + { value: 'avg', label: 'Average' }, + { value: 'count', label: 'Count of Events' }, + { value: 'count_distinct', label: 'Count Distinct' }, + { value: 'count_per_hour', label: 'Count of Events per Hour' }, + { value: 'count_per_min', label: 'Count of Events per Minute' }, + { value: 'count_per_sec', label: 'Count of Events per Second' }, + { value: 'max', label: 'Maximum' }, + { value: 'min', label: 'Minimum' }, + { value: 'p50', label: 'Median' }, + { value: 'p90', label: '90th Percentile' }, + { value: 'p95', label: '95th Percentile' }, + { value: 'p99', label: '99th Percentile' }, + { value: 'sum', label: 'Sum' }, ]; +export const isCountAggFn = (aggFn: AggFn) => + aggFn.startsWith('count_per') || aggFn === 'count'; + export const getMetricAggFns = ( dataType: MetricsDataType, ): { value: AggFn; label: string }[] => { @@ -575,261 +589,6 @@ export function FieldSelect({ ); } -export function ChartSeriesForm({ - aggFn, - field, - groupBy, - setAggFn, - setField, - setFieldAndAggFn, - setTableAndAggFn, - setGroupBy, - setSortOrder, - setWhere, - sortOrder, - table, - where, - numberFormat, - setNumberFormat, -}: { - aggFn: AggFn; - field: string | undefined; - groupBy: string | undefined; - setAggFn: (fn: AggFn) => void; - setField: (field: string | undefined) => void; - setFieldAndAggFn: (field: string | undefined, fn: AggFn) => void; - setTableAndAggFn: (table: SourceTable, fn: AggFn) => void; - setGroupBy: (groupBy: string | undefined) => void; - setSortOrder?: (sortOrder: SortOrder) => void; - setWhere: (where: string) => void; - sortOrder?: string; - table: string; - where: string; - numberFormat?: NumberFormat; - setNumberFormat?: (format?: NumberFormat) => void; -}) { - const labelWidth = 350; - const searchInputRef = useRef(null); - - const isRate = useMemo(() => { - return aggFn.includes('_rate'); - }, [aggFn]); - const _setAggFn = (fn: AggFn, _isRate?: boolean) => { - if (_isRate ?? isRate) { - if (fn.includes('_rate')) { - setAggFn(fn); - } else { - setAggFn(`${fn}_rate` as AggFn); - } - } else { - if (fn.includes('_rate')) { - setAggFn(fn.replace('_rate', '') as AggFn); - } else { - setAggFn(fn); - } - } - }; - const metricAggFns = getMetricAggFns( - legacyMetricNameToNameAndDataType(field)?.dataType, - ); - - return ( -
-
-
- v.value === aggFn)} - onChange={opt => _setAggFn(opt?.value ?? 'count')} - classNamePrefix="ds-react-select" - /> - ) : ( - v.value === sortOrder)} - onChange={opt => setSortOrder(opt?.value ?? 'desc')} - classNamePrefix="ds-react-select" - /> -
-
- ) - } - {setNumberFormat && ( -
- - - Chart Settings - - } - c="dark.2" - mb={8} - /> - -
Number Format
- -
-
- )} -
- ); -} - export function TableSelect({ table, setTableAndAggFn, @@ -978,6 +737,7 @@ export function ChartSeriesFormCompact({ const metricAggFns = getMetricAggFns( legacyMetricNameToNameAndDataType(field)?.dataType, ); + const isAggFnCountDistinct = aggFn === 'count_distinct'; return (
@@ -1012,7 +772,7 @@ export function ChartSeriesFormCompact({ /> )}
- {table === 'logs' && aggFn != 'count' && aggFn != 'count_distinct' ? ( + {table === 'logs' && !isCountAggFn(aggFn) && !isAggFnCountDistinct ? (
) : null} - {table === 'logs' && aggFn != 'count' && aggFn == 'count_distinct' ? ( + {table === 'logs' && isAggFnCountDistinct ? (
- {editedChart.seriesReturnType === 'column' && ( - { - setEditedChart( - produce(editedChart, draft => { - const draftSeries = draft.series[i]; - if (draftSeries.type === chartType) { - draftSeries.color = color; - } - }), - ); - }} - /> + {(series.type === 'table' || series.type === 'time') && ( + <> + + } + title="Table header name" + size="xs" + w={200} + value={series.displayName} + onChange={event => { + setEditedChart( + produce(editedChart, draft => { + const s = draft.series[i]; + if (s.type === 'table' || s.type === 'time') { + s.displayName = event.currentTarget.value; + } + }), + ); + }} + placeholder={`${series.aggFn}(${series.field || ''})`} + /> + )} + {editedChart.seriesReturnType === 'column' && + chartType === 'time' && ( + { + setEditedChart( + produce(editedChart, draft => { + const draftSeries = draft.series[i]; + if (draftSeries.type === chartType) { + draftSeries.color = color; + } + }), + ); + }} + /> + )} + {editedChart.series.length > 1 && (