feat: enhance log management and organization handling

- Updated API link conditions to allow skipping batch processing based on context.
- Removed unused VECTOR_RESYNC_CRON_JOB constant from server constants.
- Introduced logProviderIds field in server and webServerSettings schemas to manage log providers.
- Added organization service to handle organization deletions and vector agent removals.
- Enhanced server service to find servers with log management enabled and update log providers.
- Updated vector setup to sync vector configurations based on organization and server log provider IDs.
- Implemented Docker labels for resources to improve tracking and management.
- Refactored vector agent setup and removal processes to streamline operations.
This commit is contained in:
Mauricio Siu
2026-09-24 01:23:20 -06:00
parent 9e8c5cde31
commit ae0cdc6456
31 changed files with 10149 additions and 1697 deletions
-2
View File
@@ -13,8 +13,6 @@ export const DOKPLOY_DOCKER_PORT = process.env.DOKPLOY_DOCKER_PORT
export const CLEANUP_CRON_JOB = "50 23 * * *";
export const VECTOR_RESYNC_CRON_JOB = "*/15 * * * *";
// Body size limits for the OpenAPI catch-all route (pages/api/[...trpc].ts).
const parseByteSize = (envVar: string, fallback: number): number => {
const raw = process.env[envVar];
+5 -2
View File
@@ -1,4 +1,4 @@
import { relations } from "drizzle-orm";
import { relations, sql } from "drizzle-orm";
import {
boolean,
integer,
@@ -42,7 +42,10 @@ export const server = pgTable("server", {
.notNull()
.$defaultFn(() => generateAppName("server")),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
enableLogManagement: boolean("enableLogManagement").notNull().default(false),
logProviderIds: text("logProviderIds")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
buildsConcurrency: integer("buildsConcurrency").notNull().default(1),
createdAt: text("createdAt").notNull(),
organizationId: text("organizationId")
@@ -1,4 +1,4 @@
import { relations } from "drizzle-orm";
import { relations, sql } from "drizzle-orm";
import {
boolean,
integer,
@@ -27,7 +27,10 @@ export const webServerSettings = pgTable("webServerSettings", {
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
enableLogManagement: boolean("enableLogManagement").notNull().default(false),
logProviderIds: text("logProviderIds")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
logManagementOrganizationId: text("logManagementOrganizationId").references(
() => organization.id,
{ onDelete: "set null" },
+1
View File
@@ -36,6 +36,7 @@ export * from "./services/mount";
export * from "./services/mysql";
export * from "./services/network";
export * from "./services/notification";
export * from "./services/organization";
export * from "./services/overview";
export * from "./services/patch";
export * from "./services/patch-repo";
@@ -117,6 +117,7 @@ export const elasticsearchAdapter: LogProviderAdapter = {
type: "elasticsearch",
inputs: [inputId],
endpoints: [normalizeEndpointUrl(config.endpoint ?? "")],
api_version: "v8",
...(typeof index === "string" && index.length > 0
? { bulk: { index } }
: {}),
@@ -0,0 +1,26 @@
import { db } from "@dokploy/server/db";
import { organization } from "@dokploy/server/db/schema";
import { findServersWithLogManagementEnabled } from "@dokploy/server/services/server";
import { removeVectorAgent } from "@dokploy/server/setup/vector-setup";
import { eq } from "drizzle-orm";
const removeVectorAgents = async (organizationId: string) => {
const servers = await findServersWithLogManagementEnabled(organizationId);
await Promise.all(
servers.map((server) =>
removeVectorAgent(server.serverId).catch((error) => {
console.error(
`[Vector] Failed to remove the agent on server ${server.serverId} before deleting organization ${organizationId}:`,
error,
);
}),
),
);
};
export const deleteOrganization = async (organizationId: string) => {
await removeVectorAgents(organizationId);
return await db
.delete(organization)
.where(eq(organization.id, organizationId));
};
+29 -5
View File
@@ -7,7 +7,7 @@ import {
} from "@dokploy/server/db/schema";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";
import { and, desc, eq, isNotNull, sql } from "drizzle-orm";
import type { z } from "zod";
export type Server = typeof server.$inferSelect;
@@ -93,19 +93,43 @@ export const findServersWithLogManagementEnabled = async (
return await db.query.server.findMany({
where: and(
eq(server.organizationId, organizationId),
eq(server.enableLogManagement, true),
sql`array_length(${server.logProviderIds}, 1) > 0`,
),
columns: { serverId: true },
});
};
export const findAllServersWithLogManagementEnabled = async () => {
export const findServersByOrganizationForLogManagement = async (
organizationId: string,
) => {
return await db.query.server.findMany({
where: eq(server.enableLogManagement, true),
columns: { serverId: true, organizationId: true },
where: and(
eq(server.organizationId, organizationId),
isNotNull(server.sshKeyId),
eq(server.serverType, "deploy"),
),
columns: {
serverId: true,
name: true,
ipAddress: true,
logProviderIds: true,
},
orderBy: [desc(server.createdAt)],
});
};
export const updateServerLogProviders = async (
serverId: string,
logProviderIds: string[],
) => {
const [updated] = await db
.update(server)
.set({ logProviderIds })
.where(eq(server.serverId, serverId))
.returning();
return updated ?? null;
};
export const deleteServer = async (serverId: string) => {
const currentServer = await db
.delete(server)
@@ -45,7 +45,7 @@ export const updateWebServerSettings = async (
export const claimWebServerLogManagement = async (
organizationId: string,
enableLogManagement: boolean,
logProviderIds: string[],
) => {
const current = await getWebServerSettings();
if (!current) {
@@ -55,8 +55,9 @@ export const claimWebServerLogManagement = async (
const [updated] = await db
.update(webServerSettings)
.set({
enableLogManagement,
logManagementOrganizationId: enableLogManagement ? organizationId : null,
logProviderIds,
logManagementOrganizationId:
logProviderIds.length > 0 ? organizationId : null,
updatedAt: new Date(),
})
.where(
+205 -451
View File
@@ -1,33 +1,24 @@
import {
chmodSync as fsChmodSync,
mkdirSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import { dbUrl } from "@dokploy/server/db/constants";
import { projects } from "@dokploy/server/db/schema";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
import {
findEnabledLogProvidersByOrganization,
hasEnabledLogProvider,
type LogProvider,
toRuntimeConfig,
} from "@dokploy/server/services/log-management/service";
import { findServerById } from "@dokploy/server/services/server";
import {
findServerById,
findServersByOrganizationForLogManagement,
} from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { pullImage, pullRemoteImage } from "@dokploy/server/utils/docker/utils";
import { encodeBase64 } from "@dokploy/server/utils/docker/utils";
import { ExecError } from "@dokploy/server/utils/process/ExecError";
import {
execAsync,
execAsyncRemote,
writeFileRemote,
} from "@dokploy/server/utils/process/execAsync";
import { getRemoteDocker } from "@dokploy/server/utils/servers/remote-docker";
import type { CreateServiceOptions } from "dockerode";
import { eq } from "drizzle-orm";
import postgres from "postgres";
import { nanoid } from "nanoid";
import { quote } from "shell-quote";
import { stringify } from "yaml";
import { getLogProviderAdapter } from "../services/log-management/providers/registry";
import type {
@@ -41,124 +32,17 @@ const VECTOR_CONFIG_CONTAINER_DIR = "/etc/vector";
const VECTOR_CONFIG_CONTAINER_PATH = `${VECTOR_CONFIG_CONTAINER_DIR}/vector.yaml`;
const VECTOR_DATA_DIR_CONTAINER = "/var/lib/vector";
interface AppNameLookupEntry {
projectId: string;
projectName: string;
environmentId: string;
environmentName: string;
applicationId: string;
applicationName: string;
}
const escapeVrlString = (value: string): string =>
value
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
// biome-ignore lint/suspicious/noControlCharactersInRegex: replacing them is the point
.replace(/[\u0000-\u001f\u007f]/g, " ");
const buildAppNameLookup = async (
organizationId: string,
): Promise<Record<string, AppNameLookupEntry>> => {
const projectRows = await db.query.projects.findMany({
where: eq(projects.organizationId, organizationId),
columns: { projectId: true, name: true },
with: {
environments: {
columns: { environmentId: true, name: true },
with: {
applications: {
columns: { applicationId: true, appName: true, name: true },
},
compose: {
columns: { composeId: true, appName: true, name: true },
},
},
},
},
});
const lookup: Record<string, AppNameLookupEntry> = {};
for (const project of projectRows) {
for (const environment of project.environments) {
for (const application of environment.applications) {
lookup[application.appName] = {
projectId: project.projectId,
projectName: project.name,
environmentId: environment.environmentId,
environmentName: environment.name,
applicationId: application.applicationId,
applicationName: application.name,
};
}
for (const composeService of environment.compose) {
lookup[composeService.appName] = {
projectId: project.projectId,
projectName: project.name,
environmentId: environment.environmentId,
environmentName: environment.name,
applicationId: composeService.composeId,
applicationName: composeService.name,
};
}
}
}
return lookup;
};
const buildScopeTransformSource = (
organizationId: string,
lookup: Record<string, AppNameLookupEntry>,
): string => {
const lines: string[] = [
`.dokploy_organization = "${escapeVrlString(organizationId)}"`,
'.dokploy_project = ""',
'.dokploy_project_id = ""',
'.dokploy_environment = ""',
'.dokploy_environment_id = ""',
'.dokploy_application = ""',
'.dokploy_application_id = ""',
];
const entries = Object.entries(lookup);
if (entries.length === 0) {
return lines.join("\n");
}
lines.push('app_name = .label."com.docker.compose.project"');
lines.push("if app_name == null {");
lines.push(' app_name = .label."com.docker.stack.namespace"');
lines.push("}");
lines.push("if app_name == null {");
lines.push(' app_name = .label."com.docker.swarm.service.name"');
lines.push("}");
lines.push("if app_name != null {");
entries.forEach(([appName, entry], index) => {
const branch = index === 0 ? " if" : " } else if";
lines.push(`${branch} app_name == "${escapeVrlString(appName)}" {`);
lines.push(
` .dokploy_project = "${escapeVrlString(entry.projectName)}"`,
);
lines.push(
` .dokploy_project_id = "${escapeVrlString(entry.projectId)}"`,
);
lines.push(
` .dokploy_environment = "${escapeVrlString(entry.environmentName)}"`,
);
lines.push(
` .dokploy_environment_id = "${escapeVrlString(entry.environmentId)}"`,
);
lines.push(
` .dokploy_application = "${escapeVrlString(entry.applicationName)}"`,
);
lines.push(
` .dokploy_application_id = "${escapeVrlString(entry.applicationId)}"`,
);
});
lines.push(" }");
lines.push("}");
return lines.join("\n");
};
const buildScopeTransformSource = (): string =>
[
'.dokploy_organization = .label."dokploy.organization.id" || ""',
'.dokploy_project = .label."dokploy.project" || ""',
'.dokploy_project_id = .label."dokploy.project.id" || ""',
'.dokploy_environment = .label."dokploy.environment" || ""',
'.dokploy_environment_id = .label."dokploy.environment.id" || ""',
'.dokploy_application = .label."dokploy.application" || ""',
'.dokploy_application_id = .label."dokploy.application.id" || ""',
'.dokploy_service = .label."dokploy.service" || ""',
].join("\n");
const buildSinksAndTransforms = (
providers: Array<LogProvider>,
@@ -206,28 +90,22 @@ const buildSinksAndTransforms = (
return { sinks, transforms };
};
export interface VectorOrgData {
providers: Array<LogProvider>;
lookup: Record<string, AppNameLookupEntry>;
}
export const loadVectorOrgData = async (
const loadEnabledLogProviders = async (
organizationId: string,
): Promise<VectorOrgData> => {
const [providers, lookup] = await Promise.all([
findEnabledLogProvidersByOrganization(organizationId),
buildAppNameLookup(organizationId),
]);
return { providers, lookup };
logProviderIds?: string[],
): Promise<Array<LogProvider>> => {
const providers = await findEnabledLogProvidersByOrganization(organizationId);
return logProviderIds
? providers.filter((provider) =>
logProviderIds.includes(provider.logProviderId),
)
: providers;
};
export const buildVectorConfigYaml = async (
organizationId: string,
preloaded?: VectorOrgData,
providers: Array<LogProvider>,
options?: { dropUnmatched?: boolean },
) => {
const { providers, lookup } =
preloaded ?? (await loadVectorOrgData(organizationId));
const baseTransformId = options?.dropUnmatched
? "dokploy_scope_local_only"
: "dokploy_scope";
@@ -248,7 +126,7 @@ export const buildVectorConfigYaml = async (
dokploy_scope: {
type: "remap",
inputs: ["docker_logs_source"],
source: buildScopeTransformSource(organizationId, lookup),
source: buildScopeTransformSource(),
},
...(options?.dropUnmatched
? {
@@ -290,138 +168,89 @@ export const redactSecrets = (text: string, secrets: string[]): string => {
return redacted;
};
const VECTOR_CONFIG_LOCK_NAMESPACE = 87_234_501;
const hashServerIdToInt32 = (serverId: string): number => {
let hash = 0;
for (let i = 0; i < serverId.length; i++) {
hash = (Math.imul(31, hash) + serverId.charCodeAt(i)) | 0;
}
return hash;
};
let lockPool: ReturnType<typeof postgres> | undefined;
const getLockPool = () => {
if (!lockPool) {
lockPool = postgres(dbUrl, { max: 10 });
}
return lockPool;
};
const LOCK_RETRY_ATTEMPTS = 3;
const LOCK_RETRY_DELAY_MS = 1000;
const configWriteLocks = new Map<string, Promise<void>>();
export const withConfigWriteLock = async <T>(
serverId: string,
fn: () => Promise<T>,
): Promise<T> => {
const previous = configWriteLocks.get(serverId) ?? Promise.resolve();
let release: () => void = () => {};
const current = new Promise<void>((resolve) => {
release = resolve;
});
configWriteLocks.set(
serverId,
previous.then(() => current),
);
await previous;
try {
return await runWithCrossProcessLock(serverId, fn);
} finally {
release();
}
};
const runWithCrossProcessLock = async <T>(
serverId: string,
fn: () => Promise<T>,
): Promise<T> => {
const key1 = VECTOR_CONFIG_LOCK_NAMESPACE;
const key2 = hashServerIdToInt32(serverId);
for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
const reserved = await getLockPool().reserve();
try {
const rows =
await reserved`select pg_try_advisory_lock(${key1}, ${key2}) as locked`;
if (rows[0]?.locked) {
try {
return await fn();
} finally {
await reserved`select pg_advisory_unlock(${key1}, ${key2})`;
}
}
} finally {
reserved.release();
}
if (attempt < LOCK_RETRY_ATTEMPTS) {
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
}
}
throw new Error(
`Vector config sync for server ${serverId} is already in progress on another Dokploy instance, skipped`,
);
};
export const syncVectorConfig = async ({
serverId,
preloaded,
}: {
serverId: string;
preloaded?: VectorOrgData;
}) =>
withConfigWriteLock(serverId, async () => {
const getServerLogProviderIds = async (
serverId?: string,
): Promise<string[]> => {
if (serverId) {
const server = await findServerById(serverId);
const { VECTOR_PATH } = paths(true);
const configPath = `${VECTOR_PATH}/vector.yaml`;
const candidatePath = `${VECTOR_PATH}/vector.yaml.candidate`;
const orgData =
preloaded ?? (await loadVectorOrgData(server.organizationId));
const yamlStr = await buildVectorConfigYaml(server.organizationId, orgData);
return server.logProviderIds ?? [];
}
const settings = await getWebServerSettings();
return settings?.logProviderIds ?? [];
};
await execAsyncRemote(serverId, `mkdir -p ${VECTOR_PATH}/data`);
await writeFileRemote(serverId, candidatePath, yamlStr);
await execAsyncRemote(
serverId,
`chmod 600 ${candidatePath} && chmod 700 ${VECTOR_PATH} && chmod 700 ${VECTOR_PATH}/data`,
);
try {
await execAsyncRemote(
serverId,
`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro -v ${candidatePath}:/etc/vector/vector.yaml:ro ${VECTOR_IMAGE} validate --skip-healthchecks /etc/vector/vector.yaml`,
);
} catch (error) {
const detail =
error instanceof ExecError
? error.stderr || error.stdout || error.message
: error instanceof Error
? error.message
: String(error);
const sanitizedDetail = redactSecrets(
detail,
collectSecretValues(orgData.providers),
);
await execAsyncRemote(serverId, `rm -f ${candidatePath}`).catch(
(cleanupError) => {
console.error(
`[Vector] Failed to remove invalid config candidate ${candidatePath} on server ${serverId}:`,
cleanupError,
);
},
);
throw new Error(
`Generated Vector config failed validation, not applying it: ${sanitizedDetail}`,
);
}
await execAsyncRemote(serverId, `mv ${candidatePath} ${configPath}`);
const syncVectorConfig = async (
organizationId: string,
serverId?: string,
logProviderIds?: string[],
) => {
const { VECTOR_PATH } = paths(!!serverId);
const configPath = `${VECTOR_PATH}/vector.yaml`;
const candidatePath = `${VECTOR_PATH}/vector.yaml.candidate.${nanoid(10)}`;
const ids = logProviderIds ?? (await getServerLogProviderIds(serverId));
const providers = await loadEnabledLogProviders(organizationId, ids);
const yamlStr = await buildVectorConfigYaml(providers, {
dropUnmatched: !serverId,
});
export const buildServiceSettings = (
vectorPath: string,
): CreateServiceOptions => {
const VECTOR_PATH = vectorPath;
return {
const writeCommand = `umask 077 && mkdir -p ${quote([`${VECTOR_PATH}/data`])} && chmod 700 ${quote([VECTOR_PATH])} ${quote([`${VECTOR_PATH}/data`])} && echo "${encodeBase64(yamlStr)}" | base64 -d > ${quote([candidatePath])}`;
if (serverId) {
await execAsyncRemote(serverId, writeCommand);
} else {
await execAsync(writeCommand);
}
const validateCommand = `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro -v ${quote([`${candidatePath}:/etc/vector/vector.yaml:ro`])} ${VECTOR_IMAGE} validate --skip-healthchecks /etc/vector/vector.yaml`;
try {
if (serverId) {
await execAsyncRemote(serverId, validateCommand);
} else {
await execAsync(validateCommand);
}
} catch (error) {
const detail =
error instanceof ExecError
? error.stderr || error.stdout || error.message
: error instanceof Error
? error.message
: String(error);
const sanitizedDetail = redactSecrets(
detail,
collectSecretValues(providers),
);
const cleanupCommand = `rm -f ${quote([candidatePath])}`;
try {
if (serverId) {
await execAsyncRemote(serverId, cleanupCommand);
} else {
await execAsync(cleanupCommand);
}
} catch (cleanupError) {
console.error(
`[Vector] Failed to remove invalid config candidate ${candidatePath}${
serverId ? ` on server ${serverId}` : ""
}:`,
cleanupError,
);
}
throw new Error(
`Generated Vector config failed validation, not applying it: ${sanitizedDetail}`,
);
}
const applyCommand = `mv ${quote([candidatePath])} ${quote([configPath])}`;
if (serverId) {
await execAsyncRemote(serverId, applyCommand);
} else {
await execAsync(applyCommand);
}
};
const deployVectorService = async (serverId?: string) => {
const { VECTOR_PATH } = paths(!!serverId);
const docker = await getRemoteDocker(serverId);
const settings: CreateServiceOptions = {
Name: VECTOR_SERVICE_NAME,
TaskTemplate: {
ContainerSpec: {
@@ -456,40 +285,10 @@ export const buildServiceSettings = (
},
},
};
};
export const stripImageDigest = (image: string): string =>
image.split("@")[0] ?? image;
export const vectorServiceSpecUnchanged = (
inspect: { Spec: { TaskTemplate: any; Mode?: unknown } },
settings: CreateServiceOptions,
): boolean => {
const desiredTaskTemplate = settings.TaskTemplate as any;
const existing = inspect.Spec.TaskTemplate?.ContainerSpec ?? {};
const desired = desiredTaskTemplate?.ContainerSpec ?? {};
return (
stripImageDigest(existing.Image ?? "") ===
stripImageDigest(desired.Image ?? "") &&
JSON.stringify(existing.Args ?? []) ===
JSON.stringify(desired.Args ?? []) &&
JSON.stringify(existing.Mounts ?? []) ===
JSON.stringify(desired.Mounts ?? []) &&
JSON.stringify(inspect.Spec.Mode ?? {}) ===
JSON.stringify(settings.Mode ?? {})
);
};
const deployVectorService = async (
docker: Awaited<ReturnType<typeof getRemoteDocker>>,
settings: CreateServiceOptions,
) => {
try {
const service = docker.getService(VECTOR_SERVICE_NAME);
const inspect = await service.inspect();
if (vectorServiceSpecUnchanged(inspect, settings)) {
return;
}
await service.update({
version: Number.parseInt(inspect.Version.Index, 10),
...settings,
@@ -498,130 +297,31 @@ const deployVectorService = async (
ForceUpdate: (inspect.Spec.TaskTemplate.ForceUpdate ?? 0) + 1,
},
});
} catch (error: any) {
if (error?.statusCode !== 404) {
throw error;
}
await docker.createService(settings);
}
};
export const setupVectorAgent = async ({
serverId,
preloaded,
}: {
serverId: string;
preloaded?: VectorOrgData;
}) => {
await syncVectorConfig({ serverId, preloaded });
const { VECTOR_PATH } = paths(true);
await pullRemoteImage(VECTOR_IMAGE, serverId);
const docker = await getRemoteDocker(serverId);
await deployVectorService(docker, buildServiceSettings(VECTOR_PATH));
};
export const removeVectorAgent = async ({ serverId }: { serverId: string }) => {
const server = await findServerById(serverId);
if (!server.sshKeyId) {
throw new Error(`No SSH key configured for server ${serverId}`);
}
const docker = await getRemoteDocker(serverId);
try {
await docker.getService(VECTOR_SERVICE_NAME).remove();
} catch (error: any) {
if (error?.statusCode !== 404) {
throw error;
}
}
const { VECTOR_PATH } = paths(true);
await execAsyncRemote(serverId, `rm -rf ${VECTOR_PATH}`);
};
export const syncVectorAgent = async ({
serverId,
preloaded,
}: {
serverId: string;
preloaded?: VectorOrgData;
}): Promise<{ installed: boolean }> => {
const server = await findServerById(serverId);
const hasProvider = preloaded
? preloaded.providers.length > 0
: await hasEnabledLogProvider(server.organizationId);
const shouldRun = server.enableLogManagement && hasProvider;
if (shouldRun) {
await setupVectorAgent({ serverId, preloaded });
} else {
await removeVectorAgent({ serverId });
}
return { installed: shouldRun };
};
export const syncWebVectorConfig = async (
organizationId: string,
preloaded?: VectorOrgData,
) =>
withConfigWriteLock("web", async () => {
const { VECTOR_PATH } = paths();
const configPath = `${VECTOR_PATH}/vector.yaml`;
const candidatePath = `${VECTOR_PATH}/vector.yaml.candidate`;
const orgData = preloaded ?? (await loadVectorOrgData(organizationId));
const yamlStr = await buildVectorConfigYaml(organizationId, orgData, {
dropUnmatched: true,
});
mkdirSync(`${VECTOR_PATH}/data`, { recursive: true });
writeFileSync(candidatePath, yamlStr, "utf8");
fsChmodSync(candidatePath, 0o600);
fsChmodSync(VECTOR_PATH, 0o700);
fsChmodSync(`${VECTOR_PATH}/data`, 0o700);
console.log("Vector Updated ✅");
} catch (_) {
try {
await execAsync(
`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro -v ${candidatePath}:/etc/vector/vector.yaml:ro ${VECTOR_IMAGE} validate --skip-healthchecks /etc/vector/vector.yaml`,
);
} catch (error) {
const detail =
error instanceof ExecError
? error.stderr || error.stdout || error.message
: error instanceof Error
? error.message
: String(error);
const sanitizedDetail = redactSecrets(
detail,
collectSecretValues(orgData.providers),
);
try {
rmSync(candidatePath, { force: true });
} catch (cleanupError) {
console.error(
`[Vector] Failed to remove invalid local config candidate ${candidatePath}:`,
cleanupError,
);
await docker.createService(settings);
console.log("Vector Started ✅");
} catch (error: any) {
if (error?.statusCode !== 409) {
throw error;
}
throw new Error(
`Generated Vector config failed validation, not applying it: ${sanitizedDetail}`,
);
console.log("Vector service already exists, continuing...");
}
renameSync(candidatePath, configPath);
});
export const setupWebVectorAgent = async (
organizationId: string,
preloaded?: VectorOrgData,
) => {
await syncWebVectorConfig(organizationId, preloaded);
const { VECTOR_PATH } = paths();
await pullImage(VECTOR_IMAGE);
const docker = await getRemoteDocker();
await deployVectorService(docker, buildServiceSettings(VECTOR_PATH));
}
};
export const removeWebVectorAgent = async () => {
const docker = await getRemoteDocker();
export const setupVectorAgent = async (
organizationId: string,
serverId?: string,
logProviderIds?: string[],
) => {
await syncVectorConfig(organizationId, serverId, logProviderIds);
await deployVectorService(serverId);
};
export const removeVectorAgent = async (serverId?: string) => {
const docker = await getRemoteDocker(serverId);
try {
await docker.getService(VECTOR_SERVICE_NAME).remove();
} catch (error: any) {
@@ -629,28 +329,82 @@ export const removeWebVectorAgent = async () => {
throw error;
}
}
const { VECTOR_PATH } = paths();
rmSync(VECTOR_PATH, { recursive: true, force: true });
};
export const syncWebVectorAgent = async (
preloaded?: VectorOrgData,
): Promise<{ installed: boolean }> => {
const settings = await getWebServerSettings();
const organizationId = settings?.logManagementOrganizationId ?? null;
const hasProvider = organizationId
? preloaded
? preloaded.providers.length > 0
: await hasEnabledLogProvider(organizationId)
: false;
const shouldRun =
!!settings?.enableLogManagement && !!organizationId && hasProvider;
if (shouldRun && organizationId) {
await setupWebVectorAgent(organizationId, preloaded);
const { VECTOR_PATH } = paths(!!serverId);
const cleanupCommand = `rm -rf ${quote([VECTOR_PATH])}`;
if (serverId) {
await execAsyncRemote(serverId, cleanupCommand);
} else {
await removeWebVectorAgent();
await execAsync(cleanupCommand);
}
return { installed: shouldRun };
};
const isVectorRunning = async (serverId?: string): Promise<boolean> => {
const docker = await getRemoteDocker(serverId);
try {
await docker.getService(VECTOR_SERVICE_NAME).inspect();
return true;
} catch {
return false;
}
};
const VECTOR_CHECK_TIMEOUT_MS = 5000;
const vectorStatus = async (
serverId: string | null,
): Promise<"running" | "stopped" | "unknown"> => {
try {
const running = await Promise.race([
isVectorRunning(serverId ?? undefined),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("timeout")), VECTOR_CHECK_TIMEOUT_MS),
),
]);
return running ? "running" : "stopped";
} catch {
return "unknown";
}
};
export const getLogManagementServerStatus = async (organizationId: string) => {
const [servers, settings] = await Promise.all([
findServersByOrganizationForLogManagement(organizationId),
IS_CLOUD ? Promise.resolve(null) : getWebServerSettings(),
]);
const webOwnsThisOrg =
!!settings &&
(settings.logManagementOrganizationId === null ||
settings.logManagementOrganizationId === organizationId);
const targets: Array<{
serverId: string | null;
name: string;
ipAddress: string | null;
logProviderIds: string[];
}> = [
...(settings && webOwnsThisOrg
? [
{
serverId: null,
name: "Dokploy Server (local)",
ipAddress: null,
logProviderIds: settings.logProviderIds ?? [],
},
]
: []),
...servers.map((s) => ({
serverId: s.serverId,
name: s.name,
ipAddress: s.ipAddress,
logProviderIds: s.logProviderIds ?? [],
})),
];
return Promise.all(
targets.map(async (target) => ({
...target,
status: await vectorStatus(target.serverId),
})),
);
};
+13 -1
View File
@@ -3,6 +3,7 @@ import { findRegistryByIdWithCredentials } from "@dokploy/server/services/regist
import type { InferResultType } from "@dokploy/server/types/with";
import type { CreateServiceOptions } from "dockerode";
import { getRegistryTag, uploadImageRemoteCommand } from "../cluster/upload";
import { buildDokployLabels } from "../docker/dokploy-labels";
import {
calculateResources,
generateBindMounts,
@@ -150,7 +151,18 @@ export const mechanizeDockerContainer = async (
Args: args,
}),
...(Ulimits && { Ulimits }),
Labels,
Labels: {
...Labels,
...buildDokployLabels({
organizationId: application.environment.project.organizationId,
projectId: application.environment.projectId,
projectName: application.environment.project.name,
environmentId: application.environmentId,
environmentName: application.environment.name,
applicationId: application.applicationId,
applicationName: application.name,
}),
},
},
Networks: resolvedNetworks,
RestartPolicy,
@@ -0,0 +1,34 @@
export interface DokployResourceMetadata {
organizationId: string;
projectId: string;
projectName: string;
environmentId: string;
environmentName: string;
applicationId: string;
applicationName: string;
serviceName?: string;
}
export const DOKPLOY_LABEL_PREFIX = "dokploy";
export const buildDokployLabels = (
metadata: DokployResourceMetadata,
): Record<string, string> => ({
[`${DOKPLOY_LABEL_PREFIX}.organization.id`]: metadata.organizationId,
[`${DOKPLOY_LABEL_PREFIX}.project.id`]: metadata.projectId,
[`${DOKPLOY_LABEL_PREFIX}.project`]: metadata.projectName,
[`${DOKPLOY_LABEL_PREFIX}.environment.id`]: metadata.environmentId,
[`${DOKPLOY_LABEL_PREFIX}.environment`]: metadata.environmentName,
[`${DOKPLOY_LABEL_PREFIX}.application.id`]: metadata.applicationId,
[`${DOKPLOY_LABEL_PREFIX}.application`]: metadata.applicationName,
...(metadata.serviceName
? { [`${DOKPLOY_LABEL_PREFIX}.service`]: metadata.serviceName }
: {}),
});
export const dokployLabelsAsList = (
metadata: DokployResourceMetadata,
): string[] =>
Object.entries(buildDokployLabels(metadata)).map(
([key, value]) => `${key}=${value}`,
);
+63 -2
View File
@@ -17,6 +17,7 @@ import { cloneGitlabRepository } from "../providers/gitlab";
import { getCreateComposeFileCommand } from "../providers/raw";
import { randomizeDeployableSpecificationFile } from "./collision";
import { randomizeSpecificationFile } from "./compose";
import { buildDokployLabels } from "./dokploy-labels";
import type {
ComposeSpecification,
DefinitionsService,
@@ -111,7 +112,7 @@ export const readComposeFile = async (compose: Compose) => {
};
export const writeDomainsToCompose = async (
compose: Compose,
compose: ComposeWithMetadata,
domains: Domain[],
) => {
try {
@@ -196,8 +197,66 @@ const removeDomainLabels = (
return labels;
};
type ComposeWithMetadata = Compose & {
environment?: {
environmentId: string;
name: string;
projectId: string;
project?: { name: string; organizationId: string };
};
};
const applyDokployLabelsToCompose = (
result: ComposeSpecification,
compose: ComposeWithMetadata,
) => {
const { environment } = compose;
if (!environment?.project) return;
for (const [serviceName, service] of Object.entries(result.services ?? {})) {
if (!service) continue;
const dokployLabels = buildDokployLabels({
organizationId: environment.project.organizationId,
projectId: environment.projectId,
projectName: environment.project.name,
environmentId: environment.environmentId,
environmentName: environment.name,
applicationId: compose.composeId,
applicationName: compose.name,
serviceName,
});
if (compose.composeType === "docker-compose") {
service.labels = mergeDokployLabels(service.labels, dokployLabels);
continue;
}
if (!service.deploy) {
service.deploy = {};
}
service.deploy.labels = mergeDokployLabels(
service.deploy.labels,
dokployLabels,
);
}
};
const mergeDokployLabels = (
current: DefinitionsService["labels"],
dokployLabels: Record<string, string>,
): DefinitionsService["labels"] => {
if (current && !Array.isArray(current)) {
return { ...current, ...dokployLabels };
}
const entries = Object.entries(dokployLabels);
const keys = new Set(entries.map(([key]) => key));
const kept = (current ?? []).filter(
(label) => !keys.has(label.slice(0, label.indexOf("="))),
);
return [...kept, ...entries.map(([key, value]) => `${key}=${value}`)];
};
export const addDomainToCompose = async (
compose: Compose,
compose: ComposeWithMetadata,
domains: Domain[],
) => {
const { appName } = compose;
@@ -326,6 +385,8 @@ export const addDomainToCompose = async (
}
}
applyDokployLabelsToCompose(result, compose);
const injectedNetworkNames = await applyServiceNetworks(result, compose);
if (!compose.isolatedDeployment) {