Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: add basepath config
  • Loading branch information
datnguyennnx committed Sep 19, 2025
commit 5b905c370ce4e20aa2dbe19dec8bda712de84a2f
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ HYPERDX_APP_PORT=8080
HYPERDX_APP_URL=http://localhost
HYPERDX_LOG_LEVEL=debug
HYPERDX_OPAMP_PORT=4320
HYPERDX_BASE_PATH=/hyperdx
HYPERDX_API_BASE_PATH=/hyperdx/api
HYPERDX_OTEL_BASE_PATH=/hyperdx/otel

# Otel/Clickhouse config
HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE=default
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ build-app:
--build-context api=./packages/api \
--build-context app=./packages/app \
--build-arg CODE_VERSION=${CODE_VERSION} \
--build-arg HYPERDX_BASE_PATH="${HYPERDX_BASE_PATH}" \
--build-arg HYPERDX_API_BASE_PATH="${HYPERDX_API_BASE_PATH}" \
--build-arg HYPERDX_OTEL_BASE_PATH="${HYPERDX_OTEL_BASE_PATH}" \
Copy link
Member

@wrn14897 wrn14897 Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this mean that devs need to rebuild the image whenever the env var changes?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wrn14897 No rebuild needed! Those build-args were a holdover from an earlier iteration - I've removed them entirely from the Makefile.

The base paths are now read at runtime via process.env in centralized utils (basePath.ts), so you can switch subpaths dynamically with docker run -e HYPERDX_BASE_PATH=/hyperdx or compose restart - no image rebuilds required. Verified: Build once (make build-app), run root → subpath via -e, and curls/UI/API prefix correctly (e.g., /hyperdx/api/health 200 OK).

This aligns with your dynamic config concern. Please re-review—tests/E2E confirm no regressions!

-t ${IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION}${IMAGE_VERSION_SUB_TAG} \
--target prod

Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ services:
HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE: ${HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE}
HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL}
OPAMP_SERVER_URL: 'http://app:${HYPERDX_OPAMP_PORT}'
HYPERDX_OTEL_BASE_PATH: ${HYPERDX_OTEL_BASE_PATH:-}
ports:
- '13133:13133' # health_check extension
- '24225:24225' # fluentd receiver
Expand Down Expand Up @@ -62,6 +63,9 @@ services:
OTEL_EXPORTER_OTLP_ENDPOINT: 'http://otel-collector:4318'
OTEL_SERVICE_NAME: 'hdx-oss-app'
USAGE_STATS_ENABLED: ${USAGE_STATS_ENABLED:-true}
HYPERDX_BASE_PATH: ${HYPERDX_BASE_PATH:-}
HYPERDX_API_BASE_PATH: ${HYPERDX_API_BASE_PATH:-}
HYPERDX_OTEL_BASE_PATH: ${HYPERDX_OTEL_BASE_PATH:-}
DEFAULT_CONNECTIONS:
'[{"name":"Local
ClickHouse","host":"http://ch-server:8123","username":"default","password":""}]'
Expand Down
7 changes: 7 additions & 0 deletions docker/hyperdx/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ RUN yarn install --mode=skip-build && yarn cache clean
## API/APP Builder Image ##########################################################################
FROM node_base AS builder

ARG HYPERDX_BASE_PATH
ARG HYPERDX_API_BASE_PATH
ARG HYPERDX_OTEL_BASE_PATH

WORKDIR /app

COPY --from=api ./src ./packages/api/src
Expand All @@ -48,6 +52,9 @@ COPY --from=app ./types ./packages/app/types
ENV NEXT_TELEMETRY_DISABLED 1
ENV NEXT_OUTPUT_STANDALONE true
ENV NEXT_PUBLIC_IS_LOCAL_MODE false
ENV HYPERDX_BASE_PATH $HYPERDX_BASE_PATH
ENV HYPERDX_API_BASE_PATH $HYPERDX_API_BASE_PATH
ENV HYPERDX_OTEL_BASE_PATH $HYPERDX_OTEL_BASE_PATH
ENV NX_DAEMON=false
RUN npx nx run-many --target=build --projects=@hyperdx/common-utils,@hyperdx/api,@hyperdx/app

Expand Down
113 changes: 77 additions & 36 deletions packages/api/src/api-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import passport from './utils/passport';

const app: express.Application = express();

const API_BASE_PATH = process.env.HYPERDX_API_BASE_PATH || '';

