diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 46327afd0..bd35c7bf4 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Пресочвайте събития директно към вашият акаунт в Datadog. Очаквайте скоро.", "streamingTypePickerDescription": "Изберете вид на дестинацията, за да започнете.", - "streamingFailedToLoad": "Неуспешно зареждане на дестинации", + "streamingLastSyncError": "Възникна грешка при последната синхронизация", "streamingUnexpectedError": "Възникна неочаквана грешка.", "streamingFailedToUpdate": "Неуспешно актуализиране на дестинация", "streamingDeletedSuccess": "Дестинацията беше изтрита успешно", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Редактиране на дестинацията", "S3DestAddTitle": "Добавете S3 дестинация", "S3DestEditDescription": "Актуализирайте конфигурацията за тази S3 дестинация за предаване на събития.", - "S3DestAddDescription": "Конфигурирайте нов крайна точка на S3, за да получавате събития на вашата организация.", + "S3DestAddDescription": "Конфигурирайте ново хранилище Amazon S3 (или съвместимо с S3), за да получавате събития на вашата организация.", + "s3DestTabSettings": "Настройки", + "s3DestTabFormat": "Формат", + "s3DestNameLabel": "Име", + "s3DestNamePlaceholder": "Моята S3 дестинация", + "s3DestAccessKeyIdLabel": "Идентификатор на достъп за AWS Key ID", + "s3DestSecretAccessKeyLabel": "Тайният ключ за достъп на AWS", + "s3DestSecretAccessKeyPlaceholder": "Вашият таен ключ за достъп за AWS", + "s3DestRegionLabel": "AWS Регион", + "s3DestBucketLabel": "Име на хранилище", + "s3DestPrefixLabel": "Префикс на ключ (по избор)", + "s3DestPrefixDescription": "По избор пътеводен префикс, добавен към всеки обектен ключ. Обектите се съхраняват в {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Потребителски крайна точка (по избор)", + "s3DestEndpointDescription": "Заместете крайната точка на S3 за съвместимо с S3 хранилище като MinIO или Cloudflare R2. Оставете празно за стандартното AWS S3.", + "s3DestGzipLabel": "Gzip компресия", + "s3DestGzipDescription": "Компресирайте всеки качен обект с gzip. Намалява разходите за съхранение и размера на качването.", + "s3DestFormatTitle": "Формат на файл", + "s3DestFormatDescription": "Как събитията са сериализирани вътре във всеки качен обект.", + "s3DestFormatJsonArrayDescription": "Всеки обект е JSON масив от записи на събития. Съвместим с повечето аналитични инструменти.", + "s3DestFormatNdjsonDescription": "Всеки обект съдържа един JSON запис на ред (форматиран JSON с нов ред). Съвместим с Athena, BigQuery и Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Всеки обект е RFC-4180 CSV файл с ред заглавие. Имената на колоните са извлечени от полетата на данните за събитията.", + "s3DestSaveChanges": "Запази промените", + "s3DestCreateDestination": "Създаване на дестинация", + "s3DestUpdatedSuccess": "Дестинацията е актуализирана успешно", + "s3DestCreatedSuccess": "Дестинацията е създадена успешно", + "s3DestUpdateFailed": "Неуспешно актуализиране на дестинацията", + "s3DestCreateFailed": "Неуспешно създаване на дестинация", "datadogDestEditTitle": "Редактиране на дестинация", "datadogDestAddTitle": "Добавяне на Datadog дестинация", "datadogDestEditDescription": "Актуализирайте конфигурацията за тази Datadog дестинация за предаване на събития.", diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 38da0604a..80c242e39 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Přeposlat události přímo do vašeho účtu Datadog účtu. Brzy přijde.", "streamingTypePickerDescription": "Vyberte cílový typ pro začátek.", - "streamingFailedToLoad": "Nepodařilo se načíst destinace", + "streamingLastSyncError": "Došlo k chybě při poslední synchronizaci", "streamingUnexpectedError": "Došlo k neočekávané chybě.", "streamingFailedToUpdate": "Nepodařilo se aktualizovat cíl", "streamingDeletedSuccess": "Cíl byl úspěšně odstraněn", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Upravit cíl", "S3DestAddTitle": "Přidat S3 cíl", "S3DestEditDescription": "Aktualizujte konfiguraci tohoto S3 cíle pro streamování událostí.", - "S3DestAddDescription": "Konfigurujte nový S3 koncový bod pro přijímání událostí vaší organizace.", + "S3DestAddDescription": "Nakonfigurujte nový Amazon S3 (nebo S3-kompatibilní) bucket, aby přijímal události vaší organizace.", + "s3DestTabSettings": "Nastavení", + "s3DestTabFormat": "Formát", + "s3DestNameLabel": "Jméno", + "s3DestNamePlaceholder": "Moje cílové S3", + "s3DestAccessKeyIdLabel": "ID přístupového klíče AWS", + "s3DestSecretAccessKeyLabel": "Tajný přístupový klíč AWS", + "s3DestSecretAccessKeyPlaceholder": "Váš tajný přístupový klíč AWS", + "s3DestRegionLabel": "Oblast AWS", + "s3DestBucketLabel": "Název bucketu", + "s3DestPrefixLabel": "Předpona klíče (volitelné)", + "s3DestPrefixDescription": "Volitelná cesta předpony přidaná ke každému objektovému klíči. Objekty jsou uloženy na {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Vlastní koncový bod (volitelné)", + "s3DestEndpointDescription": "Přepište koncový bod S3 pro S3-kompatibilní úložiště, jako je MinIO nebo Cloudflare R2. Nechte prázdné pro standardní AWS S3.", + "s3DestGzipLabel": "Komprese Gzip", + "s3DestGzipDescription": "Komprimujte každý nahraný objekt pomocí gzip. Snižuje náklady na uložení a velikost nahrávání.", + "s3DestFormatTitle": "Formát souboru", + "s3DestFormatDescription": "Jak jsou události serializovány v každém nahraném objektu.", + "s3DestFormatJsonArrayDescription": "Každý objekt je pole JSON záznamů událostí. Kompatibilní s většinou analytických nástrojů.", + "s3DestFormatNdjsonDescription": "Každý objekt obsahuje jeden JSON záznam na řádku (newline-delimited JSON). Kompatibilní s Athena, BigQuery a Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Každý objekt je soubor CSV podle RFC-4180 s řádkem záhlaví. Názvy sloupců jsou odvozeny z polí dat událostí.", + "s3DestSaveChanges": "Uložit změny", + "s3DestCreateDestination": "Vytvořit destinaci", + "s3DestUpdatedSuccess": "Destinace úspěšně aktualizována", + "s3DestCreatedSuccess": "Destinace úspěšně vytvořena", + "s3DestUpdateFailed": "Aktualizace destinace se nezdařila", + "s3DestCreateFailed": "Vytvoření destinace se nezdařilo", "datadogDestEditTitle": "Upravit cíl", "datadogDestAddTitle": "Přidat Datadog cíl", "datadogDestEditDescription": "Aktualizujte konfiguraci tohoto Datadog cíle pro streamování událostí.", diff --git a/messages/de-DE.json b/messages/de-DE.json index 367c95cd3..a7e1a5fa7 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Events direkt an Ihr Datadog Konto weiterleiten. Kommen Sie bald.", "streamingTypePickerDescription": "Wählen Sie einen Zieltyp aus, um loszulegen.", - "streamingFailedToLoad": "Fehler beim Laden der Ziele", + "streamingLastSyncError": "Beim letzten Synchronisieren ist ein Fehler aufgetreten.", "streamingUnexpectedError": "Ein unerwarteter Fehler ist aufgetreten.", "streamingFailedToUpdate": "Fehler beim Aktualisieren des Ziels", "streamingDeletedSuccess": "Ziel erfolgreich gelöscht", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Ziel bearbeiten", "S3DestAddTitle": "S3-Ziel hinzufügen", "S3DestEditDescription": "Konfiguration für dieses S3-Ereignis-Streamingziel aktualisieren.", - "S3DestAddDescription": "Neuen S3-Endpunkt konfigurieren, um die Ereignisse Ihrer Organisation zu erhalten.", + "S3DestAddDescription": "Konfigurieren Sie einen neuen Amazon S3 (oder S3-kompatiblen) Bucket, um die Ereignisse Ihrer Organisation zu empfangen.", + "s3DestTabSettings": "Einstellungen", + "s3DestTabFormat": "Format", + "s3DestNameLabel": "Name", + "s3DestNamePlaceholder": "Mein S3-Ziel", + "s3DestAccessKeyIdLabel": "AWS-Zugriffsschlüssel-ID", + "s3DestSecretAccessKeyLabel": "AWS-Geheimzugriffsschlüssel", + "s3DestSecretAccessKeyPlaceholder": "Ihr AWS-Geheimzugriffsschlüssel", + "s3DestRegionLabel": "AWS-Region", + "s3DestBucketLabel": "Bucket-Name", + "s3DestPrefixLabel": "Schlüssel-Präfix (optional)", + "s3DestPrefixDescription": "Optionales Pfadpräfix, das jedem Objektschlüssel vorangestellt wird. Objekte werden unter {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename} gespeichert.", + "s3DestEndpointLabel": "Benutzerdefinierter Endpunkt (optional)", + "s3DestEndpointDescription": "Überschreiben Sie den S3-Endpunkt für S3-kompatiblen Speicher wie MinIO oder Cloudflare R2. Lassen Sie das Feld leer für standardmäßiges AWS S3.", + "s3DestGzipLabel": "Gzip-Komprimierung", + "s3DestGzipDescription": "Jedes hochgeladene Objekt mit Gzip komprimieren. Reduziert die Speicherkosten und die Upload-Größe.", + "s3DestFormatTitle": "Dateiformat", + "s3DestFormatDescription": "Wie Ereignisse in jedem hochgeladenen Objekt serialisiert werden.", + "s3DestFormatJsonArrayDescription": "Jedes Objekt ist ein JSON-Array von Ereignisdaten. Kompatibel mit den meisten Analysetools.", + "s3DestFormatNdjsonDescription": "Jedes Objekt enthält einen JSON-Datensatz pro Zeile (newline-delimited JSON). Kompatibel mit Athena, BigQuery und Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Jedes Objekt ist eine RFC-4180 CSV-Datei mit einer Kopfzeile. Spaltennamen werden aus den Ereignisdatenfeldern abgeleitet.", + "s3DestSaveChanges": "Änderungen speichern", + "s3DestCreateDestination": "Ziel erstellen", + "s3DestUpdatedSuccess": "Ziel erfolgreich aktualisiert", + "s3DestCreatedSuccess": "Ziel erfolgreich erstellt", + "s3DestUpdateFailed": "Fehler beim Aktualisieren des Ziels", + "s3DestCreateFailed": "Fehler beim Erstellen des Ziels", "datadogDestEditTitle": "Ziel bearbeiten", "datadogDestAddTitle": "Datadog-Ziel hinzufügen", "datadogDestEditDescription": "Konfiguration für dieses Datadog-Ereignis-Streamingziel aktualisieren.", diff --git a/messages/en-US.json b/messages/en-US.json index a598dcc39..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", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Edit Destination", "S3DestAddTitle": "Add S3 Destination", "S3DestEditDescription": "Update the configuration for this S3 event streaming destination.", - "S3DestAddDescription": "Configure a new S3 endpoint to receive your organization's events.", + "S3DestAddDescription": "Configure a new Amazon S3 (or S3-compatible) bucket to receive your organization's events.", + "s3DestTabSettings": "Settings", + "s3DestTabFormat": "Format", + "s3DestNameLabel": "Name", + "s3DestNamePlaceholder": "My S3 destination", + "s3DestAccessKeyIdLabel": "AWS Access Key ID", + "s3DestSecretAccessKeyLabel": "AWS Secret Access Key", + "s3DestSecretAccessKeyPlaceholder": "Your AWS secret access key", + "s3DestRegionLabel": "AWS Region", + "s3DestBucketLabel": "Bucket Name", + "s3DestPrefixLabel": "Key Prefix (optional)", + "s3DestPrefixDescription": "Optional path prefix prepended to every object key. Objects are stored at {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Custom Endpoint (optional)", + "s3DestEndpointDescription": "Override the S3 endpoint for S3-compatible storage such as MinIO or Cloudflare R2. Leave blank for standard AWS S3.", + "s3DestGzipLabel": "Gzip compression", + "s3DestGzipDescription": "Compress each uploaded object with gzip. Reduces storage costs and upload size.", + "s3DestFormatTitle": "File Format", + "s3DestFormatDescription": "How events are serialised inside each uploaded object.", + "s3DestFormatJsonArrayDescription": "Each object is a JSON array of event records. Compatible with most analytics tools.", + "s3DestFormatNdjsonDescription": "Each object contains one JSON record per line (newline-delimited JSON). Compatible with Athena, BigQuery, and Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Each object is an RFC-4180 CSV file with a header row. Column names are derived from the event data fields.", + "s3DestSaveChanges": "Save Changes", + "s3DestCreateDestination": "Create Destination", + "s3DestUpdatedSuccess": "Destination updated successfully", + "s3DestCreatedSuccess": "Destination created successfully", + "s3DestUpdateFailed": "Failed to update destination", + "s3DestCreateFailed": "Failed to create destination", "datadogDestEditTitle": "Edit Destination", "datadogDestAddTitle": "Add Datadog Destination", "datadogDestEditDescription": "Update the configuration for this Datadog event streaming destination.", diff --git a/messages/es-ES.json b/messages/es-ES.json index e610233f7..3175b4844 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Reenviar eventos directamente a tu cuenta de Datadog. Próximamente.", "streamingTypePickerDescription": "Elija un tipo de destino para empezar.", - "streamingFailedToLoad": "Error al cargar destinos", + "streamingLastSyncError": "Ocurrió un error en la última sincronización.", "streamingUnexpectedError": "Se ha producido un error inesperado.", "streamingFailedToUpdate": "Error al actualizar destino", "streamingDeletedSuccess": "Destino eliminado correctamente", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Editar destino", "S3DestAddTitle": "Añadir destino S3", "S3DestEditDescription": "Actualice la configuración para este destino de transmisión de eventos S3.", - "S3DestAddDescription": "Configure un nuevo punto final S3 para recibir los eventos de su organización.", + "S3DestAddDescription": "Configura un nuevo bucket de Amazon S3 (o compatible con S3) para recibir los eventos de tu organización.", + "s3DestTabSettings": "Ajustes", + "s3DestTabFormat": "Formato", + "s3DestNameLabel": "Nombre", + "s3DestNamePlaceholder": "Mi destino S3", + "s3DestAccessKeyIdLabel": "ID de clave de acceso de AWS", + "s3DestSecretAccessKeyLabel": "Clave de acceso secreta de AWS", + "s3DestSecretAccessKeyPlaceholder": "Tu clave de acceso secreta de AWS", + "s3DestRegionLabel": "Región de AWS", + "s3DestBucketLabel": "Nombre del bucket", + "s3DestPrefixLabel": "Prefijo clave (opcional)", + "s3DestPrefixDescription": "Prefijo de ruta opcional preanexado a cada clave de objeto. Los objetos se almacenan en {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Punto final personalizado (opcional)", + "s3DestEndpointDescription": "Sobrescribe el punto final de S3 para almacenamiento compatible con S3 como MinIO o Cloudflare R2. Deja en blanco para el estándar AWS S3.", + "s3DestGzipLabel": "Compresión Gzip", + "s3DestGzipDescription": "Comprime cada objeto subido con gzip. Reduce costos de almacenamiento y tamaño de carga.", + "s3DestFormatTitle": "Formato de archivo", + "s3DestFormatDescription": "Cómo se serializan los eventos dentro de cada objeto cargado.", + "s3DestFormatJsonArrayDescription": "Cada objeto es un arreglo JSON de registros de eventos. Compatible con la mayoría de las herramientas de analítica.", + "s3DestFormatNdjsonDescription": "Cada objeto contiene un registro JSON por línea (JSON delimitado por nueva línea). Compatible con Athena, BigQuery y Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Cada objeto es un archivo CSV conforme a RFC-4180 con una fila de encabezado. Los nombres de columna se derivan de los campos de datos del evento.", + "s3DestSaveChanges": "Guardar cambios", + "s3DestCreateDestination": "Crear destino", + "s3DestUpdatedSuccess": "Destino actualizado con éxito", + "s3DestCreatedSuccess": "Destino creado con éxito", + "s3DestUpdateFailed": "No se pudo actualizar el destino", + "s3DestCreateFailed": "No se pudo crear el destino", "datadogDestEditTitle": "Editar destino", "datadogDestAddTitle": "Añadir destino Datadog", "datadogDestEditDescription": "Actualice la configuración para este destino de transmisión de eventos Datadog.", diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 4bcbeaa0f..4391e9946 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -1356,7 +1356,7 @@ "sidebarSites": "Nœuds", "sidebarApprovals": "Demandes d'approbation", "sidebarResources": "Ressource", - "sidebarProxyResources": "Publiques", + "sidebarProxyResources": "Publique", "sidebarClientResources": "Privé", "sidebarAccessControl": "Contrôle d'accès", "sidebarLogsAndAnalytics": "Journaux & Analytiques", @@ -2458,8 +2458,8 @@ "manageUserDevicesDescription": "Voir et gérer les appareils que les utilisateurs utilisent pour se connecter en privé aux ressources", "downloadClientBannerTitle": "Télécharger le client Pangolin", "downloadClientBannerDescription": "Téléchargez le client Pangolin pour votre système afin de vous connecter au réseau Pangolin et accéder aux ressources de manière privée.", - "manageMachineClients": "Gérer les machines", - "manageMachineClientsDescription": "Créer et gérer les clients que les serveurs et systèmes utilisent pour se connecter en privé aux ressources", + "manageMachineClients": "Gérer les clients de la machine", + "manageMachineClientsDescription": "Créer et gérer des clients que les serveurs et les systèmes utilisent pour se connecter en privé aux ressources", "machineClientsBannerTitle": "Serveurs & Systèmes automatisés", "machineClientsBannerDescription": "Les clients de machine sont conçus pour les serveurs et les systèmes automatisés qui ne sont pas associés à un utilisateur spécifique. Ils s'authentifient avec un identifiant et une clé secrète, et peuvent être exécutés avec Pangolin CLI, Olm CLI ou Olm en tant que conteneur.", "machineClientsBannerPangolinCLI": "Pangolin CLI", @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Transférer des événements directement sur votre compte Datadog. Prochainement.", "streamingTypePickerDescription": "Choisissez un type de destination pour commencer.", - "streamingFailedToLoad": "Impossible de charger les destinations", + "streamingLastSyncError": "Une erreur s'est produite lors de la dernière synchronisation", "streamingUnexpectedError": "Une erreur inattendue s'est produite.", "streamingFailedToUpdate": "Impossible de mettre à jour la destination", "streamingDeletedSuccess": "Destination supprimée avec succès", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Modifier la destination", "S3DestAddTitle": "Ajouter une destination S3", "S3DestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements S3.", - "S3DestAddDescription": "Configurer un nouveau point de terminaison S3 pour recevoir les événements de votre organisation.", + "S3DestAddDescription": "Configurez un nouveau bucket Amazon S3 (ou compatible S3) pour recevoir les événements de votre organisation.", + "s3DestTabSettings": "Réglages", + "s3DestTabFormat": "Format", + "s3DestNameLabel": "Nom", + "s3DestNamePlaceholder": "Ma destination S3", + "s3DestAccessKeyIdLabel": "ID de clé d'accès AWS", + "s3DestSecretAccessKeyLabel": "Clé d'accès secrète AWS", + "s3DestSecretAccessKeyPlaceholder": "Votre clé d'accès secrète AWS", + "s3DestRegionLabel": "Région AWS", + "s3DestBucketLabel": "Nom du bucket", + "s3DestPrefixLabel": "Préfixe clé (facultatif)", + "s3DestPrefixDescription": "Préfixe de chemin facultatif préfixé à chaque clé d'objet. Les objets sont stockés à {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Point de terminaison personnalisé (facultatif)", + "s3DestEndpointDescription": "Modifiez le point de terminaison S3 pour un stockage compatible S3 tel que MinIO ou Cloudflare R2. Laissez vide pour l'AWS S3 standard.", + "s3DestGzipLabel": "Compression Gzip", + "s3DestGzipDescription": "Compressez chaque objet téléchargé avec gzip. Réduit les coûts de stockage et la taille de téléchargement.", + "s3DestFormatTitle": "Format de fichier", + "s3DestFormatDescription": "Comment les événements sont sérialisés dans chaque objet téléchargé.", + "s3DestFormatJsonArrayDescription": "Chaque objet est un tableau JSON des enregistrements d'événements. Compatible avec la plupart des outils d'analyse.", + "s3DestFormatNdjsonDescription": "Chaque objet contient un enregistrement JSON par ligne (JSON délimité par saut de ligne). Compatible avec Athena, BigQuery et Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Chaque objet est un fichier CSV RFC-4180 avec une ligne d'en-tête. Les noms de colonne sont dérivés des champs de données de l'événement.", + "s3DestSaveChanges": "Enregistrer les modifications", + "s3DestCreateDestination": "Créer une destination", + "s3DestUpdatedSuccess": "Destination mise à jour avec succès", + "s3DestCreatedSuccess": "Destination créée avec succès", + "s3DestUpdateFailed": "Échec de la mise à jour de la destination", + "s3DestCreateFailed": "Échec de la création de la destination", "datadogDestEditTitle": "Modifier la destination", "datadogDestAddTitle": "Ajouter une destination Datadog", "datadogDestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements Datadog.", @@ -3154,7 +3181,6 @@ "healthCheckTabAdvanced": "Avancé", "healthCheckStrategyNotAvailable": "Cette stratégie n'est pas disponible. Veuillez contacter le service commercial pour activer cette fonctionnalité.", "uptime30d": "Disponibilité (30j)", - "uptimeNoData": "Aucune donnée", "idpAddActionCreateNew": "Créer un nouveau fournisseur d'identité", "idpAddActionImportFromOrg": "Importer d'une autre organisation", "idpImportDialogTitle": "Importer le fournisseur d'identité", diff --git a/messages/it-IT.json b/messages/it-IT.json index c739b269d..5c432eadd 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Inoltra gli eventi direttamente al tuo account Datadog. In arrivo.", "streamingTypePickerDescription": "Scegli un tipo di destinazione per iniziare.", - "streamingFailedToLoad": "Impossibile caricare le destinazioni", + "streamingLastSyncError": "Si è verificato un errore durante l'ultima sincronizzazione", "streamingUnexpectedError": "Si è verificato un errore imprevisto.", "streamingFailedToUpdate": "Impossibile aggiornare la destinazione", "streamingDeletedSuccess": "Destinazione eliminata con successo", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Modifica Destinazione", "S3DestAddTitle": "Aggiungi Destinazione S3", "S3DestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi S3.", - "S3DestAddDescription": "Configura un nuovo endpoint S3 per ricevere gli eventi della tua organizzazione.", + "S3DestAddDescription": "Configura un nuovo bucket Amazon S3 (o compatibile con S3) per ricevere gli eventi della tua organizzazione.", + "s3DestTabSettings": "Impostazioni", + "s3DestTabFormat": "Formato", + "s3DestNameLabel": "Nome", + "s3DestNamePlaceholder": "La mia destinazione S3", + "s3DestAccessKeyIdLabel": "ID Chiave Accesso AWS", + "s3DestSecretAccessKeyLabel": "Chiave Segreta Accesso AWS", + "s3DestSecretAccessKeyPlaceholder": "La tua chiave segreta di accesso AWS", + "s3DestRegionLabel": "Regione AWS", + "s3DestBucketLabel": "Nome Bucket", + "s3DestPrefixLabel": "Prefisso Chiave (facoltativo)", + "s3DestPrefixDescription": "Prefisso percorso facoltativo anteposto a ogni chiave oggetto. Gli oggetti vengono archiviati in {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Endpoint personalizzato (facoltativo)", + "s3DestEndpointDescription": "Sostituisci l'endpoint S3 per lo storage compatibile con S3 come MinIO o Cloudflare R2. Lasciare vuoto per l'AWS S3 standard.", + "s3DestGzipLabel": "Compressione Gzip", + "s3DestGzipDescription": "Comprimi ogni oggetto caricato con gzip. Riduce i costi di archiviazione e la dimensione di caricamento.", + "s3DestFormatTitle": "Formato del File", + "s3DestFormatDescription": "Come gli eventi sono serializzati all'interno di ciascun oggetto caricato.", + "s3DestFormatJsonArrayDescription": "Ogni oggetto è un array JSON di record di eventi. Compatibile con la maggior parte degli strumenti analitici.", + "s3DestFormatNdjsonDescription": "Ogni oggetto contiene un record JSON per linea (JSON delimitato da newline). Compatibile con Athena, BigQuery e Spark.", + "s3DestFormatCsvTitle": "\"CSV\"", + "s3DestFormatCsvDescription": "Ogni oggetto è un file CSV RFC-4180 con una riga di intestazione. I nomi delle colonne sono derivati dai campi dei dati degli eventi.", + "s3DestSaveChanges": "Salva modifiche", + "s3DestCreateDestination": "Crea destinazione", + "s3DestUpdatedSuccess": "Destinazione aggiornata con successo", + "s3DestCreatedSuccess": "Destinazione creata con successo", + "s3DestUpdateFailed": "Aggiornamento della destinazione fallito", + "s3DestCreateFailed": "Creazione della destinazione fallita", "datadogDestEditTitle": "Modifica Destinazione", "datadogDestAddTitle": "Aggiungi Destinazione Datadog", "datadogDestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi Datadog.", diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 1d3d77fe2..ef66f1470 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "데이터독", "streamingDatadogDescription": "이벤트를 직접 Datadog 계정으로 전달합니다. 곧 제공됩니다.", "streamingTypePickerDescription": "목표 유형을 선택하여 시작합니다.", - "streamingFailedToLoad": "대상 로드에 실패했습니다", + "streamingLastSyncError": "마지막 동기화에서 오류가 발생했습니다.", "streamingUnexpectedError": "예기치 않은 오류가 발생했습니다.", "streamingFailedToUpdate": "대상지를 업데이트하는 데 실패했습니다", "streamingDeletedSuccess": "대상지가 성공적으로 삭제되었습니다", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "대상지 수정", "S3DestAddTitle": "S3 대상지 추가", "S3DestEditDescription": "이 S3 이벤트 스트리밍 대상지의 구성을 업데이트하세요.", - "S3DestAddDescription": "조직의 이벤트를 받기 위한 새로운 S3 엔드포인트를 구성하세요.", + "S3DestAddDescription": "조직의 이벤트를 수신할 새로운 Amazon S3(또는 S3 호환) 버킷을 구성하세요.", + "s3DestTabSettings": "설정", + "s3DestTabFormat": "형식", + "s3DestNameLabel": "이름", + "s3DestNamePlaceholder": "내 S3 대상", + "s3DestAccessKeyIdLabel": "AWS 액세스 키 ID", + "s3DestSecretAccessKeyLabel": "AWS 비밀 액세스 키", + "s3DestSecretAccessKeyPlaceholder": "귀하의 AWS 비밀 액세스 키", + "s3DestRegionLabel": "AWS 지역", + "s3DestBucketLabel": "버킷 이름", + "s3DestPrefixLabel": "키 접두사(선택 사항)", + "s3DestPrefixDescription": "하나의 객체 키 앞에 붙이는 선택적 경로 접두사입니다. 객체는 {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}에 저장됩니다.", + "s3DestEndpointLabel": "사용자 정의 엔드포인트(선택 사항)", + "s3DestEndpointDescription": "MinIO 또는 Cloudflare R2와 같은 S3 호환 저장소에 대한 S3 엔드포인트를 재정의합니다. 표준 AWS S3의 경우 비워 두십시오.", + "s3DestGzipLabel": "Gzip 압축", + "s3DestGzipDescription": "각 업로드된 객체를 gzip으로 압축합니다. 저장 비용과 업로드 크기를 줄입니다.", + "s3DestFormatTitle": "파일 형식", + "s3DestFormatDescription": "업로드된 각 객체 내에서 이벤트가 직렬화되는 방식입니다.", + "s3DestFormatJsonArrayDescription": "각 객체는 이벤트 기록의 JSON 배열입니다. 대부분의 분석 도구와 호환됩니다.", + "s3DestFormatNdjsonDescription": "각 객체는 한 줄당 하나의 JSON 레코드를 포함합니다(새 줄로 구분된 JSON). Athena, BigQuery, Spark와 호환됩니다.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "각 객체는 헤더 행이 있는 RFC-4180 CSV 파일입니다. 열 이름은 이벤트 데이터 필드에서 파생됩니다.", + "s3DestSaveChanges": "변경 사항 저장", + "s3DestCreateDestination": "대상 생성", + "s3DestUpdatedSuccess": "대상이 성공적으로 업데이트되었습니다", + "s3DestCreatedSuccess": "대상이 성공적으로 생성되었습니다", + "s3DestUpdateFailed": "대상 업데이트에 실패했습니다", + "s3DestCreateFailed": "대상 생성에 실패했습니다", "datadogDestEditTitle": "대상지 수정", "datadogDestAddTitle": "Datadog 대상지 추가", "datadogDestEditDescription": "이 Datadog 이벤트 스트리밍 대상지의 구성을 업데이트하세요.", diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 72a7c21df..15e7e246a 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Videresend arrangementer direkte til din Datadog-konto. Kommer snart.", "streamingTypePickerDescription": "Velg en måltype for å komme i gang.", - "streamingFailedToLoad": "Kan ikke laste inn destinasjoner", + "streamingLastSyncError": "Det oppstod en feil under siste synkronisering", "streamingUnexpectedError": "En uventet feil oppstod.", "streamingFailedToUpdate": "Kunne ikke oppdatere destinasjon", "streamingDeletedSuccess": "Målet ble slettet", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Rediger destinasjon", "S3DestAddTitle": "Legg til S3 destinasjon", "S3DestEditDescription": "Oppdatere konfigurasjonen for denne S3-hendelsesstrømmingsdestinasjonen.", - "S3DestAddDescription": "Konfigurer et nytt S3-endepunkt for å motta organisasjonens hendelser.", + "S3DestAddDescription": "Konfigurer en ny Amazon S3 (eller S3-kompatibel) bucket for å motta din organisasjons hendelser.", + "s3DestTabSettings": "Innstillinger", + "s3DestTabFormat": "Format", + "s3DestNameLabel": "Navn", + "s3DestNamePlaceholder": "Min S3-destinasjon", + "s3DestAccessKeyIdLabel": "AWS tilgangsnøkkel-ID", + "s3DestSecretAccessKeyLabel": "AWS hemmelige tilgangsnøkkel", + "s3DestSecretAccessKeyPlaceholder": "Din AWS secret access key", + "s3DestRegionLabel": "AWS-region", + "s3DestBucketLabel": "Bucket-navn", + "s3DestPrefixLabel": "Nøkkelprefiks (valgfritt)", + "s3DestPrefixDescription": "Valgfritt bane-prefiks lagt til hver objektnøkkel. Objekter er lagret på {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Egendefinert endepunkt (valgfritt)", + "s3DestEndpointDescription": "Overstyr S3-endepunktet for S3-kompatibel lagring som MinIO eller Cloudflare R2. La stå tomt for standard AWS S3.", + "s3DestGzipLabel": "Gzip-komprimering", + "s3DestGzipDescription": "Komprimer hvert opplastede objekt med gzip. Reduserer lagringskostnader og opplastingsstørrelse.", + "s3DestFormatTitle": "Filformat", + "s3DestFormatDescription": "Hvordan hendelser er serialisert inni hvert opplastede objekt.", + "s3DestFormatJsonArrayDescription": "Hvert objekt er et JSON-array av hendelsesposter. Kompatibel med de fleste analyseverktøy.", + "s3DestFormatNdjsonDescription": "Hvert objekt inneholder en JSON-post per linje (nylinje-delt JSON). Kompatibel med Athena, BigQuery, og Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Hvert objekt er en RFC-4180 CSV-fil med en overskriftsrad. Kolonnenavn er avledet fra hendelsesdatafeltene.", + "s3DestSaveChanges": "Lagre endringer", + "s3DestCreateDestination": "Opprett destinasjon", + "s3DestUpdatedSuccess": "Destinasjon oppdatert vellykket", + "s3DestCreatedSuccess": "Destinasjon opprettet vellykket", + "s3DestUpdateFailed": "Kunne ikke oppdatere destinasjon", + "s3DestCreateFailed": "Kunne ikke opprette destinasjon", "datadogDestEditTitle": "Rediger destinasjon", "datadogDestAddTitle": "Legg til Datadog destinasjon", "datadogDestEditDescription": "Oppdatere konfigurasjonen for denne Datadog-hendelsesstrømmingsdestinasjonen.", @@ -3174,7 +3201,7 @@ "publicIpEndpoint": "Endepunkt", "lastTriggeredAt": "Siste utløste", "reject": "Avvis", - "uptimeDaysAgo": "{count} days ago", + "uptimeDaysAgo": "{count} dager siden", "uptimeToday": "I dag", "uptimeNoDataAvailable": "Ingen data tilgjengelig", "uptimeSuffix": "oppetid", diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 5218c3388..de4ccdd33 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Stuur gebeurtenissen rechtstreeks door naar je Datadog account. Binnenkort beschikbaar.", "streamingTypePickerDescription": "Kies een bestemmingstype om te beginnen.", - "streamingFailedToLoad": "Laden van bestemmingen mislukt", + "streamingLastSyncError": "Er is een fout opgetreden bij de laatste synchronisatie", "streamingUnexpectedError": "Er is een onverwachte fout opgetreden.", "streamingFailedToUpdate": "Bijwerken bestemming mislukt", "streamingDeletedSuccess": "Bestemming succesvol verwijderd", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Bestemming bewerken", "S3DestAddTitle": "S3-bestemming toevoegen", "S3DestEditDescription": "Werk de configuratie bij voor deze S3-gebeurtenisstreamingbestemming.", - "S3DestAddDescription": "Configureer een nieuw S3-eindpunt om de gebeurtenissen van uw organisatie te ontvangen.", + "S3DestAddDescription": "Configureer een nieuwe Amazon S3 (of S3-compatibele) bucket om de gebeurtenissen van uw organisatie te ontvangen.", + "s3DestTabSettings": "Instellingen", + "s3DestTabFormat": "Formaat", + "s3DestNameLabel": "Naam", + "s3DestNamePlaceholder": "Mijn S3-bestemming", + "s3DestAccessKeyIdLabel": "AWS-toegangssleutel-ID", + "s3DestSecretAccessKeyLabel": "AWS Geheime Toegangssleutel", + "s3DestSecretAccessKeyPlaceholder": "Uw AWS geheime toegangssleutel", + "s3DestRegionLabel": "AWS-regio", + "s3DestBucketLabel": "Bucketnaam", + "s3DestPrefixLabel": "Sleutelvoorvoegsel (optioneel)", + "s3DestPrefixDescription": "Optioneel padvoorvoegsel dat aan elke object sleutel wordt toegevoegd. Objecten worden opgeslagen op {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Aangepast Eindpunt (optioneel)", + "s3DestEndpointDescription": "Overschrijf het S3-eindpunt voor S3-compatibele opslag zoals MinIO of Cloudflare R2. Laat leeg voor standaard AWS S3.", + "s3DestGzipLabel": "Gzip-compressie", + "s3DestGzipDescription": "Comprimeer elk geüpload object met gzip. Verlaagt opslagkosten en uploadgrootte.", + "s3DestFormatTitle": "Bestandsformaat", + "s3DestFormatDescription": "Hoe gebeurtenissen binnen elk geüpload object worden geserialiseerd.", + "s3DestFormatJsonArrayDescription": "Elk object is een JSON-array van gebeurtenisrecords. Compatibel met de meeste analysetools.", + "s3DestFormatNdjsonDescription": "Elk object bevat één JSON-record per regel (nieuwregel-gescheiden JSON). Compatibel met Athena, BigQuery en Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Elk object is een RFC-4180 CSV-bestand met een kopregel. Kolomnamen zijn afgeleid van de gebeurtenis gegevensvelden.", + "s3DestSaveChanges": "Wijzigingen opslaan", + "s3DestCreateDestination": "Bestemming maken", + "s3DestUpdatedSuccess": "Bestemming succesvol bijgewerkt", + "s3DestCreatedSuccess": "Bestemming succesvol gecreëerd", + "s3DestUpdateFailed": "Bijwerken bestemming mislukt", + "s3DestCreateFailed": "Aanmaken bestemming mislukt", "datadogDestEditTitle": "Bestemming bewerken", "datadogDestAddTitle": "Datadog-bestemming toevoegen", "datadogDestEditDescription": "Werk de configuratie bij voor deze Datadog-gebeurtenisstreamingbestemming.", diff --git a/messages/pl-PL.json b/messages/pl-PL.json index df4a391fc..8189a927d 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Przekaż wydarzenia bezpośrednio do Twojego konta Datadog. Już wkrótce.", "streamingTypePickerDescription": "Wybierz typ docelowy, aby rozpocząć.", - "streamingFailedToLoad": "Nie udało się załadować miejsc docelowych", + "streamingLastSyncError": "Wystąpił błąd podczas ostatniej synchronizacji", "streamingUnexpectedError": "Wystąpił nieoczekiwany błąd.", "streamingFailedToUpdate": "Nie udało się zaktualizować miejsca docelowego", "streamingDeletedSuccess": "Cel usunięty pomyślnie", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Edytuj Miejsce Docelowe", "S3DestAddTitle": "Dodaj Miejsce Docelowe S3", "S3DestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń S3.", - "S3DestAddDescription": "Skonfiguruj nowy punkt końcowy S3, aby odbierać zdarzenia Twojej organizacji.", + "S3DestAddDescription": "Skonfiguruj nowy zasobnik Amazon S3 (lub zgodny z S3), aby otrzymywać zdarzenia twojej organizacji.", + "s3DestTabSettings": "Ustawienia", + "s3DestTabFormat": "Format", + "s3DestNameLabel": "Nazwa", + "s3DestNamePlaceholder": "Moje miejsce docelowe S3", + "s3DestAccessKeyIdLabel": "AWS Access Key ID", + "s3DestSecretAccessKeyLabel": "AWS Secret Access Key", + "s3DestSecretAccessKeyPlaceholder": "Twój AWS Secret Access Key", + "s3DestRegionLabel": "Region AWS", + "s3DestBucketLabel": "Nazwa kubła", + "s3DestPrefixLabel": "Prefiks klucza (opcjonalnie)", + "s3DestPrefixDescription": "Opcjonalny prefiks ścieżki dołączony do każdego klucza obiektu. Obiekty są przechowywane w {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Niestandardowy punkt końcowy (opcjonalnie)", + "s3DestEndpointDescription": "Nadpisz punkt końcowy S3 dla zgodnego przechowywania danych, takiego jak MinIO lub Cloudflare R2. Pozostaw puste dla standardowego AWS S3.", + "s3DestGzipLabel": "Kompresja Gzip", + "s3DestGzipDescription": "Skompresuj każdy przesłany obiekt za pomocą gzip. Zmniejsza koszty przechowywania i rozmiar przesyłu.", + "s3DestFormatTitle": "Format pliku", + "s3DestFormatDescription": "Jak zdarzenia są serializowane w każdym przesłanym obiekcie.", + "s3DestFormatJsonArrayDescription": "Każdy obiekt to tablica JSON z rekordami zdarzeń. Zgodne z większością narzędzi analitycznych.", + "s3DestFormatNdjsonDescription": "Każdy obiekt zawiera jeden rekord JSON na linię (nowa linia-dzielone JSON). Zgodne z Athena, BigQuery i Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Każdy obiekt to plik CSV zgodny z RFC-4180 z wierszem nagłówka. Nazwy kolumn pochodzą z pól danych zdarzeń.", + "s3DestSaveChanges": "Zapisz zmiany", + "s3DestCreateDestination": "Utwórz miejsce docelowe", + "s3DestUpdatedSuccess": "Miejsce docelowe zaktualizowane pomyślnie", + "s3DestCreatedSuccess": "Miejsce docelowe utworzone pomyślnie", + "s3DestUpdateFailed": "Nie udało się zaktualizować miejsca docelowego", + "s3DestCreateFailed": "Nie udało się utworzyć miejsca docelowego", "datadogDestEditTitle": "Edytuj Miejsce Docelowe", "datadogDestAddTitle": "Dodaj Miejsce Docelowe Datadog", "datadogDestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń Datadog.", diff --git a/messages/pt-PT.json b/messages/pt-PT.json index bc683dc77..a3baabef8 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Encaminha eventos diretamente para a sua conta no Datadog. Em breve.", "streamingTypePickerDescription": "Escolha um tipo de destino para começar.", - "streamingFailedToLoad": "Falha ao carregar destinos", + "streamingLastSyncError": "Ocorreu um erro na última sincronização", "streamingUnexpectedError": "Ocorreu um erro inesperado.", "streamingFailedToUpdate": "Falha ao atualizar destino", "streamingDeletedSuccess": "Destino apagado com sucesso", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Editar Destino", "S3DestAddTitle": "Adicionar Destino S3", "S3DestEditDescription": "Atualize a configuração para este destino de streaming de eventos S3.", - "S3DestAddDescription": "Configure um novo endpoint S3 para receber os eventos da sua organização.", + "S3DestAddDescription": "Configure um novo bucket Amazon S3 (ou compatível com S3) para receber os eventos da sua organização.", + "s3DestTabSettings": "Configurações", + "s3DestTabFormat": "Formato", + "s3DestNameLabel": "Nome", + "s3DestNamePlaceholder": "Meu destino S3", + "s3DestAccessKeyIdLabel": "ID da Chave de Acesso AWS", + "s3DestSecretAccessKeyLabel": "Chave de Acesso Secreta AWS", + "s3DestSecretAccessKeyPlaceholder": "Sua chave de acesso secreta AWS", + "s3DestRegionLabel": "Região AWS", + "s3DestBucketLabel": "Nome do Bucket", + "s3DestPrefixLabel": "Prefixo da Chave (opcional)", + "s3DestPrefixDescription": "Prefixo de caminho opcional adicionado a cada chave de objeto. Os objetos são armazenados em {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Endpoint Personalizado (opcional)", + "s3DestEndpointDescription": "Substitua o endpoint S3 por armazenamento compatível com S3, como MinIO ou Cloudflare R2. Deixe em branco para o padrão AWS S3.", + "s3DestGzipLabel": "Compressão Gzip", + "s3DestGzipDescription": "Comprime cada objeto carregado com gzip. Reduz custos de armazenamento e tamanho de upload.", + "s3DestFormatTitle": "Formato de Arquivo", + "s3DestFormatDescription": "Como os eventos são serializados dentro de cada objeto carregado.", + "s3DestFormatJsonArrayDescription": "Cada objeto é um array JSON de registros de eventos. Compatível com a maioria das ferramentas de análise.", + "s3DestFormatNdjsonDescription": "Cada objeto contém um registro JSON por linha (JSON delimitado por nova linha). Compatível com Athena, BigQuery e Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Cada objeto é um arquivo CSV RFC-4180 com uma linha de cabeçalho. Nomes de colunas são derivados dos campos de dados do evento.", + "s3DestSaveChanges": "Salvar Alterações", + "s3DestCreateDestination": "Criar Destino", + "s3DestUpdatedSuccess": "Destino atualizado com sucesso", + "s3DestCreatedSuccess": "Destino criado com sucesso", + "s3DestUpdateFailed": "Falha ao atualizar destino", + "s3DestCreateFailed": "Falha ao criar destino", "datadogDestEditTitle": "Editar Destino", "datadogDestAddTitle": "Adicionar Destino Datadog", "datadogDestEditDescription": "Atualize a configuração para este destino de streaming de eventos Datadog.", diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 46bb5911a..766268cb9 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Перенаправлять события непосредственно на ваш аккаунт в Datadog. Скоро будет доступно.", "streamingTypePickerDescription": "Выберите тип назначения, чтобы начать.", - "streamingFailedToLoad": "Не удалось загрузить места назначения", + "streamingLastSyncError": "Во время последней синхронизации произошла ошибка", "streamingUnexpectedError": "Произошла непредвиденная ошибка.", "streamingFailedToUpdate": "Не удалось обновить место назначения", "streamingDeletedSuccess": "Адрес назначения успешно удален", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Редактировать пункт назначения", "S3DestAddTitle": "Добавить S3 пункт назначения", "S3DestEditDescription": "Обновите конфигурацию для этого S3 пункта назначения потоковых событий.", - "S3DestAddDescription": "Настройте новую S3 конечную точку для получения событий вашей организации.", + "S3DestAddDescription": "Настройте новый Amazon S3 (или совместимое S3) хранилище для получения событий вашей организации.", + "s3DestTabSettings": "Настройки", + "s3DestTabFormat": "Формат", + "s3DestNameLabel": "Имя", + "s3DestNamePlaceholder": "Моя S3 конечная точка", + "s3DestAccessKeyIdLabel": "Идентификатор ключа доступа AWS", + "s3DestSecretAccessKeyLabel": "Секретный ключ доступа AWS", + "s3DestSecretAccessKeyPlaceholder": "Ваш секретный ключ доступа AWS", + "s3DestRegionLabel": "Регион AWS", + "s3DestBucketLabel": "Имя хранилища", + "s3DestPrefixLabel": "Префикс ключа (по желанию)", + "s3DestPrefixDescription": "Необязательный префикс пути, добавляется к каждому ключу объекта. Объекты хранятся в {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.", + "s3DestEndpointLabel": "Пользовательская конечная точка (по желанию)", + "s3DestEndpointDescription": "Переопределите конечную точку S3 для совместимого хранилища, такого как MinIO или Cloudflare R2. Оставьте пустым для стандартного AWS S3.", + "s3DestGzipLabel": "Сжатие Gzip", + "s3DestGzipDescription": "Сжимайте каждый загруженный объект с помощью gzip. Уменьшает стоимость хранения и размер загрузки.", + "s3DestFormatTitle": "Формат файла", + "s3DestFormatDescription": "Как события сериализуются внутри каждого загруженного объекта.", + "s3DestFormatJsonArrayDescription": "Каждый объект — это JSON массив записей событий. Совместим с большинством аналитических инструментов.", + "s3DestFormatNdjsonDescription": "Каждый объект содержит одну запись JSON на строку (JSON, разделённый новой строкой). Совместим с Athena, BigQuery и Spark.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Каждый объект представляет собой CSV файл по стандарту RFC-4180 с заголовочной строкой. Имена столбцов выведены из полей данных событий.", + "s3DestSaveChanges": "Сохранить изменения", + "s3DestCreateDestination": "Создать конечную точку", + "s3DestUpdatedSuccess": "Конечная точка успешно обновлена", + "s3DestCreatedSuccess": "Конечная точка успешно создана", + "s3DestUpdateFailed": "Не удалось обновить конечную точку", + "s3DestCreateFailed": "Не удалось создать конечную точку", "datadogDestEditTitle": "Редактировать пункт назначения", "datadogDestAddTitle": "Добавить пункт назначения Datadog", "datadogDestEditDescription": "Обновите конфигурацию для этого пункта назначения потоковых событий Datadog.", diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 0bcd5d313..145a59eee 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "Olayları doğrudan Datadog hesabınıza iletin. Yakında gelicek.", "streamingTypePickerDescription": "Başlamak için bir hedef türü seçin.", - "streamingFailedToLoad": "Hedefler yüklenemedi", + "streamingLastSyncError": "Son senkronizasyonda bir hata oluştu", "streamingUnexpectedError": "Beklenmeyen bir hata oluştu.", "streamingFailedToUpdate": "Hedef güncellenemedi", "streamingDeletedSuccess": "Hedef başarıyla silindi", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "Hedefi Düzenle", "S3DestAddTitle": "S3 Hedefi Ekle", "S3DestEditDescription": "Bu S3 olay akışı hedefi için yapılandırmayı güncelleyin.", - "S3DestAddDescription": "Kuruluşunuzun olaylarını almak için yeni bir S3 uç noktası yapılandırın.", + "S3DestAddDescription": "Kuruluşunuzun etkinliklerini almak için yeni bir Amazon S3 (veya S3-uyumlu) kovası yapılandırın.", + "s3DestTabSettings": "Ayarlar", + "s3DestTabFormat": "Biçim", + "s3DestNameLabel": "Ad", + "s3DestNamePlaceholder": "Benim S3 hedefim", + "s3DestAccessKeyIdLabel": "AWS Erişim Anahtar Kimliği", + "s3DestSecretAccessKeyLabel": "AWS Gizli Erişim Anahtarı", + "s3DestSecretAccessKeyPlaceholder": "AWS gizli erişim anahtarınız", + "s3DestRegionLabel": "AWS Bölgesi", + "s3DestBucketLabel": "Kova Adı", + "s3DestPrefixLabel": "Anahtar Ön Eki (isteğe bağlı)", + "s3DestPrefixDescription": "Her nesne anahtarının önüne eklenen isteğe bağlı yol öneki. Nesneler {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename} konumunda saklanır.", + "s3DestEndpointLabel": "Özel Uç Nokta (isteğe bağlı)", + "s3DestEndpointDescription": "MinIO veya Cloudflare R2 gibi S3-uyumlu depolama için S3 uç noktasını geçersiz kılın. Standart AWS S3 için boş bırakın.", + "s3DestGzipLabel": "Gzip sıkıştırması", + "s3DestGzipDescription": "Her yüklü nesneyi gzip ile sıkıştırın. Depolama maliyetlerini ve yükleme boyutunu azaltır.", + "s3DestFormatTitle": "Dosya Biçimi", + "s3DestFormatDescription": "Etkinliklerin her yüklendiği nesne içinde nasıl serileştirildiği.", + "s3DestFormatJsonArrayDescription": "Her nesne bir olay kayıtlarının JSON dizisidir. Çoğu analiz aracıyla uyumludur.", + "s3DestFormatNdjsonDescription": "Her nesne satır başına bir JSON kaydı içerir (yeni satır ile ayrılmış JSON). Athena, BigQuery ve Spark ile uyumludur.", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "Her nesne, bir başlık satırı ile birlikte RFC-4180 CSV dosyasıdır. Sütun isimleri olay verileri alanlarından türetilmiştir.", + "s3DestSaveChanges": "Değişiklikleri Kaydet", + "s3DestCreateDestination": "Hedef Oluştur", + "s3DestUpdatedSuccess": "Hedef başarıyla güncellendi", + "s3DestCreatedSuccess": "Hedef başarıyla oluşturuldu", + "s3DestUpdateFailed": "Hedef güncellenemedi", + "s3DestCreateFailed": "Hedef oluşturulamadı", "datadogDestEditTitle": "Hedefi Düzenle", "datadogDestAddTitle": "Datadog Hedefi Ekle", "datadogDestEditDescription": "Bu Datadog olay akışı hedefi için yapılandırmayı güncelleyin.", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index e61e0c61a..f0162f95d 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -3062,7 +3062,7 @@ "streamingDatadogTitle": "Datadog", "streamingDatadogDescription": "直接转发事件到您的Datadog 帐户。即将推出。", "streamingTypePickerDescription": "选择要开始的目标类型。", - "streamingFailedToLoad": "加载目的地失败", + "streamingLastSyncError": "最后一次同步时发生错误", "streamingUnexpectedError": "发生意外错误.", "streamingFailedToUpdate": "更新目标失败", "streamingDeletedSuccess": "目标删除成功", @@ -3079,7 +3079,34 @@ "S3DestEditTitle": "编辑目的地", "S3DestAddTitle": "添加 S3 目的地", "S3DestEditDescription": "更新此 S3 事件流目的地的配置。", - "S3DestAddDescription": "配置新的 S3 终端以接收您的组织事件。", + "S3DestAddDescription": "配置一个新的 Amazon S3(或兼容 S3 的)存储桶以接收您的组织事件。", + "s3DestTabSettings": "设置", + "s3DestTabFormat": "格式", + "s3DestNameLabel": "名称", + "s3DestNamePlaceholder": "我的 S3 目的地", + "s3DestAccessKeyIdLabel": "AWS 访问密钥 ID", + "s3DestSecretAccessKeyLabel": "AWS 秘密访问密钥", + "s3DestSecretAccessKeyPlaceholder": "您的 AWS 密钥", + "s3DestRegionLabel": "AWS 地区", + "s3DestBucketLabel": "存储桶名称", + "s3DestPrefixLabel": "密钥前缀(可选)", + "s3DestPrefixDescription": "每个对象密钥前加的可选路径前缀。对象存储在 {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}。", + "s3DestEndpointLabel": "自定义端点(可选)", + "s3DestEndpointDescription": "替代 S3 端点用于 MinIO 或 Cloudflare R2 等兼容 S3 的存储。标准 AWS S3 留空。", + "s3DestGzipLabel": "Gzip 压缩", + "s3DestGzipDescription": "使用 gzip 压缩每个上传的对象。减少存储成本和上传大小。", + "s3DestFormatTitle": "文件格式", + "s3DestFormatDescription": "事件在每个上传对象内的序列化方式。", + "s3DestFormatJsonArrayDescription": "每个对象是事件记录的 JSON 数组。兼容大多数分析工具。", + "s3DestFormatNdjsonDescription": "每个对象每行包含一个 JSON 记录(换行分隔的 JSON)。兼容 Athena、BigQuery 和 Spark。", + "s3DestFormatCsvTitle": "CSV", + "s3DestFormatCsvDescription": "每个对象是带有标题行的 RFC-4180 CSV 文件。列名来自事件数据字段。", + "s3DestSaveChanges": "保存更改", + "s3DestCreateDestination": "创建目的地", + "s3DestUpdatedSuccess": "目的地更新成功", + "s3DestCreatedSuccess": "目的地创建成功", + "s3DestUpdateFailed": "更新目的地失败", + "s3DestCreateFailed": "创建目的地失败", "datadogDestEditTitle": "编辑目的地", "datadogDestAddTitle": "添加 Datadog 目的地", "datadogDestEditDescription": "更新此 Datadog 事件流目的地的配置。", diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index 0f1914fad..229fc9ff0 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -332,6 +332,7 @@ export const connectionAuditLog = pgTable( clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }), + clientEndpoint: text("clientEndpoint"), userId: text("userId").references(() => users.userId, { onDelete: "cascade" }), @@ -439,6 +440,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..ae7360780 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -332,6 +332,7 @@ export const connectionAuditLog = sqliteTable( clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }), + clientEndpoint: text("clientEndpoint"), userId: text("userId").references(() => users.userId, { onDelete: "cascade" }), @@ -445,6 +446,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/logConnectionAudit.ts b/server/private/lib/logConnectionAudit.ts index 039b75ec9..ce4856da0 100644 --- a/server/private/lib/logConnectionAudit.ts +++ b/server/private/lib/logConnectionAudit.ts @@ -46,6 +46,7 @@ export interface ConnectionLogRecord { orgId: string; siteId: number; clientId: number | null; + clientEndpoint: string | null; userId: string | null; sourceAddr: string; destAddr: string; diff --git a/server/private/lib/logStreaming/LogStreamingManager.ts b/server/private/lib/logStreaming/LogStreamingManager.ts index 1df67c886..03efc2809 100644 --- a/server/private/lib/logStreaming/LogStreamingManager.ts +++ b/server/private/lib/logStreaming/LogStreamingManager.ts @@ -30,10 +30,12 @@ import { LOG_TYPES, LogEvent, DestinationFailureState, - HttpConfig + HttpConfig, + S3Config } from "./types"; import { LogDestinationProvider } from "./providers/LogDestinationProvider"; import { HttpLogDestination } from "./providers/HttpLogDestination"; +import { S3LogDestination } from "./providers/S3LogDestination"; import type { EventStreamingDestination } from "@server/db"; // --------------------------------------------------------------------------- @@ -72,11 +74,11 @@ const MAX_CATCHUP_BATCHES = 20; * After the last entry the max value is re-used. */ const BACKOFF_SCHEDULE_MS = [ - 60_000, // 1 min (failure 1) - 2 * 60_000, // 2 min (failure 2) - 5 * 60_000, // 5 min (failure 3) - 10 * 60_000, // 10 min (failure 4) - 30 * 60_000 // 30 min (failure 5+) + 60_000, // 1 min (failure 1) + 2 * 60_000, // 2 min (failure 2) + 5 * 60_000, // 5 min (failure 3) + 10 * 60_000, // 10 min (failure 4) + 30 * 60_000 // 30 min (failure 5+) ]; /** @@ -204,7 +206,10 @@ export class LogStreamingManager { this.pollTimer = null; this.runPoll() .catch((err) => - logger.error("LogStreamingManager: unexpected poll error", err) + logger.error( + "LogStreamingManager: unexpected poll error", + err + ) ) .finally(() => { if (this.isRunning) { @@ -275,10 +280,13 @@ export class LogStreamingManager { } // Decrypt and parse config – skip destination if either step fails - let configFromDb: HttpConfig; + let configFromDb: unknown; try { - const decryptedConfig = decrypt(dest.config, config.getRawConfig().server.secret!); - configFromDb = JSON.parse(decryptedConfig) as HttpConfig; + const decryptedConfig = decrypt( + dest.config, + config.getRawConfig().server.secret! + ); + configFromDb = JSON.parse(decryptedConfig); } catch (err) { logger.error( `LogStreamingManager: destination ${dest.destinationId} has invalid or undecryptable config`, @@ -305,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; @@ -312,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}`, @@ -322,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)) { @@ -330,6 +347,7 @@ export class LogStreamingManager { `LogStreamingManager: destination ${dest.destinationId} recovered` ); } + await this.clearDestinationError(dest.destinationId); } } @@ -362,7 +380,10 @@ export class LogStreamingManager { .from(eventStreamingCursors) .where( and( - eq(eventStreamingCursors.destinationId, dest.destinationId), + eq( + eventStreamingCursors.destinationId, + dest.destinationId + ), eq(eventStreamingCursors.logType, logType) ) ) @@ -431,9 +452,7 @@ export class LogStreamingManager { if (rows.length === 0) break; - const events = rows.map((row) => - this.rowToLogEvent(logType, row) - ); + const events = rows.map((row) => this.rowToLogEvent(logType, row)); // Throws on failure – caught by the caller which applies back-off await provider.send(events); @@ -677,8 +696,7 @@ export class LogStreamingManager { break; } - const orgId = - typeof row.orgId === "string" ? row.orgId : ""; + const orgId = typeof row.orgId === "string" ? row.orgId : ""; return { id: row.id, @@ -708,6 +726,8 @@ export class LogStreamingManager { switch (type) { case "http": return new HttpLogDestination(config as HttpConfig); + case "s3": + return new S3LogDestination(config as S3Config); // Future providers: // case "datadog": return new DatadogLogDestination(config as DatadogConfig); default: @@ -749,6 +769,45 @@ export class LogStreamingManager { // DB helpers // ------------------------------------------------------------------------- + private async setDestinationError( + destinationId: number, + errorMessage: string + ): Promise { + // Truncate to 1000 chars so it fits comfortably in the text column. + const truncated = errorMessage.slice(0, 1000); + try { + await db + .update(eventStreamingDestinations) + .set({ lastError: truncated, lastErrorAt: Date.now() }) + .where( + eq(eventStreamingDestinations.destinationId, destinationId) + ); + } catch (err) { + logger.warn( + `LogStreamingManager: could not persist error status for destination ${destinationId}`, + err + ); + } + } + + private async clearDestinationError(destinationId: number): Promise { + try { + // Only update if there is actually an error stored, to avoid + // unnecessary writes on every successful poll cycle. + await db + .update(eventStreamingDestinations) + .set({ lastError: null, lastErrorAt: null }) + .where( + eq(eventStreamingDestinations.destinationId, destinationId) + ); + } catch (err) { + logger.warn( + `LogStreamingManager: could not clear error status for destination ${destinationId}`, + err + ); + } + } + private async loadEnabledDestinations(): Promise< EventStreamingDestination[] > { diff --git a/server/private/lib/logStreaming/providers/S3LogDestination.ts b/server/private/lib/logStreaming/providers/S3LogDestination.ts new file mode 100644 index 000000000..2e84e667d --- /dev/null +++ b/server/private/lib/logStreaming/providers/S3LogDestination.ts @@ -0,0 +1,279 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; +import { gzip as gzipCallback } from "zlib"; +import { promisify } from "util"; +import { randomUUID } from "crypto"; +import logger from "@server/logger"; +import { LogEvent, S3Config, S3PayloadFormat } from "../types"; +import { LogDestinationProvider } from "./LogDestinationProvider"; + +const gzipAsync = promisify(gzipCallback); + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +/** Maximum time (ms) to wait for a single S3 PutObject response. */ +const REQUEST_TIMEOUT_MS = 60_000; + +/** Default payload format when none is specified in the config. */ +const DEFAULT_FORMAT: S3PayloadFormat = "json_array"; + +// --------------------------------------------------------------------------- +// S3LogDestination +// --------------------------------------------------------------------------- + +/** + * Forwards a batch of log events to an S3-compatible object store by + * uploading a single object per `send()` call. + * + * **Object key layout** + * ``` + * {prefix}/{logType}/{YYYY}/{MM}/{DD}/{HH}-{mm}-{ss}-{uuid}.{ext}[.gz] + * ``` + * - `prefix` – from `config.prefix` (default: empty – key starts at logType) + * - `logType` – one of "request", "action", "access", "connection" + * - Date components are derived from the upload time (UTC) + * - `ext` – `json` | `ndjson` | `csv` + * - `.gz` – appended when `config.gzip` is true + * + * **Payload formats** (controlled by `config.format`): + * - `json_array` (default) – body is a JSON array of event objects. + * - `ndjson` – one JSON object per line (newline-delimited). + * - `csv` – RFC-4180 CSV with a header row; columns are the + * union of all field names in the batch's event data. + * + * **Compression**: when `config.gzip` is `true` the body is gzip-compressed + * before upload and `Content-Encoding: gzip` is set on the object. + * + * **Custom endpoint**: set `config.endpoint` to target any S3-compatible + * storage service (e.g. MinIO, Cloudflare R2). + */ +export class S3LogDestination implements LogDestinationProvider { + readonly type = "s3"; + + private readonly config: S3Config; + + constructor(config: S3Config) { + this.config = config; + } + + // ----------------------------------------------------------------------- + // LogDestinationProvider implementation + // ----------------------------------------------------------------------- + + async send(events: LogEvent[]): Promise { + if (events.length === 0) return; + + const format = this.config.format ?? DEFAULT_FORMAT; + const useGzip = this.config.gzip ?? false; + const logType = events[0].logType; + + const rawBody = this.serialize(events, format); + const bodyBuffer = Buffer.from(rawBody, "utf-8"); + + let uploadBody: Buffer; + let contentEncoding: string | undefined; + + if (useGzip) { + uploadBody = (await gzipAsync(bodyBuffer)) as Buffer; + contentEncoding = "gzip"; + } else { + uploadBody = bodyBuffer; + } + + const key = this.buildObjectKey(logType, format, useGzip); + const contentType = this.contentType(format); + + const clientConfig: ConstructorParameters[0] = { + region: this.config.region, + credentials: { + accessKeyId: this.config.accessKeyId, + secretAccessKey: this.config.secretAccessKey + }, + requestHandler: { + requestTimeout: REQUEST_TIMEOUT_MS + } + }; + + if (this.config.endpoint?.trim()) { + clientConfig.endpoint = this.config.endpoint.trim(); + } + + const client = new S3Client(clientConfig); + + try { + await client.send( + new PutObjectCommand({ + Bucket: this.config.bucket, + Key: key, + Body: uploadBody, + ContentType: contentType, + ...(contentEncoding + ? { ContentEncoding: contentEncoding } + : {}) + }) + ); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + throw new Error( + `S3LogDestination: failed to upload object "${key}" ` + + `to bucket "${this.config.bucket}" – ${msg}` + ); + } + } + + // ----------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------- + + /** + * Construct a unique S3 object key for the given log type and format. + * Keys are partitioned by logType and date so they can be queried or + * lifecycle-managed independently. + */ + private buildObjectKey( + logType: string, + format: S3PayloadFormat, + gzip: boolean + ): string { + const now = new Date(); + const year = now.getUTCFullYear(); + const month = String(now.getUTCMonth() + 1).padStart(2, "0"); + const day = String(now.getUTCDate()).padStart(2, "0"); + const hh = String(now.getUTCHours()).padStart(2, "0"); + const mm = String(now.getUTCMinutes()).padStart(2, "0"); + const ss = String(now.getUTCSeconds()).padStart(2, "0"); + const uid = randomUUID(); + + const ext = + format === "csv" ? "csv" : format === "ndjson" ? "ndjson" : "json"; + const fileName = `${hh}-${mm}-${ss}-${uid}.${ext}${gzip ? ".gz" : ""}`; + + const rawPrefix = (this.config.prefix ?? "").trim().replace(/\/+$/, ""); + const parts = [ + rawPrefix, + logType, + `${year}/${month}/${day}`, + fileName + ].filter((p) => p !== ""); + + return parts.join("/"); + } + + private contentType(format: S3PayloadFormat): string { + switch (format) { + case "csv": + return "text/csv; charset=utf-8"; + case "ndjson": + return "application/x-ndjson"; + default: + return "application/json"; + } + } + + private serialize(events: LogEvent[], format: S3PayloadFormat): string { + switch (format) { + case "json_array": + return JSON.stringify(events.map(toPayload)); + case "ndjson": + return events + .map((e) => JSON.stringify(toPayload(e))) + .join("\n"); + case "csv": + return toCsv(events); + } + } +} + +// --------------------------------------------------------------------------- +// Payload helpers +// --------------------------------------------------------------------------- + +function toPayload(event: LogEvent): unknown { + return { + event: event.logType, + timestamp: new Date(event.timestamp * 1000).toISOString(), + data: event.data + }; +} + +/** + * Convert a batch of events to RFC-4180 CSV. + * + * The column set is the union of `event`, `timestamp`, and all keys present in + * `event.data` across the batch, preserving insertion order. Values that + * contain commas, double-quotes, or newlines are quoted and escaped. + */ +function toCsv(events: LogEvent[]): string { + if (events.length === 0) return ""; + + // Collect all unique data keys in stable order + const keySet = new LinkedSet(); + keySet.add("event"); + keySet.add("timestamp"); + for (const e of events) { + for (const k of Object.keys(e.data)) { + keySet.add(k); + } + } + const headers = keySet.toArray(); + + const rows: string[] = [headers.map(csvEscape).join(",")]; + + for (const e of events) { + const flat: Record = { + event: e.logType, + timestamp: new Date(e.timestamp * 1000).toISOString(), + ...e.data + }; + rows.push( + headers.map((h) => csvEscape(flattenValue(flat[h]))).join(",") + ); + } + + return rows.join("\n"); +} + +/** Flatten a value to a plain string suitable for a CSV cell. */ +function flattenValue(value: unknown): string { + if (value === null || value === undefined) return ""; + if (typeof value === "object") return JSON.stringify(value); + return String(value); +} + +/** RFC-4180 CSV escaping. */ +function csvEscape(value: string): string { + if (/[",\n\r]/.test(value)) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; +} + +// --------------------------------------------------------------------------- +// Minimal ordered set (preserves insertion order, deduplicates) +// --------------------------------------------------------------------------- + +class LinkedSet { + private readonly map = new Map(); + + add(value: T): void { + this.map.set(value, true); + } + + toArray(): T[] { + return Array.from(this.map.keys()); + } +} diff --git a/server/private/lib/logStreaming/types.ts b/server/private/lib/logStreaming/types.ts index 1bcd25a66..193a5ff6b 100644 --- a/server/private/lib/logStreaming/types.ts +++ b/server/private/lib/logStreaming/types.ts @@ -107,6 +107,40 @@ export interface HttpConfig { bodyTemplate?: string; } +// --------------------------------------------------------------------------- +// S3 destination configuration +// --------------------------------------------------------------------------- + +/** + * Controls how the batch of events is serialised into each S3 object. + * + * - `json_array` – `[{…}, {…}]` – default; each object is a JSON array. + * - `ndjson` – `{…}\n{…}` – newline-delimited JSON, one object per line. + * - `csv` – RFC-4180 CSV with a header row derived from the event fields. + */ +export type S3PayloadFormat = "json_array" | "ndjson" | "csv"; + +export interface S3Config { + /** Human-readable label for the destination */ + name: string; + /** AWS Access Key ID */ + accessKeyId: string; + /** AWS Secret Access Key */ + secretAccessKey: string; + /** AWS region (e.g. "us-east-1") */ + region: string; + /** Target S3 bucket name */ + bucket: string; + /** Optional key prefix – appended before the auto-generated path */ + prefix?: string; + /** Override the S3 endpoint for S3-compatible storage (e.g. MinIO, R2) */ + endpoint?: string; + /** How events are serialised into each object. Defaults to "json_array". */ + format: S3PayloadFormat; + /** Whether to gzip-compress the object before upload. */ + gzip: boolean; +} + // --------------------------------------------------------------------------- // Per-destination per-log-type cursor (reflects the DB table) // --------------------------------------------------------------------------- diff --git a/server/private/routers/auditLogs/queryConnectionAuditLog.ts b/server/private/routers/auditLogs/queryConnectionAuditLog.ts index 715652838..930ee6111 100644 --- a/server/private/routers/auditLogs/queryConnectionAuditLog.ts +++ b/server/private/routers/auditLogs/queryConnectionAuditLog.ts @@ -124,15 +124,11 @@ function getWhere(data: Q) { data.clientId ? eq(connectionAuditLog.clientId, data.clientId) : undefined, - data.siteId - ? eq(connectionAuditLog.siteId, data.siteId) - : undefined, + data.siteId ? eq(connectionAuditLog.siteId, data.siteId) : undefined, data.siteResourceId ? eq(connectionAuditLog.siteResourceId, data.siteResourceId) : undefined, - data.userId - ? eq(connectionAuditLog.userId, data.userId) - : undefined + data.userId ? eq(connectionAuditLog.userId, data.userId) : undefined ); } @@ -144,6 +140,7 @@ export function queryConnection(data: Q) { orgId: connectionAuditLog.orgId, siteId: connectionAuditLog.siteId, clientId: connectionAuditLog.clientId, + clientEndpoint: connectionAuditLog.clientEndpoint, userId: connectionAuditLog.userId, sourceAddr: connectionAuditLog.sourceAddr, destAddr: connectionAuditLog.destAddr, @@ -203,10 +200,7 @@ async function enrichWithDetails( ]; // Fetch resource details from main database - const resourceMap = new Map< - number, - { name: string; niceId: string } - >(); + const resourceMap = new Map(); if (siteResourceIds.length > 0) { const resourceDetails = await primaryDb .select({ @@ -268,10 +262,7 @@ async function enrichWithDetails( } // Fetch user details from main database - const userMap = new Map< - string, - { email: string | null } - >(); + const userMap = new Map(); if (userIds.length > 0) { const userDetails = await primaryDb .select({ @@ -290,29 +281,25 @@ async function enrichWithDetails( return logs.map((log) => ({ ...log, resourceName: log.siteResourceId - ? resourceMap.get(log.siteResourceId)?.name ?? null + ? (resourceMap.get(log.siteResourceId)?.name ?? null) : null, resourceNiceId: log.siteResourceId - ? resourceMap.get(log.siteResourceId)?.niceId ?? null - : null, - siteName: log.siteId - ? siteMap.get(log.siteId)?.name ?? null + ? (resourceMap.get(log.siteResourceId)?.niceId ?? null) : null, + siteName: log.siteId ? (siteMap.get(log.siteId)?.name ?? null) : null, siteNiceId: log.siteId - ? siteMap.get(log.siteId)?.niceId ?? null + ? (siteMap.get(log.siteId)?.niceId ?? null) : null, clientName: log.clientId - ? clientMap.get(log.clientId)?.name ?? null + ? (clientMap.get(log.clientId)?.name ?? null) : null, clientNiceId: log.clientId - ? clientMap.get(log.clientId)?.niceId ?? null + ? (clientMap.get(log.clientId)?.niceId ?? null) : null, clientType: log.clientId - ? clientMap.get(log.clientId)?.type ?? null + ? (clientMap.get(log.clientId)?.type ?? null) : null, - userEmail: log.userId - ? userMap.get(log.userId)?.email ?? null - : null + userEmail: log.userId ? (userMap.get(log.userId)?.email ?? null) : null })); } @@ -521,4 +508,4 @@ export async function queryConnectionAuditLogs( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts b/server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts index 10a6c3600..27b5d9a5b 100644 --- a/server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts +++ b/server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts @@ -51,6 +51,8 @@ export type ListEventStreamingDestinationsResponse = { type: string; config: string; enabled: boolean; + lastError: string | null; + lastErrorAt: number | null; createdAt: number; updatedAt: number; sendConnectionLogs: boolean; @@ -79,7 +81,8 @@ async function query(orgId: string, limit: number, offset: number) { registry.registerPath({ method: "get", path: "/org/{orgId}/event-streaming-destination", - description: "List all event streaming destinations for a specific organization.", + description: + "List all event streaming destinations for a specific organization.", tags: [OpenAPITags.Org], request: { query: querySchema, diff --git a/server/private/routers/newt/handleConnectionLogMessage.ts b/server/private/routers/newt/handleConnectionLogMessage.ts index 6355eb783..cf2ba2daa 100644 --- a/server/private/routers/newt/handleConnectionLogMessage.ts +++ b/server/private/routers/newt/handleConnectionLogMessage.ts @@ -11,7 +11,7 @@ * This file is not licensed under the AGPLv3. */ -import { db } from "@server/db"; +import { clientSitesAssociationsCache, db } from "@server/db"; import { MessageHandler } from "@server/routers/ws"; import { sites, Newt, clients, orgs } from "@server/db"; import { and, eq, inArray } from "drizzle-orm"; @@ -146,7 +146,11 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { // each unique sourceAddr + the org's CIDR suffix and do a targeted IN query. const ipToClient = new Map< string, - { clientId: number; userId: string | null } + { + clientId: number; + userId: string | null; + clientEndpoint: string | null; + } >(); if (cidrSuffix) { @@ -172,9 +176,21 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { .select({ clientId: clients.clientId, userId: clients.userId, - subnet: clients.subnet + subnet: clients.subnet, + clientEndpoint: clientSitesAssociationsCache.endpoint }) .from(clients) + .leftJoin( + // this should be one to one + clientSitesAssociationsCache, + and( + eq( + clients.clientId, + clientSitesAssociationsCache.clientId + ), + eq(clientSitesAssociationsCache.siteId, newt.siteId) + ) + ) .where( and( eq(clients.orgId, orgId), @@ -189,7 +205,8 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { ); ipToClient.set(ip, { clientId: c.clientId, - userId: c.userId + userId: c.userId, + clientEndpoint: c.clientEndpoint }); } } @@ -234,6 +251,7 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => { orgId, siteId: newt.siteId, clientId: clientInfo?.clientId ?? null, + clientEndpoint: clientInfo?.clientEndpoint ?? null, userId: clientInfo?.userId ?? null, sourceAddr: session.sourceAddr, destAddr: session.destAddr, diff --git a/server/routers/auditLogs/types.ts b/server/routers/auditLogs/types.ts index 972eebfe3..b8168ef1e 100644 --- a/server/routers/auditLogs/types.ts +++ b/server/routers/auditLogs/types.ts @@ -100,6 +100,7 @@ export type QueryConnectionAuditLogResponse = { orgId: string | null; siteId: number | null; clientId: number | null; + clientEndpoint: string | null; userId: string | null; sourceAddr: string; destAddr: string; diff --git a/src/app/[orgId]/settings/logs/connection/page.tsx b/src/app/[orgId]/settings/logs/connection/page.tsx index 0fc8f95b7..c2a630332 100644 --- a/src/app/[orgId]/settings/logs/connection/page.tsx +++ b/src/app/[orgId]/settings/logs/connection/page.tsx @@ -645,6 +645,12 @@ export default function ConnectionLogsPage() { )} */} +
+ Client Endpoint:{" "} + + {row.clientEndpoint ?? "-"} + +
Site: {row.siteName ?? "-"} {row.siteNiceId && ( diff --git a/src/app/[orgId]/settings/logs/streaming/page.tsx b/src/app/[orgId]/settings/logs/streaming/page.tsx index 022a8eb2e..8579527d0 100644 --- a/src/app/[orgId]/settings/logs/streaming/page.tsx +++ b/src/app/[orgId]/settings/logs/streaming/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { useParams } from "next/navigation"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; @@ -22,7 +22,18 @@ import { } from "@app/components/Credenza"; import { Button } from "@app/components/ui/button"; import { Switch } from "@app/components/ui/switch"; -import { Globe, MoreHorizontal, Plus } from "lucide-react"; +import { + Globe, + MoreHorizontal, + Plus, + AlertCircle, + ChevronDown +} from "lucide-react"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; import { AxiosResponse } from "axios"; import { build } from "@server/build"; import Image from "next/image"; @@ -38,7 +49,10 @@ import { HttpDestinationCredenza, parseHttpConfig } from "@app/components/HttpDestinationCredenza"; -import { S3DestinationCredenza } from "@app/components/S3DestinationCredenza"; +import { + S3DestinationCredenza, + parseS3Config +} from "@app/components/S3DestinationCredenza"; import { DatadogDestinationCredenza } from "@app/components/DatadogDestinationCredenza"; import { useTranslations } from "next-intl"; @@ -64,6 +78,42 @@ interface DestinationCardProps { disabled?: boolean; } +function getDestinationDisplay(destination: Destination): { + name: string; + typeLabel: string; + detail: string; + icon: React.ReactNode; +} { + if (destination.type === "s3") { + const cfg = parseS3Config(destination.config); + const detail = cfg.bucket + ? `s3://${cfg.bucket}${cfg.prefix ? `/${cfg.prefix.replace(/^\/+/, "")}` : ""}` + : ""; + return { + name: cfg.name, + typeLabel: "Amazon S3", + detail, + icon: ( + Amazon S3 + ) + }; + } + // Default: HTTP + const cfg = parseHttpConfig(destination.config); + return { + name: cfg.name, + typeLabel: "HTTP", + detail: cfg.url, + icon: + }; +} + function DestinationCard({ destination, onToggle, @@ -73,25 +123,25 @@ function DestinationCard({ disabled = false }: DestinationCardProps) { const t = useTranslations(); - const cfg = parseHttpConfig(destination.config); + const { name, typeLabel, detail, icon } = + getDestinationDisplay(destination); return (
{/* Top row: icon + name/type + toggle */}
- {/* Squirkle icon: gray outer → white inner → black globe */}
- + {icon}

- {cfg.name || t("streamingUnnamedDestination")} + {name || t("streamingUnnamedDestination")}

- HTTP + {typeLabel}

@@ -105,15 +155,40 @@ function DestinationCard({ />
- {/* URL preview */} + {/* Detail preview (URL for HTTP, s3:// path for S3) */}

- {cfg.url || ( + {detail || ( {t("streamingNoUrlConfigured")} )}

+ {/* Error indicator */} + {destination.lastError && ( + + + + + + {destination.lastError} + + + )} + {/* Footer: edit button + three-dots menu */}
@@ -375,15 +378,21 @@ export function HttpDestinationCredenza({ htmlFor="auth-bearer" className="cursor-pointer font-medium" > - {t("httpDestAuthBearerTitle")} + {t( + "httpDestAuthBearerTitle" + )}

- {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("httpDestFormatJsonArrayTitle")} + {t( + "httpDestFormatJsonArrayTitle" + )}

- {t("httpDestFormatJsonArrayDescription")} + {t( + "httpDestFormatJsonArrayDescription" + )}

@@ -616,7 +643,9 @@ export function HttpDestinationCredenza({ {t("httpDestFormatNdjsonTitle")}

- {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 7702e7932..e6c128805 100644 --- a/src/components/S3DestinationCredenza.tsx +++ b/src/components/S3DestinationCredenza.tsx @@ -1,6 +1,6 @@ "use client"; - +import { useState, useEffect } from "react"; import { Credenza, CredenzaBody, @@ -12,13 +12,64 @@ import { CredenzaTitle } from "@app/components/Credenza"; import { Button } from "@app/components/ui/button"; -import { ContactSalesBanner } from "@app/components/ContactSalesBanner"; +import { Input } from "@app/components/ui/input"; +import { Label } from "@app/components/ui/label"; +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"; import { useTranslations } from "next-intl"; +import { Destination } from "@app/components/HttpDestinationCredenza"; + +// ── Types ────────────────────────────────────────────────────────────────────── + +export type S3PayloadFormat = "json_array" | "ndjson" | "csv"; + +export interface S3Config { + name: string; + accessKeyId: string; + secretAccessKey: string; + region: string; + bucket: string; + prefix: string; + endpoint: string; + format: S3PayloadFormat; + gzip: boolean; +} + +// ── Helpers ──────────────────────────────────────────────────────────────────── + +export const defaultS3Config = (): S3Config => ({ + name: "", + accessKeyId: "", + secretAccessKey: "", + region: "us-east-1", + bucket: "", + prefix: "", + endpoint: "", + format: "json_array", + gzip: false +}); + +export function parseS3Config(raw: string): S3Config { + try { + return { ...defaultS3Config(), ...JSON.parse(raw) }; + } catch { + return defaultS3Config(); + } +} + +// ── Component ────────────────────────────────────────────────────────────────── export interface S3DestinationCredenzaProps { open: boolean; onOpenChange: (open: boolean) => void; - editing: any; + editing: Destination | null; orgId: string; onSaved: () => void; } @@ -28,18 +79,84 @@ export function S3DestinationCredenza({ onOpenChange, editing, orgId, - onSaved, + onSaved }: S3DestinationCredenzaProps) { + const api = createApiClient(useEnvContext()); const t = useTranslations(); + const [saving, setSaving] = useState(false); + const [cfg, setCfg] = useState(defaultS3Config()); + const [sendAccessLogs, setSendAccessLogs] = useState(false); + const [sendActionLogs, setSendActionLogs] = useState(false); + const [sendConnectionLogs, setSendConnectionLogs] = useState(false); + const [sendRequestLogs, setSendRequestLogs] = useState(false); + + useEffect(() => { + if (open) { + setCfg(editing ? parseS3Config(editing.config) : defaultS3Config()); + setSendAccessLogs(editing?.sendAccessLogs ?? false); + setSendActionLogs(editing?.sendActionLogs ?? false); + setSendConnectionLogs(editing?.sendConnectionLogs ?? false); + setSendRequestLogs(editing?.sendRequestLogs ?? false); + } + }, [open, editing]); + + const update = (patch: Partial) => + setCfg((prev) => ({ ...prev, ...patch })); + + const isValid = + cfg.name.trim() !== "" && + cfg.accessKeyId.trim() !== "" && + cfg.secretAccessKey.trim() !== "" && + cfg.region.trim() !== "" && + cfg.bucket.trim() !== ""; + + async function handleSave() { + if (!isValid) return; + setSaving(true); + try { + const payload = { + type: "s3", + config: JSON.stringify(cfg), + sendAccessLogs, + sendActionLogs, + sendConnectionLogs, + sendRequestLogs + }; + if (editing) { + await api.post( + `/org/${orgId}/event-streaming-destination/${editing.destinationId}`, + payload + ); + toast({ title: t("s3DestUpdatedSuccess") }); + } else { + await api.put( + `/org/${orgId}/event-streaming-destination`, + payload + ); + toast({ title: t("s3DestCreatedSuccess") }); + } + onSaved(); + onOpenChange(false); + } catch (e) { + toast({ + variant: "destructive", + title: editing + ? t("s3DestUpdateFailed") + : t("s3DestCreateFailed"), + description: formatAxiosError(e, t("streamingUnexpectedError")) + }); + } finally { + setSaving(false); + } + } + return ( - {editing - ? t("S3DestEditTitle") - : t("S3DestAddTitle")} + {editing ? t("S3DestEditTitle") : t("S3DestAddTitle")} {editing @@ -49,13 +166,375 @@ export function S3DestinationCredenza({ - + {editing?.lastError && ( + + + + {editing.lastError} + + + )} + + {/* ── Settings tab ────────────────────────────── */} +
+ {/* Name */} +
+ + + update({ name: e.target.value }) + } + /> +
+ + {/* AWS Access Key ID */} +
+ + + update({ + accessKeyId: e.target.value + }) + } + autoComplete="off" + /> +
+ + {/* AWS Secret Access Key */} +
+ + + update({ + secretAccessKey: e.target.value + }) + } + autoComplete="new-password" + /> +
+ + {/* Region */} +
+ + + update({ region: e.target.value }) + } + /> +
+ + {/* Bucket */} +
+ + + update({ bucket: e.target.value }) + } + /> +
+ + {/* Prefix */} +
+ + + update({ prefix: e.target.value }) + } + /> +

+ {t("s3DestPrefixDescription")} +

+
+ + {/* Custom endpoint (optional – for S3-compatible storage) */} +
+ + + update({ endpoint: e.target.value }) + } + /> +

+ {t("s3DestEndpointDescription")} +

+
+
+ + {/* ── Format tab ───────────────────────────────── */} +
+ {/* Gzip compression toggle */} +
+ update({ gzip: v })} + className="mt-0.5" + /> +
+ +

+ {t("s3DestGzipDescription")} +

+
+
+ + {/* Payload format selector */} +
+
+ +

+ {t("s3DestFormatDescription")} +

+
+ + + update({ + format: v as S3PayloadFormat + }) + } + className="gap-2" + > + {/* JSON Array */} + + + {/* NDJSON */} + + + {/* CSV */} + + +
+
+ + {/* ── Logs tab ──────────────────────────────────── */} +
+
+ +

+ {t("httpDestLogTypesDescription")} +

+
+ +
+
+ + setSendAccessLogs(v === true) + } + className="mt-0.5" + /> +
+ +

+ {t("httpDestAccessLogsDescription")} +

+
+
+ +
+ + setSendActionLogs(v === true) + } + className="mt-0.5" + /> +
+ +

+ {t("httpDestActionLogsDescription")} +

+
+
+ +
+ + setSendConnectionLogs(v === true) + } + className="mt-0.5" + /> +
+ +

+ {t( + "httpDestConnectionLogsDescription" + )} +

+
+
+ +
+ + setSendRequestLogs(v === true) + } + className="mt-0.5" + /> +
+ +

+ {t( + "httpDestRequestLogsDescription" + )} +

+
+
+
+
+
- + +