DX: standalone app docker image (#532)

For better self-hosting experience, users should be able to run
```
docker run -e MONGO_URI=xxx -p 8080:8080 hyperdx/hyperdx:2-beta
```
to spin up the project that includes the server components
This commit is contained in:
Warren 2024-12-16 15:13:16 -08:00 committed by GitHub
parent 74767e820e
commit 9993fb2097
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 72 additions and 108 deletions

5
.env
View file

@ -4,7 +4,9 @@ IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx
IMAGE_NAME_HDX=docker.hyperdx.io/hyperdx/hyperdx
LOCAL_IMAGE_NAME=ghcr.io/hyperdxio/hyperdx-local
LOCAL_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-local
IMAGE_VERSION_SUB_TAG=.7
OTEL_COLLECTOR_IMAGE_NAME=ghcr.io/hyperdxio/hyperdx-otel-collector
OTEL_COLLECTOR_IMAGE_NAME_DOCKERHUB=hyperdx/hyperdx-otel-collector
IMAGE_VERSION_SUB_TAG=.8
IMAGE_VERSION=2-beta
# Set up domain URLs
@ -12,7 +14,6 @@ HYPERDX_API_PORT=8000 #optional (should not be taken by other services)
HYPERDX_APP_PORT=8080
HYPERDX_APP_URL=http://localhost
HYPERDX_LOG_LEVEL=debug
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 # port is fixed
# TEMPORARY: local development
HYPERDX_API_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

View file

@ -98,18 +98,18 @@ release-local:
.PHONY: release
release:
docker buildx build --platform ${BUILD_PLATFORMS} ./docker/otel-collector \
-t ${IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION}${IMAGE_VERSION_SUB_TAG}-otel-collector \
-t ${IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION}-otel-collector \
-t ${IMAGE_NAME}:${IMAGE_VERSION}${IMAGE_VERSION_SUB_TAG}-otel-collector \
-t ${IMAGE_NAME}:${IMAGE_VERSION}-otel-collector \
--target prod --push &
-t ${OTEL_COLLECTOR_IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION}${IMAGE_VERSION_SUB_TAG} \
-t ${OTEL_COLLECTOR_IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION} \
-t ${OTEL_COLLECTOR_IMAGE_NAME}:${IMAGE_VERSION}${IMAGE_VERSION_SUB_TAG} \
-t ${OTEL_COLLECTOR_IMAGE_NAME}:${IMAGE_VERSION} \
--target prod --push & \
docker buildx build --squash . -f ./docker/fullstack/Dockerfile \
--build-context fullstack=./docker/fullstack \
--build-context api=./packages/api \
--build-context app=./packages/app \
--platform ${BUILD_PLATFORMS} \
-t ${IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION}${IMAGE_VERSION_SUB_TAG}-app \
-t ${IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION}-app \
-t ${IMAGE_NAME}:${IMAGE_VERSION}${IMAGE_VERSION_SUB_TAG}-app \
-t ${IMAGE_NAME}:${IMAGE_VERSION}-app \
-t ${IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION}${IMAGE_VERSION_SUB_TAG} \
-t ${IMAGE_NAME_DOCKERHUB}:${IMAGE_VERSION} \
-t ${IMAGE_NAME}:${IMAGE_VERSION}${IMAGE_VERSION_SUB_TAG} \
-t ${IMAGE_NAME}:${IMAGE_VERSION} \
--target prod --push

View file

@ -56,10 +56,6 @@ services:
# ports:
# - 9000:9000
environment:
APP_TYPE: 'api'
CLICKHOUSE_HOST: http://ch-server:8123
CLICKHOUSE_PASSWORD: api
CLICKHOUSE_USER: api
EXPRESS_SESSION_SECRET: 'hyperdx is cool 👋'
MONGO_URI: 'mongodb://db:29999/hyperdx-test'
NODE_ENV: ci

View file

@ -78,10 +78,6 @@ services:
# command: 'dev:task check-alerts'
# environment:
# APP_TYPE: 'scheduled-task'
# CLICKHOUSE_HOST: http://ch-server:8123
# CLICKHOUSE_LOG_LEVEL: ${HYPERDX_LOG_LEVEL}
# CLICKHOUSE_PASSWORD: worker
# CLICKHOUSE_USER: worker
# EXPRESS_SESSION_SECRET: 'hyperdx is cool 👋'
# FRONTEND_URL: 'http://localhost:8080' # need to be localhost (CORS)
# HDX_NODE_ADVANCED_NETWORK_CAPTURE: 1
@ -113,11 +109,6 @@ services:
ports:
- ${HYPERDX_API_PORT}:${HYPERDX_API_PORT}
environment:
APP_TYPE: 'api'
CLICKHOUSE_HOST: http://ch-server:8123
CLICKHOUSE_LOG_LEVEL: ${HYPERDX_LOG_LEVEL}
CLICKHOUSE_PASSWORD: api
CLICKHOUSE_USER: api
EXPRESS_SESSION_SECRET: 'hyperdx is cool 👋'
FRONTEND_URL: 'http://localhost:${HYPERDX_APP_PORT}' # need to be localhost (CORS)
HDX_NODE_ADVANCED_NETWORK_CAPTURE: 1
@ -125,11 +116,9 @@ services:
HDX_NODE_CONSOLE_CAPTURE: 1
HYPERDX_API_KEY: ${HYPERDX_API_KEY}
HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL}
INGESTOR_API_URL: 'http://ingestor:8002'
MINER_API_URL: 'http://miner:5123'
MONGO_URI: 'mongodb://db:27017/hyperdx'
NODE_ENV: development
OTEL_EXPORTER_OTLP_ENDPOINT: 'http://otel-collector:4318'
OTEL_SERVICE_NAME: 'hdx-oss-dev-api'
PORT: ${HYPERDX_API_PORT}
REDIS_URL: redis://redis:6379

