diff --git a/messages/en-US.json b/messages/en-US.json
index 0e9cb5786..9a23043d5 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -3062,7 +3062,7 @@
"streamingDatadogTitle": "Datadog",
"streamingDatadogDescription": "Forward events directly to your Datadog account.",
"streamingTypePickerDescription": "Choose a destination type to get started.",
- "streamingFailedToLoad": "Failed to load destinations",
+ "streamingLastSyncError": "An error occurred on the last sync",
"streamingUnexpectedError": "An unexpected error occurred.",
"streamingFailedToUpdate": "Failed to update destination",
"streamingDeletedSuccess": "Destination deleted successfully",
diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts
index 0f1914fad..6137743bf 100644
--- a/server/db/pg/schema/privateSchema.ts
+++ b/server/db/pg/schema/privateSchema.ts
@@ -439,6 +439,8 @@ export const eventStreamingDestinations = pgTable(
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
config: text("config").notNull(), // JSON string with the configuration for the destination
enabled: boolean("enabled").notNull().default(true),
+ lastError: text("lastError"), // last send error message, null if healthy
+ lastErrorAt: bigint("lastErrorAt", { mode: "number" }), // epoch ms of last error, null if healthy
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
}
diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts
index 05c917887..a25183055 100644
--- a/server/db/sqlite/schema/privateSchema.ts
+++ b/server/db/sqlite/schema/privateSchema.ts
@@ -445,6 +445,8 @@ export const eventStreamingDestinations = sqliteTable(
enabled: integer("enabled", { mode: "boolean" })
.notNull()
.default(true),
+ lastError: text("lastError"), // last send error message, null if healthy
+ lastErrorAt: integer("lastErrorAt"), // epoch ms of last error, null if healthy
createdAt: integer("createdAt").notNull(),
updatedAt: integer("updatedAt").notNull()
}
diff --git a/server/private/lib/logStreaming/LogStreamingManager.ts b/server/private/lib/logStreaming/LogStreamingManager.ts
index a9575fec6..03efc2809 100644
--- a/server/private/lib/logStreaming/LogStreamingManager.ts
+++ b/server/private/lib/logStreaming/LogStreamingManager.ts
@@ -313,6 +313,7 @@ export class LogStreamingManager {
if (enabledTypes.length === 0) return;
let anyFailure = false;
+ let firstError: string | null = null;
for (const logType of enabledTypes) {
if (!this.isRunning) break;
@@ -320,6 +321,10 @@ export class LogStreamingManager {
await this.processLogType(dest, provider, logType);
} catch (err) {
anyFailure = true;
+ if (firstError === null) {
+ firstError =
+ err instanceof Error ? err.message : String(err);
+ }
logger.error(
`LogStreamingManager: failed to process "${logType}" logs ` +
`for destination ${dest.destinationId}`,
@@ -330,6 +335,10 @@ export class LogStreamingManager {
if (anyFailure) {
this.recordFailure(dest.destinationId);
+ await this.setDestinationError(
+ dest.destinationId,
+ firstError ?? "Unknown error"
+ );
} else {
// Any success resets the failure/back-off state
if (this.failures.has(dest.destinationId)) {
@@ -338,6 +347,7 @@ export class LogStreamingManager {
`LogStreamingManager: destination ${dest.destinationId} recovered`
);
}
+ await this.clearDestinationError(dest.destinationId);
}
}
@@ -759,6 +769,45 @@ export class LogStreamingManager {
// DB helpers
// -------------------------------------------------------------------------
+ private async setDestinationError(
+ destinationId: number,
+ errorMessage: string
+ ): Promise
- {t("httpDestAuthNoneDescription")} + {t( + "httpDestAuthNoneDescription" + )}
- {t("httpDestAuthBearerDescription")} + {t( + "httpDestAuthBearerDescription" + )}
{cfg.authType === "bearer" && ( - {t("httpDestAuthBasicTitle")} + {t( + "httpDestAuthBasicTitle" + )}- {t("httpDestAuthBasicDescription")} + {t( + "httpDestAuthBasicDescription" + )}
{cfg.authType === "basic" && ( - {t("httpDestAuthCustomTitle")} + {t( + "httpDestAuthCustomTitle" + )}- {t("httpDestAuthCustomDescription")} + {t( + "httpDestAuthCustomDescription" + )}
{cfg.authType === "custom" && (- {t("httpDestFormatJsonArrayDescription")} + {t( + "httpDestFormatJsonArrayDescription" + )}
- {t("httpDestFormatNdjsonDescription")} + {t( + "httpDestFormatNdjsonDescription" + )}
@@ -636,7 +665,9 @@ export function HttpDestinationCredenza({ {t("httpDestFormatSingleTitle")}- {t("httpDestFormatSingleDescription")} + {t( + "httpDestFormatSingleDescription" + )}
@@ -717,7 +748,9 @@ export function HttpDestinationCredenza({ {t("httpDestConnectionLogsTitle")}- {t("httpDestConnectionLogsDescription")} + {t( + "httpDestConnectionLogsDescription" + )}
@@ -739,7 +772,9 @@ export function HttpDestinationCredenza({ {t("httpDestRequestLogsTitle")}- {t("httpDestRequestLogsDescription")} + {t( + "httpDestRequestLogsDescription" + )}
@@ -764,10 +799,12 @@ export function HttpDestinationCredenza({ loading={saving} disabled={!isValid || saving} > - {editing ? t("httpDestSaveChanges") : t("httpDestCreateDestination")} + {editing + ? t("httpDestSaveChanges") + : t("httpDestCreateDestination")} ); -} \ No newline at end of file +} diff --git a/src/components/S3DestinationCredenza.tsx b/src/components/S3DestinationCredenza.tsx index 03b055f58..e6c128805 100644 --- a/src/components/S3DestinationCredenza.tsx +++ b/src/components/S3DestinationCredenza.tsx @@ -18,6 +18,8 @@ import { Switch } from "@app/components/ui/switch"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; import { Checkbox } from "@app/components/ui/checkbox"; +import { AlertCircle } from "lucide-react"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; @@ -164,6 +166,14 @@ export function S3DestinationCredenza({