const sess: session.SessionOptions & { cookie: session.CookieOptions } = {
resave: false,
saveUninitialized: false,
Expand Down Expand Up @@ -79,45 +81,84 @@ if (config.USAGE_STATS_ENABLED) {
// ---------------------------------------------------------------------
// ----------------------- Internal Routers ----------------------------
// ---------------------------------------------------------------------
// PUBLIC ROUTES
app.use('/', routers.rootRouter);
if (API_BASE_PATH) {
const apiRouter = express.Router();

// PUBLIC ROUTES
apiRouter.use('/', routers.rootRouter);

// PRIVATE ROUTES
apiRouter.use('/alerts', isUserAuthenticated, routers.alertsRouter);
apiRouter.use('/dashboards', isUserAuthenticated, routers.dashboardRouter);
apiRouter.use('/me', isUserAuthenticated, routers.meRouter);
apiRouter.use('/team', isUserAuthenticated, routers.teamRouter);
apiRouter.use('/webhooks', isUserAuthenticated, routers.webhooksRouter);
apiRouter.use('/connections', isUserAuthenticated, connectionsRouter);
apiRouter.use('/sources', isUserAuthenticated, sourcesRouter);
apiRouter.use('/saved-search', isUserAuthenticated, savedSearchRouter);
apiRouter.use('/clickhouse-proxy', isUserAuthenticated, clickhouseProxyRouter);
apiRouter.use('/api/v2', externalRoutersV2);

// Only initialize Swagger in development or if explicitly enabled
if (
process.env.NODE_ENV !== 'production' &&
process.env.ENABLE_SWAGGER === 'true'
) {
import('./utils/swagger')
.then(({ setupSwagger }) => {
console.log('Swagger UI setup and available at /api/v2/docs');
setupSwagger(app);
})
.catch(error => {
console.error(
'Failed to dynamically load or setup Swagger. Swagger UI will not be available.',
error,
);
});
}

app.use(API_BASE_PATH, apiRouter);
} else {
// PUBLIC ROUTES
app.use('/', routers.rootRouter);

// PRIVATE ROUTES
app.use('/alerts', isUserAuthenticated, routers.alertsRouter);
app.use('/dashboards', isUserAuthenticated, routers.dashboardRouter);
app.use('/me', isUserAuthenticated, routers.meRouter);
app.use('/team', isUserAuthenticated, routers.teamRouter);
app.use('/webhooks', isUserAuthenticated, routers.webhooksRouter);
app.use('/connections', isUserAuthenticated, connectionsRouter);
app.use('/sources', isUserAuthenticated, sourcesRouter);
app.use('/saved-search', isUserAuthenticated, savedSearchRouter);
app.use('/clickhouse-proxy', isUserAuthenticated, clickhouseProxyRouter);
// ---------------------------------------------------------------------
// PRIVATE ROUTES
app.use('/alerts', isUserAuthenticated, routers.alertsRouter);
app.use('/dashboards', isUserAuthenticated, routers.dashboardRouter);
app.use('/me', isUserAuthenticated, routers.meRouter);
app.use('/team', isUserAuthenticated, routers.teamRouter);
app.use('/webhooks', isUserAuthenticated, routers.webhooksRouter);
app.use('/connections', isUserAuthenticated, connectionsRouter);
app.use('/sources', isUserAuthenticated, sourcesRouter);
app.use('/saved-search', isUserAuthenticated, savedSearchRouter);
app.use('/clickhouse-proxy', isUserAuthenticated, clickhouseProxyRouter);

// TODO: Separate external API routers from internal routers
// ---------------------------------------------------------------------
// ----------------------- External Routers ----------------------------
// ---------------------------------------------------------------------
// API v2
// Only initialize Swagger in development or if explicitly enabled
if (
process.env.NODE_ENV !== 'production' &&
process.env.ENABLE_SWAGGER === 'true'
) {
import('./utils/swagger')
.then(({ setupSwagger }) => {
console.log('Swagger UI setup and available at /api/v2/docs');
setupSwagger(app);
})
.catch(error => {
console.error(
'Failed to dynamically load or setup Swagger. Swagger UI will not be available.',
error,
);
});
}

// TODO: Separate external API routers from internal routers
// ---------------------------------------------------------------------
// ----------------------- External Routers ----------------------------
// ---------------------------------------------------------------------
// API v2
// Only initialize Swagger in development or if explicitly enabled
if (
process.env.NODE_ENV !== 'production' &&
process.env.ENABLE_SWAGGER === 'true'
) {
import('./utils/swagger')
.then(({ setupSwagger }) => {
console.log('Swagger UI setup and available at /api/v2/docs');
setupSwagger(app);
})
.catch(error => {
console.error(
'Failed to dynamically load or setup Swagger. Swagger UI will not be available.',
error,
);
});
app.use('/api/v2', externalRoutersV2);
}

app.use('/api/v2', externalRoutersV2);
// ---------------------------------------------------------------------

// error handling
app.use(appErrorHandler);
Expand Down
6 changes: 4 additions & 2 deletions packages/api/src/opamp/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ import { opampController } from '@/opamp/controllers/opampController';
// Create Express application
const app = express();

const OTEL_BASE_PATH = process.env.HYPERDX_OTEL_BASE_PATH || '';

app.disable('x-powered-by');

// Special body parser setup for OpAMP
app.use(
'/v1/opamp',
`${OTEL_BASE_PATH}/v1/opamp`,
express.raw({
type: 'application/x-protobuf',
limit: '10mb',
}),
);

// OpAMP endpoint
app.post('/v1/opamp', opampController.handleOpampMessage.bind(opampController));
app.post(`${OTEL_BASE_PATH}/v1/opamp`, opampController.handleOpampMessage.bind(opampController));

// Health check endpoint
app.get('/health', (req, res) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ FROM base AS builder
ARG OTEL_EXPORTER_OTLP_ENDPOINT
ARG OTEL_SERVICE_NAME
ARG IS_LOCAL_MODE
ARG HYPERDX_BASE_PATH
ENV NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT $OTEL_EXPORTER_OTLP_ENDPOINT
ENV NEXT_PUBLIC_OTEL_SERVICE_NAME $OTEL_SERVICE_NAME
ENV NEXT_PUBLIC_IS_LOCAL_MODE $IS_LOCAL_MODE
ENV HYPERDX_BASE_PATH $HYPERDX_BASE_PATH
ENV NX_DAEMON false

COPY ./packages/app/src ./packages/app/src
Expand Down
1 change: 1 addition & 0 deletions packages/app/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const withNextra = require('nextra')({
});

module.exports = {
basePath: process.env.HYPERDX_BASE_PATH || '',
experimental: {
instrumentationHook: true,
// External packages to prevent bundling issues with Next.js 14
Expand Down
2 changes: 1 addition & 1 deletion packages/app/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function Document() {
return (
<Html lang="en">
<Head>
<script src="/__ENV.js" />
<script src={`${process.env.HYPERDX_BASE_PATH || ''}/__ENV.js`} />
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js"></script>
<link
rel="stylesheet"
Expand Down
3 changes: 2 additions & 1 deletion packages/app/pages/api/[...all].ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware';

const DEFAULT_SERVER_URL = `http://127.0.0.1:${process.env.HYPERDX_API_PORT}`;
const API_BASE_PATH = process.env.HYPERDX_API_BASE_PATH || '';

export const config = {
api: {
Expand All @@ -14,7 +15,7 @@ export default (req: NextApiRequest, res: NextApiResponse) => {
const proxy = createProxyMiddleware({
changeOrigin: true,
// logger: console, // DEBUG
pathRewrite: { '^/api': '' },
pathRewrite: { '^/api': API_BASE_PATH },
target: process.env.SERVER_URL || DEFAULT_SERVER_URL,
autoRewrite: true,
// ...(IS_DEV && {
Expand Down
8 changes: 7 additions & 1 deletion packages/app/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,14 @@ export function loginHook(request: Request, options: any, response: Response) {
}
}

// Get basePath from runtime environment
const getApiPrefix = () => {
const basePath = process.env.HYPERDX_BASE_PATH || '';
return `${basePath}/api`;
};

export const server = ky.create({
prefixUrl: '/api',
prefixUrl: getApiPrefix(),
credentials: 'include',
hooks: {
afterResponse: [loginHook],
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/clickhouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { getLocalConnections } from '@/connection';
import api from './api';
import { DEFAULT_QUERY_TIMEOUT } from './defaults';

const PROXY_CLICKHOUSE_HOST = '/api/clickhouse-proxy';
const PROXY_CLICKHOUSE_HOST = `${process.env.HYPERDX_BASE_PATH || ''}/api/clickhouse-proxy`;

export const getClickhouseClient = (
options: ClickhouseClientOptions = {},
Expand Down