View file

@ -53,7 +53,7 @@ services:
networks:
- internal
otel-collector:
image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-otel-collector
image: ${OTEL_COLLECTOR_IMAGE_NAME}:${IMAGE_VERSION}
environment:
CLICKHOUSE_SERVER_ENDPOINT: 'ch-server:9000'
HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL}
@ -75,10 +75,7 @@ services:
# command: './build/tasks/index.js check-alerts'
# environment:
# APP_TYPE: 'scheduled-task'
# CLICKHOUSE_HOST: http://ch-server:8123
# CLICKHOUSE_LOG_LEVEL: ${HYPERDX_LOG_LEVEL}
# CLICKHOUSE_PASSWORD: worker
# CLICKHOUSE_USER: worker
# EXPRESS_SESSION_SECRET: 'hyperdx is cool 👋'
# FRONTEND_URL: ${HYPERDX_APP_URL}:${HYPERDX_APP_PORT} # need to be localhost (CORS)
# HDX_NODE_ADVANCED_NETWORK_CAPTURE: 1
@ -101,32 +98,20 @@ services:
# - db
# - redis
app:
image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}-app
image: ${IMAGE_NAME_HDX}:${IMAGE_VERSION}
ports:
- ${HYPERDX_API_PORT}:${HYPERDX_API_PORT}
- ${HYPERDX_APP_PORT}:${HYPERDX_APP_PORT}
environment:
APP_TYPE: 'api'
CLICKHOUSE_HOST: http://ch-server:8123
CLICKHOUSE_LOG_LEVEL: ${HYPERDX_LOG_LEVEL}
CLICKHOUSE_PASSWORD: api
CLICKHOUSE_USER: api
EXPRESS_SESSION_SECRET: 'hyperdx is cool 👋'
FRONTEND_URL: ${HYPERDX_APP_URL}:${HYPERDX_APP_PORT}
HDX_NODE_ADVANCED_NETWORK_CAPTURE: 1
HDX_NODE_BETA_MODE: 1
HDX_NODE_CONSOLE_CAPTURE: 1
HYPERDX_API_KEY: ${HYPERDX_API_KEY}
HYPERDX_API_PORT: ${HYPERDX_API_PORT}
HYPERDX_APP_PORT: ${HYPERDX_APP_PORT}
HYPERDX_APP_URL: ${HYPERDX_APP_URL}
HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL}
INGESTOR_API_URL: 'http://ingestor:8002'
MINER_API_URL: 'http://miner:5123'
MONGO_URI: 'mongodb://db:27017/hyperdx'
NEXT_PUBLIC_CLICKHOUSE_HOST: http://ch-server:8123
NEXT_PUBLIC_SERVER_URL: http://127.0.0.1:${HYPERDX_API_PORT}
OTEL_EXPORTER_OTLP_ENDPOINT: 'http://otel-collector:4318'
OTEL_SERVICE_NAME: 'hdx-oss-api'
REDIS_URL: redis://redis:6379
USAGE_STATS_ENABLED: ${USAGE_STATS_ENABLED:-true}

