mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
parent
9004826d7c
commit
d72d1d2d26
29 changed files with 1954 additions and 75 deletions
5
.changeset/mean-pumpkins-greet.md
Normal file
5
.changeset/mean-pumpkins-greet.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/api": patch
|
||||
---
|
||||
|
||||
Add ingestion key authentication in OTel collector via OpAMP
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -32,5 +32,5 @@
|
|||
"**/yarn.lock": true,
|
||||
"**/yarn-*.cjs": true
|
||||
},
|
||||
"cSpell.words": ["micropip", "pyimport", "pyodide"]
|
||||
"cSpell.words": ["micropip", "opamp", "pyimport", "pyodide"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,19 @@ services:
|
|||
depends_on:
|
||||
- otel-collector
|
||||
otel-collector:
|
||||
image: otel/opentelemetry-collector-contrib:0.120.0
|
||||
# image: otel/opentelemetry-collector-contrib:0.120.0
|
||||
build:
|
||||
context: ./docker/otel-collector
|
||||
target: dev
|
||||
environment:
|
||||
CLICKHOUSE_PROMETHEUS_METRICS_ENDPOINT: 'ch-server:9363'
|
||||
CLICKHOUSE_ENDPOINT: 'tcp://ch-server:9000?dial_timeout=10s'
|
||||
HYPERDX_API_KEY: ${HYPERDX_API_KEY}
|
||||
HYPERDX_LOG_LEVEL: ${HYPERDX_LOG_LEVEL}
|
||||
OPAMP_SERVER_URL: 'http://host.docker.internal:4320'
|
||||
volumes:
|
||||
- ./docker/otel-collector/config.yaml:/etc/otelcol-contrib/config.yaml
|
||||
- ./docker/otel-collector/supervisor_docker.yaml:/etc/otel/supervisor.yaml
|
||||
ports:
|
||||
- '13133:13133' # health_check extension
|
||||
- '24225:24225' # fluentd receiver
|
||||
|
|
@ -57,7 +62,9 @@ services:
|
|||
- internal
|
||||
healthcheck:
|
||||
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
|
||||
test: wget --no-verbose --tries=1 http://127.0.0.1:8123/ping || exit 1
|
||||
test:
|
||||
wget -O /dev/null --no-verbose --tries=1 http://127.0.0.1:8123/ping ||
|
||||
exit 1
|
||||
interval: 1s
|
||||
timeout: 1s
|
||||
retries: 60
|
||||
|
|
|
|||
|
|
@ -1,18 +1,40 @@
|
|||
## base #############################################################################################
|
||||
FROM otel/opentelemetry-collector-contrib:0.120.0 AS base
|
||||
FROM otel/opentelemetry-collector-contrib:0.126.0 AS col
|
||||
FROM ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-opampsupervisor:0.126.0 AS supervisor
|
||||
|
||||
# From: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/aa5c3aa4c7ec174361fcaf908de8eaca72263078/cmd/opampsupervisor/Dockerfile#L18
|
||||
FROM alpine:latest@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c AS prep
|
||||
RUN apk --update add ca-certificates
|
||||
RUN mkdir -p /etc/otel/supervisor-data/
|
||||
|
||||
FROM scratch AS base
|
||||
|
||||
ARG USER_UID=10001
|
||||
ARG USER_GID=10001
|
||||
USER ${USER_UID}:${USER_GID}
|
||||
|
||||
COPY --from=prep /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=prep --chmod=777 --chown=${USER_UID}:${USER_GID} /etc/otel/supervisor-data /etc/otel/supervisor-data
|
||||
COPY --from=supervisor --chmod=755 /usr/local/bin/opampsupervisor /
|
||||
COPY ./supervisor_docker.yaml /etc/otel/supervisor.yaml
|
||||
COPY --from=col --chmod=755 /otelcol-contrib /otelcontribcol
|
||||
|
||||
## dev ##############################################################################################
|
||||
FROM base as dev
|
||||
FROM base AS dev
|
||||
|
||||
COPY ./config.yaml /etc/otelcol-contrib/config.yaml
|
||||
|
||||
EXPOSE 4317 4318 13133
|
||||
|
||||
ENTRYPOINT ["/opampsupervisor"]
|
||||
CMD ["--config", "/etc/otel/supervisor.yaml"]
|
||||
|
||||
## prod #############################################################################################
|
||||
FROM base as prod
|
||||
FROM base AS prod
|
||||
|
||||
COPY ./config.yaml /etc/otelcol-contrib/config.yaml
|
||||
|
||||
EXPOSE 4317 4318 13133
|
||||
|
||||
ENTRYPOINT ["/opampsupervisor"]
|
||||
CMD ["--config", "/etc/otel/supervisor.yaml"]
|
||||
|
|
|
|||
|
|
@ -12,18 +12,19 @@ receivers:
|
|||
# Data sources: logs
|
||||
fluentforward:
|
||||
endpoint: '0.0.0.0:24225'
|
||||
# Configured via OpAMP w/ authentication
|
||||
# Data sources: traces, metrics, logs
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
include_metadata: true
|
||||
endpoint: '0.0.0.0:4317'
|
||||
http:
|
||||
cors:
|
||||
allowed_origins: ['*']
|
||||
allowed_headers: ['*']
|
||||
include_metadata: true
|
||||
endpoint: '0.0.0.0:4318'
|
||||
# otlp/hyperdx:
|
||||
# protocols:
|
||||
# grpc:
|
||||
# include_metadata: true
|
||||
# endpoint: '0.0.0.0:4317'
|
||||
# http:
|
||||
# cors:
|
||||
# allowed_origins: ['*']
|
||||
# allowed_headers: ['*']
|
||||
# include_metadata: true
|
||||
# endpoint: '0.0.0.0:4318'
|
||||
processors:
|
||||
transform:
|
||||
log_statements:
|
||||
|
|
@ -32,8 +33,10 @@ processors:
|
|||
statements:
|
||||
# JSON parsing: Extends log attributes with the fields from structured log body content, either as an OTEL map or
|
||||
# as a string containing JSON content.
|
||||
- set(log.cache, ExtractPatterns(log.body, "(?P<0>(\\{.*\\}))")) where IsString(log.body)
|
||||
- merge_maps(log.attributes, ParseJSON(log.cache["0"]), "upsert") where IsMap(log.cache)
|
||||
- set(log.cache, ExtractPatterns(log.body, "(?P<0>(\\{.*\\}))")) where
|
||||
IsString(log.body)
|
||||
- merge_maps(log.attributes, ParseJSON(log.cache["0"]), "upsert")
|
||||
where IsMap(log.cache)
|
||||
- flatten(log.attributes) where IsMap(log.cache)
|
||||
- merge_maps(log.attributes, log.body, "upsert") where IsMap(log.body)
|
||||
- context: log
|
||||
|
|
@ -42,24 +45,37 @@ processors:
|
|||
- severity_number == 0 and severity_text == ""
|
||||
statements:
|
||||
# Infer: extract the first log level keyword from the first 256 characters of the body
|
||||
- set(log.cache["substr"], log.body.string) where Len(log.body.string) < 256
|
||||
- set(log.cache["substr"], Substring(log.body.string, 0, 256)) where Len(log.body.string) >= 256
|
||||
- set(log.cache, ExtractPatterns(log.cache["substr"], "(?i)(?P<0>(alert|crit|emerg|fatal|error|err|warn|notice|debug|dbug|trace))"))
|
||||
- set(log.cache["substr"], log.body.string) where Len(log.body.string)
|
||||
< 256
|
||||
- set(log.cache["substr"], Substring(log.body.string, 0, 256)) where
|
||||
Len(log.body.string) >= 256
|
||||
- set(log.cache, ExtractPatterns(log.cache["substr"],
|
||||
"(?i)(?P<0>(alert|crit|emerg|fatal|error|err|warn|notice|debug|dbug|trace))"))
|
||||
# Infer: detect FATAL
|
||||
- set(log.severity_number, SEVERITY_NUMBER_FATAL) where IsMatch(log.cache["0"], "(?i)(alert|crit|emerg|fatal)")
|
||||
- set(log.severity_text, "fatal") where log.severity_number == SEVERITY_NUMBER_FATAL
|
||||
- set(log.severity_number, SEVERITY_NUMBER_FATAL) where
|
||||
IsMatch(log.cache["0"], "(?i)(alert|crit|emerg|fatal)")
|
||||
- set(log.severity_text, "fatal") where log.severity_number ==
|
||||
SEVERITY_NUMBER_FATAL
|
||||
# Infer: detect ERROR
|
||||
- set(log.severity_number, SEVERITY_NUMBER_ERROR) where IsMatch(log.cache["0"], "(?i)(error|err)")
|
||||
- set(log.severity_text, "error") where log.severity_number == SEVERITY_NUMBER_ERROR
|
||||
- set(log.severity_number, SEVERITY_NUMBER_ERROR) where
|
||||
IsMatch(log.cache["0"], "(?i)(error|err)")
|
||||
- set(log.severity_text, "error") where log.severity_number ==
|
||||
SEVERITY_NUMBER_ERROR
|
||||
# Infer: detect WARN
|
||||
- set(log.severity_number, SEVERITY_NUMBER_WARN) where IsMatch(log.cache["0"], "(?i)(warn|notice)")
|
||||
- set(log.severity_text, "warn") where log.severity_number == SEVERITY_NUMBER_WARN
|
||||
- set(log.severity_number, SEVERITY_NUMBER_WARN) where
|
||||
IsMatch(log.cache["0"], "(?i)(warn|notice)")
|
||||
- set(log.severity_text, "warn") where log.severity_number ==
|
||||
SEVERITY_NUMBER_WARN
|
||||
# Infer: detect DEBUG
|
||||
- set(log.severity_number, SEVERITY_NUMBER_DEBUG) where IsMatch(log.cache["0"], "(?i)(debug|dbug)")
|
||||
- set(log.severity_text, "debug") where log.severity_number == SEVERITY_NUMBER_DEBUG
|
||||
- set(log.severity_number, SEVERITY_NUMBER_DEBUG) where
|
||||
IsMatch(log.cache["0"], "(?i)(debug|dbug)")
|
||||
- set(log.severity_text, "debug") where log.severity_number ==
|
||||
SEVERITY_NUMBER_DEBUG
|
||||
# Infer: detect TRACE
|
||||
- set(log.severity_number, SEVERITY_NUMBER_TRACE) where IsMatch(log.cache["0"], "(?i)(trace)")
|
||||
- set(log.severity_text, "trace") where log.severity_number == SEVERITY_NUMBER_TRACE
|
||||
- set(log.severity_number, SEVERITY_NUMBER_TRACE) where
|
||||
IsMatch(log.cache["0"], "(?i)(trace)")
|
||||
- set(log.severity_text, "trace") where log.severity_number ==
|
||||
SEVERITY_NUMBER_TRACE
|
||||
# Infer: else
|
||||
- set(log.severity_text, "info") where log.severity_number == 0
|
||||
- set(log.severity_number, SEVERITY_NUMBER_INFO) where log.severity_number == 0
|
||||
|
|
@ -132,15 +148,15 @@ service:
|
|||
extensions: [health_check]
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
# receivers: [otlp/hyperdx]
|
||||
processors: [memory_limiter, batch]
|
||||
exporters: [clickhouse]
|
||||
metrics:
|
||||
receivers: [otlp, prometheus]
|
||||
# receivers: [otlp/hyperdx, prometheus]
|
||||
processors: [memory_limiter, batch]
|
||||
exporters: [clickhouse]
|
||||
logs/in:
|
||||
receivers: [otlp, fluentforward]
|
||||
# receivers: [otlp/hyperdx, fluentforward]
|
||||
exporters: [routing/logs]
|
||||
logs/out-default:
|
||||
receivers: [routing/logs]
|
||||
|
|
|
|||
26
docker/otel-collector/supervisor_docker.yaml
Normal file
26
docker/otel-collector/supervisor_docker.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/cmd/opampsupervisor/specification/README.md#supervisor-configuration
|
||||
server:
|
||||
endpoint: ${OPAMP_SERVER_URL}/v1/opamp
|
||||
tls:
|
||||
# Disable verification to test locally.
|
||||
# Don't do this in production.
|
||||
insecure_skip_verify: true
|
||||
# For more TLS settings see config/configtls.ClientConfig
|
||||
|
||||
capabilities:
|
||||
reports_effective_config: true
|
||||
reports_own_metrics: true
|
||||
reports_own_logs: true
|
||||
reports_own_traces: true
|
||||
reports_health: true
|
||||
accepts_remote_config: true
|
||||
reports_remote_config: true
|
||||
|
||||
agent:
|
||||
executable: /otelcontribcol
|
||||
config_files:
|
||||
- /etc/otelcol-contrib/config.yaml
|
||||
# passthrough_logs: true # enable to debug collector logs, can crash collector due to perf issues with this flag enabled
|
||||
|
||||
storage:
|
||||
directory: /etc/otel/supervisor-data/
|
||||
|
|
@ -33,6 +33,7 @@
|
|||
"app:lint": "nx run @hyperdx/app:ci:lint",
|
||||
"dev": "docker compose -f docker-compose.dev.yml up -d && yarn app:dev && docker compose -f docker-compose.dev.yml down",
|
||||
"dev:down": "docker compose -f docker-compose.dev.yml down",
|
||||
"dev:compose": "docker compose -f docker-compose.dev.yml",
|
||||
"lint": "npx nx run-many -t ci:lint",
|
||||
"version": "make version",
|
||||
"release": "npx changeset tag && npx changeset publish"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
HYPERDX_API_KEY="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
HYPERDX_API_PORT=8000
|
||||
HYPERDX_OPAMP_PORT=4320
|
||||
HYPERDX_APP_PORT=8080
|
||||
HYPERDX_LOG_LEVEL=debug
|
||||
EXPRESS_SESSION_SECRET="hyperdx is cool 👋"
|
||||
|
|
@ -15,6 +16,7 @@ NODE_ENV=development
|
|||
OTEL_SERVICE_NAME="hdx-oss-dev-api"
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
|
||||
PORT=${HYPERDX_API_PORT}
|
||||
OPAMP_PORT=${HYPERDX_OPAMP_PORT}
|
||||
REDIS_URL=redis://localhost:6379
|
||||
USAGE_STATS_ENABLED=false
|
||||
NODE_OPTIONS="--max-http-header-size=131072"
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ FRONTEND_URL=http://app:8080
|
|||
MONGO_URI=mongodb://localhost:29999/hyperdx-test
|
||||
NODE_ENV=test
|
||||
PORT=9000
|
||||
OPAMP_PORT=4320
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"@opentelemetry/host-metrics": "^0.35.5",
|
||||
"@opentelemetry/sdk-metrics": "^1.30.1",
|
||||
"@slack/webhook": "^6.1.0",
|
||||
"@types/node": "^22.15.18",
|
||||
"axios": "^1.6.2",
|
||||
"compression": "^1.7.4",
|
||||
"connect-mongo": "^4.6.0",
|
||||
|
|
@ -43,6 +44,7 @@
|
|||
"passport-local-mongoose": "^6.1.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"promised-handlebars": "^2.0.1",
|
||||
"protobufjs": "^7.5.2",
|
||||
"semver": "^7.5.2",
|
||||
"serialize-error": "^8.1.0",
|
||||
"sqlstring": "^2.3.3",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const MINER_API_URL = env.MINER_API_URL 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 OPAMP_PORT = Number.parseInt(env.OPAMP_PORT as string);
|
||||
export const USAGE_STATS_ENABLED = env.USAGE_STATS_ENABLED !== 'false';
|
||||
export const RUN_SCHEDULED_TASKS_EXTERNALLY =
|
||||
env.RUN_SCHEDULED_TASKS_EXTERNALLY === 'true';
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const LOCAL_APP_TEAM = {
|
|||
// Placeholder keys
|
||||
hookId: uuidv4(),
|
||||
apiKey: uuidv4(),
|
||||
collectorAuthenticationEnforced: false,
|
||||
toJSON() {
|
||||
return this;
|
||||
},
|
||||
|
|
@ -28,24 +29,30 @@ export async function isTeamExisting() {
|
|||
return teamCount > 0;
|
||||
}
|
||||
|
||||
export async function createTeam({ name }: { name: string }) {
|
||||
export async function createTeam({
|
||||
name,
|
||||
collectorAuthenticationEnforced = true,
|
||||
}: {
|
||||
name: string;
|
||||
collectorAuthenticationEnforced?: boolean;
|
||||
}) {
|
||||
if (await isTeamExisting()) {
|
||||
throw new Error('Team already exists');
|
||||
}
|
||||
|
||||
const team = new Team({ name });
|
||||
const team = new Team({ name, collectorAuthenticationEnforced });
|
||||
|
||||
await team.save();
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
export function getTeam(id: string | ObjectId, fields?: string[]) {
|
||||
export function getTeam(id?: string | ObjectId, fields?: string[]) {
|
||||
if (config.IS_LOCAL_APP_MODE) {
|
||||
return LOCAL_APP_TEAM;
|
||||
}
|
||||
|
||||
return Team.findById(id, fields);
|
||||
return Team.findOne({}, fields);
|
||||
}
|
||||
|
||||
export function getTeamByApiKey(apiKey: string) {
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ class MockServer extends Server {
|
|||
protected shouldHandleGracefulShutdown = false;
|
||||
|
||||
getHttpServer() {
|
||||
return this.httpServer;
|
||||
return this.appServer;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
|
|
@ -276,15 +276,21 @@ class MockServer extends Server {
|
|||
|
||||
stop() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.httpServer.close(err => {
|
||||
this.appServer.close(err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
super
|
||||
.shutdown()
|
||||
.then(() => resolve())
|
||||
.catch(err => reject(err));
|
||||
this.opampServer.close(err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
super
|
||||
.shutdown()
|
||||
.then(() => resolve())
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface ITeam {
|
|||
allowedAuthMethods?: 'password'[];
|
||||
apiKey: string;
|
||||
hookId: string;
|
||||
collectorAuthenticationEnforced: boolean;
|
||||
}
|
||||
|
||||
export default mongoose.model<ITeam>(
|
||||
|
|
@ -29,6 +30,10 @@ export default mongoose.model<ITeam>(
|
|||
return uuidv4();
|
||||
},
|
||||
},
|
||||
collectorAuthenticationEnforced: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
|
|
|||
11
packages/api/src/opamp/README.md
Normal file
11
packages/api/src/opamp/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
Implements an HTTP OpAMP server that serves configurations to supervised
|
||||
collectors.
|
||||
|
||||
Spec: https://github.com/open-telemetry/opamp-spec/tree/main
|
||||
|
||||
Workflow:
|
||||
|
||||
- Sup pings /v1/opamp with status
|
||||
- Server checks if configs should be updated
|
||||
- Return new config if current config is outdated
|
||||
- Config derived from team doc with ingestion api key
|
||||
31
packages/api/src/opamp/app.ts
Normal file
31
packages/api/src/opamp/app.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import express from 'express';
|
||||
|
||||
import { appErrorHandler } from '@/middleware/error';
|
||||
import { opampController } from '@/opamp/controllers/opampController';
|
||||
|
||||
// Create Express application
|
||||
const app = express();
|
||||
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// Special body parser setup for OpAMP
|
||||
app.use(
|
||||
'/v1/opamp',
|
||||
express.raw({
|
||||
type: 'application/x-protobuf',
|
||||
limit: '10mb',
|
||||
}),
|
||||
);
|
||||
|
||||
// OpAMP endpoint
|
||||
app.post('/v1/opamp', opampController.handleOpampMessage.bind(opampController));
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({ status: 'OK' });
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use(appErrorHandler);
|
||||
|
||||
export default app;
|
||||
203
packages/api/src/opamp/controllers/opampController.ts
Normal file
203
packages/api/src/opamp/controllers/opampController.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import { Request, Response } from 'express';
|
||||
|
||||
import { getTeam } from '@/controllers/team';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
import { agentService } from '../services/agentService';
|
||||
import {
|
||||
createRemoteConfig,
|
||||
decodeAgentToServer,
|
||||
encodeServerToAgent,
|
||||
serverCapabilities,
|
||||
} from '../utils/protobuf';
|
||||
|
||||
type CollectorConfig = {
|
||||
extensions: Record<string, any>;
|
||||
receivers: {
|
||||
'otlp/hyperdx'?: {
|
||||
protocols: {
|
||||
grpc: {
|
||||
endpoint: string;
|
||||
include_metadata: boolean;
|
||||
auth?: {
|
||||
authenticator: string;
|
||||
};
|
||||
};
|
||||
http: {
|
||||
endpoint: string;
|
||||
cors: {
|
||||
allowed_origins: string[];
|
||||
allowed_headers: string[];
|
||||
};
|
||||
include_metadata: boolean;
|
||||
auth?: {
|
||||
authenticator: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
nop?: null;
|
||||
};
|
||||
service: {
|
||||
extensions: string[];
|
||||
pipelines: {
|
||||
[key: string]: {
|
||||
receivers: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
export class OpampController {
|
||||
/**
|
||||
* Handle an OpAMP message from an agent
|
||||
*/
|
||||
public async handleOpampMessage(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
// Check content type
|
||||
const contentType = req.get('Content-Type');
|
||||
if (contentType !== 'application/x-protobuf') {
|
||||
res
|
||||
.status(415)
|
||||
.send(
|
||||
'Unsupported Media Type: Content-Type must be application/x-protobuf',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode the AgentToServer message
|
||||
const agentToServer = decodeAgentToServer(req.body);
|
||||
logger.debug('agentToServer', agentToServer);
|
||||
logger.debug(
|
||||
// @ts-ignore
|
||||
`Received message from agent: ${agentToServer.instanceUid?.toString(
|
||||
'hex',
|
||||
)}`,
|
||||
);
|
||||
|
||||
// Process the agent status
|
||||
const agent = agentService.processAgentStatus(agentToServer);
|
||||
|
||||
// Prepare the response
|
||||
const serverToAgent: any = {
|
||||
instanceUid: agent.instanceUid,
|
||||
capabilities: serverCapabilities,
|
||||
};
|
||||
|
||||
// Check if we should send a remote configuration
|
||||
if (agentService.agentAcceptsRemoteConfig(agent)) {
|
||||
const team = await getTeam();
|
||||
// This is later merged with otel-collector/config.yaml
|
||||
// we need to instantiate a valid config so the collector
|
||||
// can at least start up
|
||||
const NOP_CONFIG: CollectorConfig = {
|
||||
extensions: {},
|
||||
receivers: {
|
||||
nop: null,
|
||||
},
|
||||
service: {
|
||||
extensions: [],
|
||||
pipelines: {
|
||||
traces: {
|
||||
receivers: ['nop'],
|
||||
},
|
||||
metrics: {
|
||||
receivers: ['nop'],
|
||||
},
|
||||
'logs/in': {
|
||||
receivers: ['nop'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
let config = NOP_CONFIG;
|
||||
|
||||
// If team is not found, don't send a remoteConfig, we aren't ready
|
||||
// to collect telemetry yet
|
||||
if (team) {
|
||||
config = {
|
||||
extensions: {},
|
||||
receivers: {
|
||||
'otlp/hyperdx': {
|
||||
protocols: {
|
||||
grpc: {
|
||||
endpoint: '0.0.0.0:4317',
|
||||
include_metadata: true,
|
||||
},
|
||||
http: {
|
||||
endpoint: '0.0.0.0:4318',
|
||||
cors: {
|
||||
allowed_origins: ['*'],
|
||||
allowed_headers: ['*'],
|
||||
},
|
||||
include_metadata: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
service: {
|
||||
extensions: [],
|
||||
pipelines: {
|
||||
traces: {
|
||||
receivers: ['otlp/hyperdx'],
|
||||
},
|
||||
metrics: {
|
||||
receivers: ['otlp/hyperdx', 'prometheus'],
|
||||
},
|
||||
'logs/in': {
|
||||
receivers: ['otlp/hyperdx', 'fluentforward'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (team.collectorAuthenticationEnforced) {
|
||||
const ingestionKey = team.apiKey;
|
||||
|
||||
if (config.receivers['otlp/hyperdx'] == null) {
|
||||
// should never happen
|
||||
throw new Error('otlp/hyperdx receiver not found');
|
||||
}
|
||||
|
||||
config.extensions['bearertokenauth/hyperdx'] = {
|
||||
scheme: '',
|
||||
tokens: [ingestionKey],
|
||||
};
|
||||
config.receivers['otlp/hyperdx'].protocols.grpc.auth = {
|
||||
authenticator: 'bearertokenauth/hyperdx',
|
||||
};
|
||||
config.receivers['otlp/hyperdx'].protocols.http.auth = {
|
||||
authenticator: 'bearertokenauth/hyperdx',
|
||||
};
|
||||
config.service.extensions = ['bearertokenauth/hyperdx'];
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(config, null, 2));
|
||||
|
||||
const remoteConfig = createRemoteConfig(
|
||||
new Map([['config.json', Buffer.from(JSON.stringify(config))]]),
|
||||
'application/json',
|
||||
);
|
||||
|
||||
serverToAgent.remoteConfig = remoteConfig;
|
||||
logger.debug(
|
||||
`Sending remote config to agent: ${agent.instanceUid.toString(
|
||||
'hex',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Encode and send the response
|
||||
const encodedResponse = encodeServerToAgent(serverToAgent);
|
||||
|
||||
res.setHeader('Content-Type', 'application/x-protobuf');
|
||||
res.send(encodedResponse);
|
||||
} catch (error) {
|
||||
logger.error('Error handling OpAMP message:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const opampController = new OpampController();
|
||||
92
packages/api/src/opamp/models/agent.ts
Normal file
92
packages/api/src/opamp/models/agent.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
export interface AgentAttribute {
|
||||
key: string;
|
||||
value: {
|
||||
stringValue?: string;
|
||||
intValue?: number;
|
||||
doubleValue?: number;
|
||||
boolValue?: boolean;
|
||||
arrayValue?: AgentAttribute[];
|
||||
kvlistValue?: AgentAttribute[];
|
||||
bytesValue?: Buffer;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AgentDescription {
|
||||
identifyingAttributes: AgentAttribute[];
|
||||
nonIdentifyingAttributes: AgentAttribute[];
|
||||
}
|
||||
|
||||
export interface RemoteConfigStatus {
|
||||
lastRemoteConfigHash?: Buffer;
|
||||
status: 'UNSET' | 'APPLIED' | 'APPLYING' | 'FAILED';
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface EffectiveConfig {
|
||||
configMap: {
|
||||
[key: string]: {
|
||||
body: Buffer;
|
||||
contentType: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
instanceUid: Buffer;
|
||||
sequenceNum: number;
|
||||
agentDescription?: AgentDescription;
|
||||
capabilities: number;
|
||||
effectiveConfig?: EffectiveConfig;
|
||||
remoteConfigStatus?: RemoteConfigStatus;
|
||||
lastSeen: Date;
|
||||
currentConfigHash?: Buffer;
|
||||
}
|
||||
|
||||
// TODO: Evaluate if we need to store agent state here at all
|
||||
export class AgentStore {
|
||||
private agents: Map<string, Agent> = new Map();
|
||||
|
||||
/**
|
||||
* Add or update an agent in the store
|
||||
*/
|
||||
public upsertAgent(agent: Agent): void {
|
||||
const instanceUidStr = this.bufferToHex(agent.instanceUid);
|
||||
this.agents.set(instanceUidStr, {
|
||||
...agent,
|
||||
lastSeen: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an agent by its instance UID
|
||||
*/
|
||||
public getAgent(instanceUid: Buffer): Agent | undefined {
|
||||
const instanceUidStr = this.bufferToHex(instanceUid);
|
||||
return this.agents.get(instanceUidStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all agents in the store
|
||||
*/
|
||||
public getAllAgents(): Agent[] {
|
||||
return Array.from(this.agents.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an agent from the store
|
||||
*/
|
||||
public removeAgent(instanceUid: Buffer): boolean {
|
||||
const instanceUidStr = this.bufferToHex(instanceUid);
|
||||
return this.agents.delete(instanceUidStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Buffer to a hex string for use as a map key
|
||||
*/
|
||||
private bufferToHex(buffer: Buffer): string {
|
||||
return buffer.toString('hex');
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance of the agent store
|
||||
export const agentStore = new AgentStore();
|
||||
67
packages/api/src/opamp/proto/anyvalue.proto
Normal file
67
packages/api/src/opamp/proto/anyvalue.proto
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright 2019, OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file is copied and modified from https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/common/v1/common.proto
|
||||
// Modifications:
|
||||
// - Removal of unneeded InstrumentationLibrary and StringKeyValue messages.
|
||||
// - Change of go_package to reference a package in this repo.
|
||||
// - Removal of gogoproto usage.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package opamp.proto;
|
||||
|
||||
option go_package = "github.com/open-telemetry/opamp-go/protobufs";
|
||||
|
||||
// AnyValue is used to represent any type of attribute value. AnyValue may contain a
|
||||
// primitive value such as a string or integer or it may contain an arbitrary nested
|
||||
// object containing arrays, key-value lists and primitives.
|
||||
message AnyValue {
|
||||
// The value is one of the listed fields. It is valid for all values to be unspecified
|
||||
// in which case this AnyValue is considered to be "null".
|
||||
oneof value {
|
||||
string string_value = 1;
|
||||
bool bool_value = 2;
|
||||
int64 int_value = 3;
|
||||
double double_value = 4;
|
||||
ArrayValue array_value = 5;
|
||||
KeyValueList kvlist_value = 6;
|
||||
bytes bytes_value = 7;
|
||||
}
|
||||
}
|
||||
|
||||
// ArrayValue is a list of AnyValue messages. We need ArrayValue as a message
|
||||
// since oneof in AnyValue does not allow repeated fields.
|
||||
message ArrayValue {
|
||||
// Array of values. The array may be empty (contain 0 elements).
|
||||
repeated AnyValue values = 1;
|
||||
}
|
||||
|
||||
// KeyValueList is a list of KeyValue messages. We need KeyValueList as a message
|
||||
// since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need
|
||||
// a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to
|
||||
// avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches
|
||||
// are semantically equivalent.
|
||||
message KeyValueList {
|
||||
// A collection of key/value pairs of key-value pairs. The list may be empty (may
|
||||
// contain 0 elements).
|
||||
repeated KeyValue values = 1;
|
||||
}
|
||||
|
||||
// KeyValue is a key-value pair that is used to store Span attributes, Link
|
||||
// attributes, etc.
|
||||
message KeyValue {
|
||||
string key = 1;
|
||||
AnyValue value = 2;
|
||||
}
|
||||
1018
packages/api/src/opamp/proto/opamp.proto
Normal file
1018
packages/api/src/opamp/proto/opamp.proto
Normal file
File diff suppressed because it is too large
Load diff
96
packages/api/src/opamp/services/agentService.ts
Normal file
96
packages/api/src/opamp/services/agentService.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import logger from '@/utils/logger';
|
||||
|
||||
import { Agent, agentStore } from '../models/agent';
|
||||
|
||||
export class AgentService {
|
||||
/**
|
||||
* Process an agent status report
|
||||
*/
|
||||
public processAgentStatus(agentToServer: any): Agent {
|
||||
try {
|
||||
// Extract necessary fields from the message
|
||||
const {
|
||||
instanceUid,
|
||||
sequenceNum,
|
||||
agentDescription,
|
||||
capabilities,
|
||||
effectiveConfig,
|
||||
remoteConfigStatus,
|
||||
} = agentToServer;
|
||||
|
||||
// Get the existing agent or create a new one
|
||||
let agent = agentStore.getAgent(instanceUid);
|
||||
|
||||
if (!agent) {
|
||||
// New agent, create a new record
|
||||
agent = {
|
||||
instanceUid,
|
||||
sequenceNum,
|
||||
agentDescription,
|
||||
capabilities,
|
||||
effectiveConfig,
|
||||
remoteConfigStatus,
|
||||
lastSeen: new Date(),
|
||||
};
|
||||
} else {
|
||||
// Existing agent, update its record
|
||||
agent = {
|
||||
...agent,
|
||||
sequenceNum,
|
||||
lastSeen: new Date(),
|
||||
};
|
||||
|
||||
// Update optional fields if they exist in the message
|
||||
if (agentDescription) {
|
||||
agent.agentDescription = agentDescription;
|
||||
}
|
||||
|
||||
if (capabilities) {
|
||||
agent.capabilities = capabilities;
|
||||
}
|
||||
|
||||
if (effectiveConfig) {
|
||||
agent.effectiveConfig = effectiveConfig;
|
||||
}
|
||||
|
||||
if (remoteConfigStatus) {
|
||||
agent.remoteConfigStatus = remoteConfigStatus;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the agent in the store
|
||||
agentStore.upsertAgent(agent);
|
||||
|
||||
return agent;
|
||||
} catch (error) {
|
||||
logger.error('Error processing agent status:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an agent accepts remote configuration
|
||||
*/
|
||||
public agentAcceptsRemoteConfig(agent: Agent): boolean {
|
||||
// Check if the agent has the AcceptsRemoteConfig capability bit set
|
||||
// AcceptsRemoteConfig = 0x00000002
|
||||
return (agent.capabilities & 0x00000002) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an agent by its instance UID
|
||||
*/
|
||||
public getAgent(instanceUid: Buffer): Agent | undefined {
|
||||
return agentStore.getAgent(instanceUid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered agents
|
||||
*/
|
||||
public getAllAgents(): Agent[] {
|
||||
return agentStore.getAllAgents();
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance of the agent service
|
||||
export const agentService = new AgentService();
|
||||
142
packages/api/src/opamp/utils/protobuf.ts
Normal file
142
packages/api/src/opamp/utils/protobuf.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { createHash } from 'crypto';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as protobuf from 'protobufjs';
|
||||
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
// Define the root path of the proto file
|
||||
const PROTO_PATH = path.resolve(__dirname, '../proto/opamp.proto');
|
||||
|
||||
// Load the OpAMP proto definition
|
||||
let root: protobuf.Root;
|
||||
try {
|
||||
if (!fs.existsSync(PROTO_PATH)) {
|
||||
throw new Error(`Proto file not found at ${PROTO_PATH}`);
|
||||
}
|
||||
root = protobuf.loadSync(PROTO_PATH);
|
||||
logger.debug('OpAMP proto definition loaded successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to load OpAMP proto definition:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Get message types
|
||||
const AgentToServer = root.lookupType('opamp.AgentToServer');
|
||||
const ServerToAgent = root.lookupType('opamp.ServerToAgent');
|
||||
const AgentRemoteConfig = root.lookupType('opamp.AgentRemoteConfig');
|
||||
const AgentConfigMap = root.lookupType('opamp.AgentConfigMap');
|
||||
const AgentConfigFile = root.lookupType('opamp.AgentConfigFile');
|
||||
const ServerCapabilities = root.lookupEnum('opamp.ServerCapabilities');
|
||||
|
||||
// Define the server capabilities
|
||||
const serverCapabilities =
|
||||
ServerCapabilities.values.AcceptsStatus |
|
||||
ServerCapabilities.values.OffersRemoteConfig |
|
||||
ServerCapabilities.values.AcceptsEffectiveConfig;
|
||||
|
||||
/**
|
||||
* Decode an AgentToServer message from binary data
|
||||
*/
|
||||
export function decodeAgentToServer(data: Buffer): protobuf.Message {
|
||||
try {
|
||||
return AgentToServer.decode(data);
|
||||
} catch (error) {
|
||||
logger.error('Failed to decode AgentToServer message:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a ServerToAgent message to binary data
|
||||
*/
|
||||
export function encodeServerToAgent(message: any): Buffer {
|
||||
try {
|
||||
// Verify the message
|
||||
const error = ServerToAgent.verify(message);
|
||||
if (error) {
|
||||
throw new Error(`Invalid ServerToAgent message: ${error}`);
|
||||
}
|
||||
|
||||
// Create a message instance
|
||||
const serverToAgent = ServerToAgent.create(message);
|
||||
|
||||
// Encode the message
|
||||
return Buffer.from(ServerToAgent.encode(serverToAgent).finish());
|
||||
} catch (error) {
|
||||
logger.error('Failed to encode ServerToAgent message:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a remote configuration message
|
||||
*/
|
||||
export function createRemoteConfig(
|
||||
configFiles: Map<string, Buffer>,
|
||||
configType: string = 'text/yaml',
|
||||
): any {
|
||||
try {
|
||||
// Convert the configFiles map to the format expected by AgentConfigMap
|
||||
const configMap: { [key: string]: any } = {};
|
||||
|
||||
configFiles.forEach((content, filename) => {
|
||||
configMap[filename] = {
|
||||
body: content,
|
||||
contentType: configType,
|
||||
};
|
||||
});
|
||||
|
||||
// Create the AgentConfigMap message
|
||||
const agentConfigMap = {
|
||||
configMap: configMap,
|
||||
};
|
||||
|
||||
// Calculate the config hash
|
||||
const configHash = calculateConfigHash(configFiles);
|
||||
|
||||
// Create the AgentRemoteConfig message
|
||||
return {
|
||||
config: agentConfigMap,
|
||||
configHash: configHash,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to create remote config message:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a hash for the configuration files
|
||||
*/
|
||||
function calculateConfigHash(configFiles: Map<string, Buffer>): Buffer {
|
||||
try {
|
||||
const hash = createHash('sha256');
|
||||
|
||||
// Sort keys to ensure consistent hashing
|
||||
const sortedKeys = Array.from(configFiles.keys()).sort();
|
||||
|
||||
for (const key of sortedKeys) {
|
||||
const content = configFiles.get(key);
|
||||
if (content) {
|
||||
hash.update(key);
|
||||
hash.update(content);
|
||||
}
|
||||
}
|
||||
|
||||
return hash.digest();
|
||||
} catch (error) {
|
||||
logger.error('Failed to calculate config hash:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
AgentConfigFile,
|
||||
AgentConfigMap,
|
||||
AgentRemoteConfig,
|
||||
AgentToServer,
|
||||
root,
|
||||
serverCapabilities,
|
||||
ServerToAgent,
|
||||
};
|
||||
|
|
@ -96,6 +96,7 @@ router.post(
|
|||
|
||||
const team = await createTeam({
|
||||
name: `${email}'s Team`,
|
||||
collectorAuthenticationEnforced: true,
|
||||
});
|
||||
user.team = team._id;
|
||||
user.name = email;
|
||||
|
|
|
|||
|
|
@ -5,17 +5,23 @@ import { serializeError } from 'serialize-error';
|
|||
import app from '@/api-app';
|
||||
import * as config from '@/config';
|
||||
import { connectDB, mongooseConnection } from '@/models';
|
||||
import opampApp from '@/opamp/app';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export default class Server {
|
||||
protected shouldHandleGracefulShutdown = true;
|
||||
|
||||
protected httpServer!: http.Server;
|
||||
protected appServer!: http.Server;
|
||||
protected opampServer!: http.Server;
|
||||
|
||||
private createServer() {
|
||||
private createAppServer() {
|
||||
return http.createServer(app);
|
||||
}
|
||||
|
||||
private createOpampServer() {
|
||||
return http.createServer(opampApp);
|
||||
}
|
||||
|
||||
protected async shutdown(signal?: string) {
|
||||
let hasError = false;
|
||||
logger.info('Closing all db clients...');
|
||||
|
|
@ -36,29 +42,41 @@ export default class Server {
|
|||
}
|
||||
|
||||
async start() {
|
||||
this.httpServer = this.createServer();
|
||||
this.httpServer.keepAliveTimeout = 61000; // Ensure all inactive connections are terminated by the ALB, by setting this a few seconds higher than the ALB idle timeout
|
||||
this.httpServer.headersTimeout = 62000; // Ensure the headersTimeout is set higher than the keepAliveTimeout due to this nodejs regression bug: https://github.com/nodejs/node/issues/27363
|
||||
this.appServer = this.createAppServer();
|
||||
this.appServer.keepAliveTimeout = 61000; // Ensure all inactive connections are terminated by the ALB, by setting this a few seconds higher than the ALB idle timeout
|
||||
this.appServer.headersTimeout = 62000; // Ensure the headersTimeout is set higher than the keepAliveTimeout due to this nodejs regression bug: https://github.com/nodejs/node/issues/27363
|
||||
|
||||
this.httpServer.listen(config.PORT, () => {
|
||||
this.opampServer = this.createOpampServer();
|
||||
this.opampServer.keepAliveTimeout = 61000;
|
||||
this.opampServer.headersTimeout = 62000;
|
||||
|
||||
this.appServer.listen(config.PORT, () => {
|
||||
logger.info(
|
||||
`Server listening on port ${config.PORT}, NODE_ENV=${process.env.NODE_ENV}`,
|
||||
`API Server listening on port ${config.PORT}, NODE_ENV=${process.env.NODE_ENV}`,
|
||||
);
|
||||
});
|
||||
|
||||
this.opampServer.listen(config.OPAMP_PORT, () => {
|
||||
logger.info(
|
||||
`OpAMP Server listening on port ${config.OPAMP_PORT}, NODE_ENV=${process.env.NODE_ENV}`,
|
||||
);
|
||||
});
|
||||
|
||||
if (this.shouldHandleGracefulShutdown) {
|
||||
gracefulShutdown(this.httpServer, {
|
||||
signals: 'SIGINT SIGTERM',
|
||||
timeout: 10000, // 10 secs
|
||||
development: config.IS_DEV,
|
||||
forceExit: true, // triggers process.exit() at the end of shutdown process
|
||||
preShutdown: async () => {
|
||||
// needed operation before httpConnections are shutted down
|
||||
},
|
||||
onShutdown: this.shutdown,
|
||||
finally: () => {
|
||||
logger.info('Server gracefully shut down...');
|
||||
}, // finally function (sync) - e.g. for logging
|
||||
[this.appServer, this.opampServer].forEach(server => {
|
||||
gracefulShutdown(server, {
|
||||
signals: 'SIGINT SIGTERM',
|
||||
timeout: 10000, // 10 secs
|
||||
development: config.IS_DEV,
|
||||
forceExit: true, // triggers process.exit() at the end of shutdown process
|
||||
preShutdown: async () => {
|
||||
// needed operation before httpConnections are shutted down
|
||||
},
|
||||
onShutdown: this.shutdown,
|
||||
finally: () => {
|
||||
logger.info('Server gracefully shut down...');
|
||||
}, // finally function (sync) - e.g. for logging
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# OpenTelemetry Collector Smoke Tests
|
||||
|
||||
This directory contains smoke tests for validating the OpenTelemetry Collector functionality in HyperDX.
|
||||
This directory contains smoke tests for validating the OpenTelemetry Collector
|
||||
functionality in HyperDX.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
|
@ -9,7 +10,10 @@ Before running the tests, ensure you have the following tools installed:
|
|||
- [Bats](https://github.com/bats-core/bats-core) - Bash Automated Testing System
|
||||
- [Docker](https://www.docker.com/) and Docker Compose
|
||||
- [curl](https://curl.se/) - Command line tool for transferring data
|
||||
- [ClickHouse client](https://clickhouse.com/docs/en/integrations/sql-clients/clickhouse-client) - Command-line client for ClickHouse
|
||||
- [ClickHouse client](https://clickhouse.com/docs/en/integrations/sql-clients/clickhouse-client) -
|
||||
Command-line client for ClickHouse
|
||||
- Make sure to run `clickhouse install` to install the ClickHouse client after
|
||||
downloading ClickHouse
|
||||
|
||||
## Running the Tests
|
||||
|
||||
|
|
@ -29,12 +33,15 @@ bats hdx-1453-auto-parse-json.bats
|
|||
## Test Structure
|
||||
|
||||
- `*.bats` - Test files written in Bats
|
||||
- `setup_suite.bash` - Contains global setup_suite and teardown_suite functions that run once for the entire test suite
|
||||
- `setup_suite.bash` - Contains global setup_suite and teardown_suite functions
|
||||
that run once for the entire test suite
|
||||
- `data/` - Test data used by the tests
|
||||
- `test_helpers/` - Utility functions for the tests
|
||||
- `docker-compose.yaml` - Docker Compose configuration for the test environment
|
||||
|
||||
The test suite uses Bats' `setup_suite` and `teardown_suite` hooks to initialize the environment only once, regardless of how many test files are run. This optimizes test execution by:
|
||||
The test suite uses Bats' `setup_suite` and `teardown_suite` hooks to initialize
|
||||
the environment only once, regardless of how many test files are run. This
|
||||
optimizes test execution by:
|
||||
|
||||
1. Validating the environment once
|
||||
2. Starting Docker containers once at the beginning of the test suite
|
||||
|
|
@ -42,7 +49,9 @@ The test suite uses Bats' `setup_suite` and `teardown_suite` hooks to initialize
|
|||
|
||||
## Debugging
|
||||
|
||||
If you need to debug the tests, you can set the `SKIP_CLEANUP` environment variable to prevent the Docker containers from being torn down after the tests complete:
|
||||
If you need to debug the tests, you can set the `SKIP_CLEANUP` environment
|
||||
variable to prevent the Docker containers from being torn down after the tests
|
||||
complete:
|
||||
|
||||
```bash
|
||||
SKIP_CLEANUP=1 bats *.bats
|
||||
|
|
@ -54,7 +63,9 @@ or
|
|||
SKIP_CLEANUP=true bats *.bats
|
||||
```
|
||||
|
||||
With `SKIP_CLEANUP` enabled, the test containers will remain running after the tests complete, allowing you to inspect logs, connect to the containers, and debug issues.
|
||||
With `SKIP_CLEANUP` enabled, the test containers will remain running after the
|
||||
tests complete, allowing you to inspect logs, connect to the containers, and
|
||||
debug issues.
|
||||
|
||||
To manually clean up the containers after debugging:
|
||||
|
||||
|
|
|
|||
|
|
@ -11,15 +11,23 @@ services:
|
|||
networks:
|
||||
- internal
|
||||
healthcheck:
|
||||
test: wget --no-verbose --tries=1 http://127.0.0.1:8123/ping || exit 1
|
||||
test:
|
||||
wget -O /dev/null --no-verbose --tries=1 http://127.0.0.1:8123/ping ||
|
||||
exit 1
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
otel-collector:
|
||||
extends:
|
||||
file: ../../docker-compose.ci.yml
|
||||
service: otel-collector
|
||||
image: otel/opentelemetry-collector-contrib:0.126.0
|
||||
volumes:
|
||||
- ../../docker/otel-collector/config.yaml:/etc/otelcol-contrib/config.yaml
|
||||
- ./receiver-config.yaml:/etc/otelcol-contrib/receiver-config.yaml
|
||||
command:
|
||||
[
|
||||
'--config=/etc/otelcol-contrib/receiver-config.yaml',
|
||||
'--config=/etc/otelcol-contrib/config.yaml',
|
||||
]
|
||||
environment:
|
||||
- CLICKHOUSE_ENDPOINT=tcp://ch-server:9000?dial_timeout=10s
|
||||
- CLICKHOUSE_PROMETHEUS_METRICS_ENDPOINT=ch-server:9363
|
||||
|
|
|
|||
42
smoke-tests/otel-collector/receiver-config.yaml
Normal file
42
smoke-tests/otel-collector/receiver-config.yaml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Usually added via OpAMP
|
||||
receivers:
|
||||
# Troubleshooting
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
- job_name: 'otelcol'
|
||||
scrape_interval: 30s
|
||||
static_configs:
|
||||
- targets:
|
||||
- '0.0.0.0:8888'
|
||||
- ${env:CLICKHOUSE_PROMETHEUS_METRICS_ENDPOINT}
|
||||
# Data sources: logs
|
||||
fluentforward:
|
||||
endpoint: '0.0.0.0:24225'
|
||||
# Configured via OpAMP w/ authentication
|
||||
# Data sources: traces, metrics, logs
|
||||
otlp/hyperdx:
|
||||
protocols:
|
||||
grpc:
|
||||
include_metadata: true
|
||||
endpoint: '0.0.0.0:4317'
|
||||
http:
|
||||
cors:
|
||||
allowed_origins: ['*']
|
||||
allowed_headers: ['*']
|
||||
include_metadata: true
|
||||
endpoint: '0.0.0.0:4318'
|
||||
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers:
|
||||
- otlp/hyperdx
|
||||
metrics:
|
||||
receivers:
|
||||
- otlp/hyperdx
|
||||
- prometheus
|
||||
logs/in:
|
||||
receivers:
|
||||
- otlp/hyperdx
|
||||
- fluentforward
|
||||
|
|
@ -13,7 +13,7 @@ validate_env() {
|
|||
|
||||
# Check if clickhouse-client is installed
|
||||
if ! command -v clickhouse-client &> /dev/null; then
|
||||
echo "❌ Error: clickhouse-client is not installed. Please install clickhouse-client to continue." >&3
|
||||
echo "❌ Error: clickhouse-client is not installed. Please install clickhouse-client to continue. (Did you run `clickhouse install` yet?)" >&3
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
38
yarn.lock
38
yarn.lock
|
|
@ -4277,6 +4277,7 @@ __metadata:
|
|||
"@types/lodash": "npm:^4.14.198"
|
||||
"@types/minimist": "npm:^1.2.2"
|
||||
"@types/ms": "npm:^0.7.31"
|
||||
"@types/node": "npm:^22.15.18"
|
||||
"@types/object-hash": "npm:^2.2.1"
|
||||
"@types/passport-http-bearer": "npm:^1.0.37"
|
||||
"@types/passport-local": "npm:^1.0.34"
|
||||
|
|
@ -4317,6 +4318,7 @@ __metadata:
|
|||
passport-local-mongoose: "npm:^6.1.0"
|
||||
pluralize: "npm:^8.0.0"
|
||||
promised-handlebars: "npm:^2.0.1"
|
||||
protobufjs: "npm:^7.5.2"
|
||||
rimraf: "npm:^4.4.1"
|
||||
semver: "npm:^7.5.2"
|
||||
serialize-error: "npm:^8.1.0"
|
||||
|
|
@ -10061,6 +10063,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^22.15.18":
|
||||
version: 22.15.18
|
||||
resolution: "@types/node@npm:22.15.18"
|
||||
dependencies:
|
||||
undici-types: "npm:~6.21.0"
|
||||
checksum: 10c0/e23178c568e2dc6b93b6aa3b8dfb45f9556e527918c947fe7406a4c92d2184c7396558912400c3b1b8d0fa952ec63819aca2b8e4d3545455fc6f1e9623e09ca6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/normalize-package-data@npm:^2.4.0":
|
||||
version: 2.4.1
|
||||
resolution: "@types/normalize-package-data@npm:2.4.1"
|
||||
|
|
@ -23424,6 +23435,26 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"protobufjs@npm:^7.5.2":
|
||||
version: 7.5.2
|
||||
resolution: "protobufjs@npm:7.5.2"
|
||||
dependencies:
|
||||
"@protobufjs/aspromise": "npm:^1.1.2"
|
||||
"@protobufjs/base64": "npm:^1.1.2"
|
||||
"@protobufjs/codegen": "npm:^2.0.4"
|
||||
"@protobufjs/eventemitter": "npm:^1.1.0"
|
||||
"@protobufjs/fetch": "npm:^1.1.0"
|
||||
"@protobufjs/float": "npm:^1.0.2"
|
||||
"@protobufjs/inquire": "npm:^1.1.0"
|
||||
"@protobufjs/path": "npm:^1.1.2"
|
||||
"@protobufjs/pool": "npm:^1.1.0"
|
||||
"@protobufjs/utf8": "npm:^1.1.0"
|
||||
"@types/node": "npm:>=13.7.0"
|
||||
long: "npm:^5.0.0"
|
||||
checksum: 10c0/c4ac6298280dfe6116e7a1b722310de807037b1abed816db2af2a595ddc9dc6160447eebd4ad8e2968d41f0f85375f62e893adfe760af1a943d195931c7bb875
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"protobufjs@npm:~6.11.2":
|
||||
version: 6.11.4
|
||||
resolution: "protobufjs@npm:6.11.4"
|
||||
|
|
@ -27718,6 +27749,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici-types@npm:~6.21.0":
|
||||
version: 6.21.0
|
||||
resolution: "undici-types@npm:6.21.0"
|
||||
checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unicode-canonical-property-names-ecmascript@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0"
|
||||
|
|
|
|||
Loading…
Reference in a new issue