View file

@ -55,6 +55,9 @@ FROM node:${NODE_VERSION}-alpine AS prod
ENV NODE_ENV production
# Install libs used for the start script
RUN npm install -g concurrently@9.1.0
USER node
# Set up API
@ -72,4 +75,4 @@ COPY --from=app_builder /app/app/package.json ./package.json
# Set up start script
COPY --chown=node:node --from=fullstack ./entry.sh /etc/local/entry.sh
CMD sh /etc/local/entry.sh
ENTRYPOINT ["sh", "/etc/local/entry.sh"]

View file

@ -1,8 +1,13 @@
#!/bin/bash
export FRONTEND_URL="${FRONTEND_URL:-${HYPERDX_APP_URL:-http://localhost}:${HYPERDX_APP_PORT:-8080}}"
echo "Visit the HyperDX UI at $FRONTEND_URL"
echo ""
# Use concurrently to run both the API and App servers
npx concurrently \
"--kill-others" \
"--names=API,APP" \
"APP_TYPE=api PORT=${HYPERDX_API_PORT:-8000} node -r /app/api/node_modules/@hyperdx/node-opentelemetry/build/src/tracing /app/api/build/index.js" \
"/app/app/node_modules/.bin/next start -p ${HYPERDX_APP_PORT:-8080}"
"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/build/index.js" \
"HYPERDX_API_PORT=${HYPERDX_API_PORT:-8000} /app/app/node_modules/.bin/next start -p ${HYPERDX_APP_PORT:-8080}"

View file

@ -11,7 +11,6 @@ export SERVER_URL="http://127.0.0.1:${HYPERDX_API_PORT:-8000}"
export FRONTEND_URL="${FRONTEND_URL:-${HYPERDX_APP_URL:-http://localhost}:${HYPERDX_APP_PORT:-8080}}"
# Internal Services
export CLICKHOUSE_HOST="http://ch-server:8123"
export CLICKHOUSE_SERVER_ENDPOINT="ch-server:9000"
export MONGO_URI="mongodb://db:27017/hyperdx"
export REDIS_URI="redis://redis:6379"
@ -52,7 +51,6 @@ done
otelcol-contrib --config /etc/otelcol-contrib/config.yaml &
# Api
# APP_TYPE=api \
# CLICKHOUSE_USER=api \
# CLICKHOUSE_PASSWORD=api \
# PORT=8000 \

View file

@ -34,7 +34,7 @@ const sess: session.SessionOptions & { cookie: session.CookieOptions } = {
};
app.set('trust proxy', 1);
if (config.FRONTEND_URL) {
if (config.FRONTEND_URL && !config.IS_CI) {
const feUrl = new URL(config.FRONTEND_URL);
sess.cookie.domain = feUrl.hostname;
if (feUrl.protocol === 'https:') {

View file

@ -162,11 +162,10 @@ export class CHLogger implements _CHLogger {
}
}
// TODO: move this to somewhere else
// TODO: TO BE DEPRECATED
export const client = createClient({
host: config.CLICKHOUSE_HOST,
username: config.CLICKHOUSE_USER,
password: config.CLICKHOUSE_PASSWORD,
host: 'http://localhost:8123',
username: 'default',
request_timeout: ms('1m'),
compression: {
request: false,

View file

@ -1,26 +1,30 @@
const env = process.env;
// DEFAULTS
const DEFAULT_APP_TYPE = 'api';
const DEFAULT_EXPRESS_SESSION = 'hyperdx is cool 👋';
const DEFAULT_FRONTEND_URL = `http://localhost:${env.HYPERDX_APP_PORT}`;
export const NODE_ENV = env.NODE_ENV as string;
export const APP_TYPE = env.APP_TYPE as 'api' | 'scheduled-task';
export const CLICKHOUSE_HOST = env.CLICKHOUSE_HOST as string;
export const CLICKHOUSE_PASSWORD = env.CLICKHOUSE_PASSWORD as string;
export const CLICKHOUSE_USER = env.CLICKHOUSE_USER as string;
export const APP_TYPE = (env.APP_TYPE || DEFAULT_APP_TYPE) as
| 'api'
| 'scheduled-task';
export const CODE_VERSION = env.CODE_VERSION as string;
export const EXPRESS_SESSION_SECRET = env.EXPRESS_SESSION_SECRET as string;
export const FRONTEND_URL = env.FRONTEND_URL as string;
export const EXPRESS_SESSION_SECRET = (env.EXPRESS_SESSION_SECRET ||
DEFAULT_EXPRESS_SESSION) as string;
export const FRONTEND_URL = (env.FRONTEND_URL ||
DEFAULT_FRONTEND_URL) as string;
export const HYPERDX_API_KEY = env.HYPERDX_API_KEY as string;
export const HYPERDX_LOG_LEVEL = env.HYPERDX_LOG_LEVEL as string;
export const INGESTOR_API_URL = env.INGESTOR_API_URL as string;
export const IS_CI = NODE_ENV === 'ci';
export const IS_DEV = NODE_ENV === 'development';
export const IS_PROD = NODE_ENV === 'production';
export const MINER_API_URL = env.MINER_API_URL as string;
export const MONGO_URI = env.MONGO_URI as string;
export const OTEL_EXPORTER_OTLP_ENDPOINT =
env.OTEL_EXPORTER_OTLP_ENDPOINT as string;
export const MONGO_URI = env.MONGO_URI;
export const OTEL_SERVICE_NAME = env.OTEL_SERVICE_NAME as string;
export const PORT = Number.parseInt(env.PORT as string);
export const REDIS_URL = env.REDIS_URL as string;
export const REDIS_URL = env.REDIS_URL;
export const USAGE_STATS_ENABLED = env.USAGE_STATS_ENABLED !== 'false';
// Only for single container local deployments, disable authentication

View file

@ -32,6 +32,9 @@ export const connectDB = async () => {
if (!config.IS_CI) {
throw new Error('ONLY execute this in CI env 😈 !!!');
}
if (config.MONGO_URI == null) {
throw new Error('MONGO_URI is not set');
}
await mongoose.connect(config.MONGO_URI);
};

View file

@ -35,6 +35,9 @@ mongoose.connection.on('reconnectFailed', () => {
});
export const connectDB = async () => {
if (config.MONGO_URI == null) {
throw new Error('MONGO_URI is not set');
}
await mongoose.connect(config.MONGO_URI, {
heartbeatFrequencyMS: 10000, // retry failed heartbeats
maxPoolSize: 100, // 5 nodes -> max 1000 connections

View file

@ -2,28 +2,19 @@ import http from 'http';
import gracefulShutdown from 'http-graceful-shutdown';
import { serializeError } from 'serialize-error';
import apiServer from './api-app';
import * as config from './config';
import { connectDB, mongooseConnection } from './models';
import logger from './utils/logger';
import redisClient from './utils/redis';
export default class Server {
protected readonly appType = config.APP_TYPE;
protected shouldHandleGracefulShutdown = true;
protected httpServer!: http.Server;
private async createServer() {
switch (this.appType) {
case 'api':
return http.createServer(
// eslint-disable-next-line n/no-unsupported-features/es-syntax
(await import('./api-app').then(m => m.default)) as any,
);
default:
throw new Error(`Invalid APP_TYPE: ${config.APP_TYPE}`);
}
return http.createServer(apiServer);
}
protected async shutdown(signal?: string) {

View file

@ -51,35 +51,10 @@ const getClickhouseTableSize = async () => {
return result.data;
};
const healthChecks = async () => {
const ping = async (url: string) => {
try {
const res = await fetch(url);
return res.status === 200;
} catch (err) {
return false;
}
};
const otelCollectorUrl = new URL(config.OTEL_EXPORTER_OTLP_ENDPOINT ?? '');
const [pingOtelCollector, pingCH] = await Promise.all([
otelCollectorUrl.hostname && otelCollectorUrl.protocol
? ping(`${otelCollectorUrl.protocol}//${otelCollectorUrl.hostname}:13133`)
: Promise.resolve(null),
ping(`${config.CLICKHOUSE_HOST}/ping`),
]);
return {
pingOtelCollector,
pingCH,
};
};
export default async () => {
try {
const nowInMs = Date.now();
const [userCounts, team, chTables, servicesHealth] = await Promise.all([
const [userCounts, team, chTables] = await Promise.all([
User.countDocuments(),
Team.find(
{},
@ -88,7 +63,6 @@ export default async () => {
},
).limit(1),
getClickhouseTableSize(),
healthChecks(),
]);
const clusterId = team[0]?._id;
logger.info({
@ -96,7 +70,6 @@ export default async () => {
clusterId,
version: config.CODE_VERSION,
userCounts,
servicesHealth,
os: {
arch: os.arch(),
freemem: os.freemem(),

View file

@ -6,7 +6,6 @@ import {
APP_TYPE,
HYPERDX_API_KEY,
HYPERDX_LOG_LEVEL,
INGESTOR_API_URL,
IS_PROD,
} from '@/config';
@ -30,7 +29,6 @@ const DEFAULT_FORMAT = winston.format.combine(
const hyperdxTransport = HYPERDX_API_KEY
? getWinstonTransport(MAX_LEVEL, {
bufferSize: APP_TYPE === 'scheduled-task' ? 1 : 100,
...(INGESTOR_API_URL && { baseUrl: INGESTOR_API_URL }),
})
: null;

View file

@ -8,8 +8,16 @@ const client = createClient({
url: config.REDIS_URL,
});
// check if client is initialized
if (client == null) {
logger.warn('Redis client is not initialized');
// IMPLEMENT: use local in-memory cache
}
client.on('error', (err: any) => {
logger.error('Redis error: ', serializeError(err));
if (config.REDIS_URL) {
logger.error('Redis error: ', serializeError(err));
}
});
// TODO: add tests
@ -22,6 +30,9 @@ class SimpleCache<T> {
) {}
async refresh() {
if (client == null) {
throw new Error('Redis client is not initialized');
}
const dt = Date.now();
const result = await this.fetcher();
if (this.shouldRefreshOnResult(result)) {
@ -38,6 +49,9 @@ class SimpleCache<T> {
}
async get(): Promise<T> {
if (client == null) {
throw new Error('Redis client is not initialized');
}
const cached = await client.get(this.key);
if (cached != null) {
logger.info({

View file

@ -8,7 +8,7 @@
},
"scripts": {
"dev": "next dev",
"dev:local": "NEXT_PUBLIC_CLICKHOUSE_HOST=http://localhost:8123 NEXT_PUBLIC_IS_LOCAL_MODE=true next dev -p 8080",
"dev:local": "NEXT_PUBLIC_IS_LOCAL_MODE=true next dev -p 8080",
"build": "next build",
"start": "next start",
"lint": "eslint --quiet . --ext .ts,.tsx",

View file

@ -3,6 +3,8 @@ import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware';
import { IS_DEV } from '@/config';
const DEFAULT_SERVER_URL = `http://127.0.0.1:${process.env.HYPERDX_API_PORT}`;
export const config = {
api: {
externalResolver: true,
@ -15,7 +17,7 @@ export default (req: NextApiRequest, res: NextApiResponse) => {
changeOrigin: true,
// logger: console, // DEBUG
pathRewrite: { '^/api': '' },
target: process.env.NEXT_PUBLIC_SERVER_URL,
target: process.env.NEXT_PUBLIC_SERVER_URL || DEFAULT_SERVER_URL,
autoRewrite: true,
/**
* Fix bodyParser