insforge 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +58 -0
- package/.env.example +49 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +79 -0
- package/.github/copilot-instructions.md +147 -0
- package/.github/workflows/build-image.yml +65 -0
- package/.github/workflows/ci-premerge-check.yml +24 -0
- package/.github/workflows/deploy-aws.yml +130 -0
- package/.github/workflows/lint-and-format.yml +33 -0
- package/.prettierignore +65 -0
- package/.prettierrc +9 -0
- package/CHANGELOG.md +3 -0
- package/CONTRIBUTING.md +126 -0
- package/Dockerfile +27 -0
- package/GITHUB_OAUTH_SETUP.md +49 -0
- package/GOOGLE_OAUTH_SETUP.md +148 -0
- package/LICENSE +201 -0
- package/README.md +134 -0
- package/assets/Dark.svg +23 -0
- package/assets/archDiagram.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/mcpInstallv2.png +0 -0
- package/assets/sampleResponse.png +0 -0
- package/assets/signin.png +0 -0
- package/assets/userflow.png +0 -0
- package/backend/migrations/000_create-base-tables.sql +142 -0
- package/backend/migrations/001_create-helper-functions.sql +41 -0
- package/backend/migrations/002_rename-auth-tables.sql +30 -0
- package/backend/migrations/003_create-users-table.sql +56 -0
- package/backend/migrations/004_add-reload-postgrest-func.sql +24 -0
- package/backend/migrations/005_enable-project-admin-modify-users.sql +30 -0
- package/backend/migrations/006_modify-ai-usage-table.sql +25 -0
- package/backend/migrations/007_drop-metadata-table.sql +2 -0
- package/backend/migrations/008_add-system-tables.sql +77 -0
- package/backend/migrations/009_add-function-secrets.sql +24 -0
- package/backend/migrations/010_modify-ai-config-modalities.sql +93 -0
- package/backend/migrations/011_refactor-secrets-table.sql +15 -0
- package/backend/migrations/012_add-storage-uploaded-by.sql +8 -0
- package/backend/package.json +75 -0
- package/backend/src/api/middleware/auth.ts +240 -0
- package/backend/src/api/middleware/error.ts +231 -0
- package/backend/src/api/middleware/upload.ts +59 -0
- package/backend/src/api/routes/agent.ts +29 -0
- package/backend/src/api/routes/ai.ts +472 -0
- package/backend/src/api/routes/auth.oauth.ts +482 -0
- package/backend/src/api/routes/auth.ts +386 -0
- package/backend/src/api/routes/database.advance.ts +275 -0
- package/backend/src/api/routes/database.records.ts +246 -0
- package/backend/src/api/routes/database.tables.ts +161 -0
- package/backend/src/api/routes/docs.ts +66 -0
- package/backend/src/api/routes/functions.ts +183 -0
- package/backend/src/api/routes/logs.ts +150 -0
- package/backend/src/api/routes/metadata.ts +160 -0
- package/backend/src/api/routes/openapi.ts +82 -0
- package/backend/src/api/routes/secrets.ts +199 -0
- package/backend/src/api/routes/storage.ts +547 -0
- package/backend/src/api/routes/usage.ts +96 -0
- package/backend/src/core/ai/chat.ts +207 -0
- package/backend/src/core/ai/client.ts +242 -0
- package/backend/src/core/ai/config.ts +187 -0
- package/backend/src/core/ai/image.ts +156 -0
- package/backend/src/core/ai/model.ts +117 -0
- package/backend/src/core/ai/usage.ts +290 -0
- package/backend/src/core/auth/auth.ts +781 -0
- package/backend/src/core/auth/oauth.ts +398 -0
- package/backend/src/core/database/advance.ts +1074 -0
- package/backend/src/core/database/manager.ts +178 -0
- package/backend/src/core/database/table.ts +772 -0
- package/backend/src/core/documentation/agent.ts +689 -0
- package/backend/src/core/documentation/openapi.ts +856 -0
- package/backend/src/core/functions/functions.ts +310 -0
- package/backend/src/core/logs/analytics.ts +76 -0
- package/backend/src/core/logs/audit.ts +255 -0
- package/backend/src/core/logs/providers/base.provider.ts +83 -0
- package/backend/src/core/logs/providers/cloudwatch.provider.ts +510 -0
- package/backend/src/core/logs/providers/localdb.provider.ts +246 -0
- package/backend/src/core/secrets/encryption.ts +58 -0
- package/backend/src/core/secrets/secrets.ts +410 -0
- package/backend/src/core/socket/socket.ts +388 -0
- package/backend/src/core/socket/types.ts +79 -0
- package/backend/src/core/storage/storage.ts +923 -0
- package/backend/src/server.ts +288 -0
- package/backend/src/types/ai.ts +46 -0
- package/backend/src/types/auth.ts +90 -0
- package/backend/src/types/database.ts +136 -0
- package/backend/src/types/error-constants.ts +86 -0
- package/backend/src/types/logs.ts +47 -0
- package/backend/src/types/profile.ts +55 -0
- package/backend/src/types/storage.ts +23 -0
- package/backend/src/utils/cloud-token.ts +39 -0
- package/backend/src/utils/constants.ts +1 -0
- package/backend/src/utils/environment.ts +35 -0
- package/backend/src/utils/helpers.ts +49 -0
- package/backend/src/utils/logger.ts +13 -0
- package/backend/src/utils/response.ts +62 -0
- package/backend/src/utils/seed.ts +205 -0
- package/backend/src/utils/sql-parser.ts +63 -0
- package/backend/src/utils/uuid.ts +9 -0
- package/backend/src/utils/validations.ts +129 -0
- package/backend/tests/README.md +134 -0
- package/backend/tests/cleanup-all-test-data.sh +231 -0
- package/backend/tests/cloud/test-s3-multitenant.sh +132 -0
- package/backend/tests/local/comprehensive-curl-tests.sh +156 -0
- package/backend/tests/local/test-auth-router.sh +144 -0
- package/backend/tests/local/test-database-router.sh +222 -0
- package/backend/tests/local/test-e2e.sh +241 -0
- package/backend/tests/local/test-fk-errors.sh +97 -0
- package/backend/tests/local/test-id-field.sh +201 -0
- package/backend/tests/local/test-public-bucket.sh +265 -0
- package/backend/tests/local/test-secrets.sh +248 -0
- package/backend/tests/local/test-serverless-functions.sh.disabled +325 -0
- package/backend/tests/local/test-traditional-rest.sh +209 -0
- package/backend/tests/manual/README.md +51 -0
- package/backend/tests/manual/create-large-table-simple.sql +11 -0
- package/backend/tests/manual/seed-large-table.sql +101 -0
- package/backend/tests/manual/setup-large-table-extras.sql +34 -0
- package/backend/tests/manual/test-better-auth.sh +303 -0
- package/backend/tests/manual/test-bulk-upsert.sh +410 -0
- package/backend/tests/manual/test-database-advance.sh +297 -0
- package/backend/tests/manual/test-postgrest-stability.sh +192 -0
- package/backend/tests/manual/test-rawsql-export-import.sh +412 -0
- package/backend/tests/manual/test-universal-storage.sh +264 -0
- package/backend/tests/manual/test-users.sql +18 -0
- package/backend/tests/run-all-tests.sh +140 -0
- package/backend/tests/setup.ts +22 -0
- package/backend/tests/test-config.sh +303 -0
- package/backend/tsconfig.json +23 -0
- package/backend/tsup.config.ts +18 -0
- package/backend/vitest.config.ts +22 -0
- package/docker-compose.prod.yml +145 -0
- package/docker-compose.yml +167 -0
- package/docker-init/db/db-init.sql +125 -0
- package/docker-init/db/jwt.sql +5 -0
- package/docker-init/db/logs.sql +9 -0
- package/docker-init/db/postgresql.conf +17 -0
- package/docs/deprecated/insforge-auth-api.md +215 -0
- package/docs/deprecated/insforge-auth-sdk.md +100 -0
- package/docs/deprecated/insforge-db-api.md +359 -0
- package/docs/deprecated/insforge-db-sdk.md +140 -0
- package/docs/deprecated/insforge-debug-sdk.md +157 -0
- package/docs/deprecated/insforge-debug.md +65 -0
- package/docs/deprecated/insforge-instructions.md +124 -0
- package/docs/deprecated/insforge-project.md +118 -0
- package/docs/deprecated/insforge-storage-api.md +279 -0
- package/docs/deprecated/insforge-storage-sdk.md +159 -0
- package/docs/insforge-instructions-sdk.md +407 -0
- package/eslint.config.js +317 -0
- package/examples/oauth/frontend-oauth-example.html +251 -0
- package/examples/response-examples.md +444 -0
- package/frontend/README.md +112 -0
- package/frontend/components.json +17 -0
- package/frontend/index.html +13 -0
- package/frontend/package.json +63 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/App.tsx +106 -0
- package/frontend/src/assets/icons/checkbox_checked.svg +6 -0
- package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -0
- package/frontend/src/assets/icons/checked.svg +3 -0
- package/frontend/src/assets/icons/error.svg +3 -0
- package/frontend/src/assets/icons/pencil.svg +4 -0
- package/frontend/src/assets/icons/refresh.svg +4 -0
- package/frontend/src/assets/icons/step_active.svg +3 -0
- package/frontend/src/assets/icons/step_inactive.svg +11 -0
- package/frontend/src/assets/icons/warning.svg +3 -0
- package/frontend/src/assets/logos/amazon.svg +1 -0
- package/frontend/src/assets/logos/claude_code.svg +3 -0
- package/frontend/src/assets/logos/cline.svg +6 -0
- package/frontend/src/assets/logos/cursor.svg +20 -0
- package/frontend/src/assets/logos/discord.svg +9 -0
- package/frontend/src/assets/logos/gemini.svg +19 -0
- package/frontend/src/assets/logos/github.svg +5 -0
- package/frontend/src/assets/logos/google.svg +13 -0
- package/frontend/src/assets/logos/grok.svg +10 -0
- package/frontend/src/assets/logos/insforge_dark.svg +15 -0
- package/frontend/src/assets/logos/insforge_light.svg +15 -0
- package/frontend/src/assets/logos/openai.svg +10 -0
- package/frontend/src/assets/logos/roo_code.svg +9 -0
- package/frontend/src/assets/logos/trae.svg +3 -0
- package/frontend/src/assets/logos/windsurf.svg +10 -0
- package/frontend/src/components/ButtonWithLoading.tsx +27 -0
- package/frontend/src/components/Checkbox.tsx +61 -0
- package/frontend/src/components/CodeBlock.tsx +32 -0
- package/frontend/src/components/ConfirmDialog.tsx +96 -0
- package/frontend/src/components/CopyButton.tsx +69 -0
- package/frontend/src/components/DeleteActionButton.tsx +42 -0
- package/frontend/src/components/EmptyState.tsx +41 -0
- package/frontend/src/components/ErrorState.tsx +35 -0
- package/frontend/src/components/FeatureSidebar.tsx +126 -0
- package/frontend/src/components/FeatureSidebarItem.tsx +101 -0
- package/frontend/src/components/JsonHighlight.tsx +61 -0
- package/frontend/src/components/LoadingState.tsx +16 -0
- package/frontend/src/components/PaginationControls.tsx +54 -0
- package/frontend/src/components/PromptDialog.tsx +68 -0
- package/frontend/src/components/SearchInput.tsx +90 -0
- package/frontend/src/components/SelectionClearButton.tsx +26 -0
- package/frontend/src/components/Stepper.tsx +139 -0
- package/frontend/src/components/ThemeToggle.tsx +58 -0
- package/frontend/src/components/TypeBadge.tsx +20 -0
- package/frontend/src/components/datagrid/DataGrid.tsx +264 -0
- package/frontend/src/components/datagrid/DefaultCellRenderer.tsx +114 -0
- package/frontend/src/components/datagrid/IdCell.tsx +44 -0
- package/frontend/src/components/datagrid/SortableHeader.tsx +74 -0
- package/frontend/src/components/datagrid/cell-editors/BooleanCellEditor.tsx +54 -0
- package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +483 -0
- package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +362 -0
- package/frontend/src/components/datagrid/cell-editors/TextCellEditor.tsx +38 -0
- package/frontend/src/components/datagrid/cell-editors/index.ts +14 -0
- package/frontend/src/components/datagrid/cell-editors/types.ts +43 -0
- package/frontend/src/components/datagrid/datagridTypes.tsx +72 -0
- package/frontend/src/components/datagrid/index.tsx +20 -0
- package/frontend/src/components/index.ts +39 -0
- package/frontend/src/components/layout/AppHeader.tsx +146 -0
- package/frontend/src/components/layout/AppSidebar.tsx +190 -0
- package/frontend/src/components/layout/CloudLayout.tsx +95 -0
- package/frontend/src/components/layout/Layout.tsx +43 -0
- package/frontend/src/components/radix/Alert.tsx +45 -0
- package/frontend/src/components/radix/AlertDialog.tsx +115 -0
- package/frontend/src/components/radix/Avatar.tsx +45 -0
- package/frontend/src/components/radix/Badge.tsx +33 -0
- package/frontend/src/components/radix/Button.tsx +50 -0
- package/frontend/src/components/radix/Card.tsx +58 -0
- package/frontend/src/components/radix/Dialog.tsx +98 -0
- package/frontend/src/components/radix/DropdownMenu.tsx +185 -0
- package/frontend/src/components/radix/Form.tsx +167 -0
- package/frontend/src/components/radix/Input.tsx +22 -0
- package/frontend/src/components/radix/Label.tsx +19 -0
- package/frontend/src/components/radix/Popover.tsx +29 -0
- package/frontend/src/components/radix/ScrollArea.tsx +44 -0
- package/frontend/src/components/radix/Select.tsx +151 -0
- package/frontend/src/components/radix/Separator.tsx +26 -0
- package/frontend/src/components/radix/Sheet.tsx +119 -0
- package/frontend/src/components/radix/Skeleton.tsx +7 -0
- package/frontend/src/components/radix/Switch.tsx +29 -0
- package/frontend/src/components/radix/Tabs.tsx +50 -0
- package/frontend/src/components/radix/Textarea.tsx +21 -0
- package/frontend/src/components/radix/Tooltip.tsx +28 -0
- package/frontend/src/features/ai/components/AIConfigCard.tsx +154 -0
- package/frontend/src/features/ai/components/AIConfigDialog.tsx +76 -0
- package/frontend/src/features/ai/components/AIConfigForm.tsx +222 -0
- package/frontend/src/features/ai/components/AIEmptyState.tsx +18 -0
- package/frontend/src/features/ai/components/fields/ModalityField.tsx +87 -0
- package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +134 -0
- package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +33 -0
- package/frontend/src/features/ai/helpers.ts +155 -0
- package/frontend/src/features/ai/hooks/useAIConfigs.ts +221 -0
- package/frontend/src/features/ai/hooks/useAIUsage.ts +77 -0
- package/frontend/src/features/ai/page/AIPage.tsx +178 -0
- package/frontend/src/features/ai/services/ai.service.ts +148 -0
- package/frontend/src/features/auth/components/AddOAuthDialog.tsx +106 -0
- package/frontend/src/features/auth/components/AuthMethodTab.tsx +238 -0
- package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +303 -0
- package/frontend/src/features/auth/components/OAuthEmptyState.tsx +15 -0
- package/frontend/src/features/auth/components/UserFormDialog.tsx +248 -0
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +183 -0
- package/frontend/src/features/auth/components/UsersTab.tsx +114 -0
- package/frontend/src/features/auth/hooks/useOAuthConfig.ts +129 -0
- package/frontend/src/features/auth/hooks/useUsers.ts +57 -0
- package/frontend/src/features/auth/index.ts +9 -0
- package/frontend/src/features/auth/page/AuthenticationPage.tsx +169 -0
- package/frontend/src/features/auth/services/auth.service.ts +112 -0
- package/frontend/src/features/auth/services/oauth.service.ts +49 -0
- package/frontend/src/features/dashboard/page/DashboardPage.tsx +194 -0
- package/frontend/src/features/database/components/ColumnTypeSelect.tsx +64 -0
- package/frontend/src/features/database/components/DatabaseDataGrid.tsx +282 -0
- package/frontend/src/features/database/components/ForeignKeyCell.tsx +187 -0
- package/frontend/src/features/database/components/ForeignKeyPopover.tsx +378 -0
- package/frontend/src/features/database/components/LinkRecordModal.tsx +288 -0
- package/frontend/src/features/database/components/RecordFormDialog.tsx +164 -0
- package/frontend/src/features/database/components/RecordFormField.tsx +568 -0
- package/frontend/src/features/database/components/TableEmptyState.tsx +21 -0
- package/frontend/src/features/database/components/TableForm.tsx +656 -0
- package/frontend/src/features/database/components/TableFormColumn.tsx +137 -0
- package/frontend/src/features/database/components/TableListSkeleton.tsx +9 -0
- package/frontend/src/features/database/components/TableSidebar.tsx +47 -0
- package/frontend/src/features/database/constants.ts +26 -0
- package/frontend/src/features/database/helpers.ts +125 -0
- package/frontend/src/features/database/hooks/UseLinkModal.tsx +78 -0
- package/frontend/src/features/database/index.ts +12 -0
- package/frontend/src/features/database/page/DatabasePage.tsx +626 -0
- package/frontend/src/features/database/schema.ts +25 -0
- package/frontend/src/features/database/services/database.service.ts +216 -0
- package/frontend/src/features/functions/components/FunctionEmptyState.tsx +15 -0
- package/frontend/src/features/functions/components/FunctionRow.tsx +71 -0
- package/frontend/src/features/functions/components/FunctionViewer.tsx +46 -0
- package/frontend/src/features/functions/components/FunctionsContent.tsx +88 -0
- package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -0
- package/frontend/src/features/functions/components/SecretEmptyState.tsx +23 -0
- package/frontend/src/features/functions/components/SecretRow.tsx +68 -0
- package/frontend/src/features/functions/components/SecretsContent.tsx +120 -0
- package/frontend/src/features/functions/hooks/useFunctions.ts +106 -0
- package/frontend/src/features/functions/page/FunctionsPage.tsx +28 -0
- package/frontend/src/features/functions/services/functions.service.ts +48 -0
- package/frontend/src/features/login/components/AuthErrorBoundary.tsx +87 -0
- package/frontend/src/features/login/components/PrivateRoute.tsx +24 -0
- package/frontend/src/features/login/page/CloudLoginPage.tsx +93 -0
- package/frontend/src/features/login/page/LoginPage.tsx +174 -0
- package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +313 -0
- package/frontend/src/features/logs/components/LogsTable.tsx +199 -0
- package/frontend/src/features/logs/hooks/useAuditLogs.ts +39 -0
- package/frontend/src/features/logs/index.ts +5 -0
- package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +530 -0
- package/frontend/src/features/logs/page/AuditsPage.tsx +192 -0
- package/frontend/src/features/logs/services/log.service.ts +171 -0
- package/frontend/src/features/metadata/hooks/useMetadata.ts +53 -0
- package/frontend/src/features/metadata/index.ts +0 -0
- package/frontend/src/features/metadata/page/MetadataPage.tsx +136 -0
- package/frontend/src/features/metadata/services/metadata.service.ts +17 -0
- package/frontend/src/features/onboard/components/CompletionCard.tsx +41 -0
- package/frontend/src/features/onboard/components/OnboardButton.tsx +84 -0
- package/frontend/src/features/onboard/components/StepContent.tsx +91 -0
- package/frontend/src/features/onboard/components/TestConnectionStep.tsx +53 -0
- package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +35 -0
- package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +144 -0
- package/frontend/src/features/onboard/components/mcp/index.ts +4 -0
- package/frontend/src/features/onboard/components/mcp/mcp-helper.tsx +98 -0
- package/frontend/src/features/onboard/index.ts +3 -0
- package/frontend/src/features/onboard/page/OnBoardPage.tsx +104 -0
- package/frontend/src/features/onboard/types.ts +8 -0
- package/frontend/src/features/secrets/hooks/useSecrets.ts +139 -0
- package/frontend/src/features/secrets/services/secrets.service.ts +57 -0
- package/frontend/src/features/storage/components/BucketEmptyState.tsx +19 -0
- package/frontend/src/features/storage/components/BucketFormDialog.tsx +194 -0
- package/frontend/src/features/storage/components/BucketListSkeleton.tsx +17 -0
- package/frontend/src/features/storage/components/FilePreviewDialog.tsx +287 -0
- package/frontend/src/features/storage/components/StorageDataGrid.tsx +239 -0
- package/frontend/src/features/storage/components/StorageManager.tsx +236 -0
- package/frontend/src/features/storage/components/StorageSidebar.tsx +44 -0
- package/frontend/src/features/storage/components/UploadToast.tsx +46 -0
- package/frontend/src/features/storage/index.ts +3 -0
- package/frontend/src/features/storage/page/StoragePage.tsx +553 -0
- package/frontend/src/features/storage/services/storage.service.ts +144 -0
- package/frontend/src/features/visualizer/components/AuthNode.tsx +107 -0
- package/frontend/src/features/visualizer/components/BucketNode.tsx +34 -0
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +359 -0
- package/frontend/src/features/visualizer/components/TableNode.tsx +152 -0
- package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +24 -0
- package/frontend/src/features/visualizer/components/index.ts +5 -0
- package/frontend/src/features/visualizer/page/VisualizerPage.tsx +127 -0
- package/frontend/src/index.css +248 -0
- package/frontend/src/lib/api/client.ts +163 -0
- package/frontend/src/lib/contexts/AuthContext.tsx +157 -0
- package/frontend/src/lib/contexts/OnboardStepContext.tsx +68 -0
- package/frontend/src/lib/contexts/SocketContext.tsx +303 -0
- package/frontend/src/lib/contexts/ThemeContext.tsx +125 -0
- package/frontend/src/lib/hooks/useAuth.ts +4 -0
- package/frontend/src/lib/hooks/useConfirm.ts +55 -0
- package/frontend/src/lib/hooks/useInterval.ts +27 -0
- package/frontend/src/lib/hooks/useMediaQuery.ts +59 -0
- package/frontend/src/lib/hooks/useOnboardingCompletion.ts +29 -0
- package/frontend/src/lib/hooks/usePagination.ts +27 -0
- package/frontend/src/lib/hooks/useTimeout.ts +27 -0
- package/frontend/src/lib/hooks/useToast.tsx +229 -0
- package/frontend/src/lib/utils/constants.ts +38 -0
- package/frontend/src/lib/utils/utils.ts +165 -0
- package/frontend/src/lib/utils/validation-schemas.ts +126 -0
- package/frontend/src/main.tsx +16 -0
- package/frontend/src/rdg.css +194 -0
- package/frontend/src/vite-env.d.ts +12 -0
- package/frontend/tailwind.config.js +97 -0
- package/frontend/tsconfig.json +26 -0
- package/frontend/tsconfig.node.json +10 -0
- package/frontend/vite.config.ts +37 -0
- package/frontend/vitest.config.ts +36 -0
- package/functions/deno.json +25 -0
- package/functions/server.ts +290 -0
- package/functions/worker-template.js +126 -0
- package/openapi/ai.yaml +689 -0
- package/openapi/auth.yaml +563 -0
- package/openapi/functions.yaml +476 -0
- package/openapi/health.yaml +30 -0
- package/openapi/logs.yaml +224 -0
- package/openapi/metadata.yaml +178 -0
- package/openapi/records.yaml +382 -0
- package/openapi/secrets.yaml +371 -0
- package/openapi/storage.yaml +876 -0
- package/openapi/tables.yaml +464 -0
- package/package.json +88 -0
- package/shared-schemas/package.json +31 -0
- package/shared-schemas/src/ai-api.schema.ts +167 -0
- package/shared-schemas/src/ai.schema.ts +54 -0
- package/shared-schemas/src/auth-api.schema.ts +193 -0
- package/shared-schemas/src/auth.schema.ts +94 -0
- package/shared-schemas/src/database-api.schema.ts +259 -0
- package/shared-schemas/src/database.schema.ts +69 -0
- package/shared-schemas/src/functions-api.schema.ts +25 -0
- package/shared-schemas/src/functions.schema.ts +16 -0
- package/shared-schemas/src/index.ts +13 -0
- package/shared-schemas/src/logs-api.schema.ts +49 -0
- package/shared-schemas/src/logs.schema.ts +14 -0
- package/shared-schemas/src/metadata.schema.ts +56 -0
- package/shared-schemas/src/storage-api.schema.ts +65 -0
- package/shared-schemas/src/storage.schema.ts +19 -0
- package/shared-schemas/tsconfig.json +21 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { LogSource, AnalyticsLogRecord, LogSourceStats } from '@/types/logs.js';
|
|
2
|
+
|
|
3
|
+
export interface AnalyticsProvider {
|
|
4
|
+
initialize(): Promise<void>;
|
|
5
|
+
|
|
6
|
+
getLogSources(): Promise<LogSource[]>;
|
|
7
|
+
|
|
8
|
+
getLogsBySource(
|
|
9
|
+
sourceName: string,
|
|
10
|
+
limit?: number,
|
|
11
|
+
beforeTimestamp?: string
|
|
12
|
+
): Promise<{
|
|
13
|
+
logs: AnalyticsLogRecord[];
|
|
14
|
+
total: number;
|
|
15
|
+
tableName: string;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
getLogSourceStats(): Promise<LogSourceStats[]>;
|
|
19
|
+
|
|
20
|
+
searchLogs(
|
|
21
|
+
query: string,
|
|
22
|
+
sourceName?: string,
|
|
23
|
+
limit?: number,
|
|
24
|
+
offset?: number
|
|
25
|
+
): Promise<{
|
|
26
|
+
logs: (AnalyticsLogRecord & { source: string })[];
|
|
27
|
+
total: number;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Base class with common functionality
|
|
34
|
+
export abstract class BaseAnalyticsProvider implements AnalyticsProvider {
|
|
35
|
+
// Source name mapping for user-friendly display
|
|
36
|
+
protected sourceNameMap: Record<string, string> = {
|
|
37
|
+
'cloudflare.logs.prod': 'insforge.logs',
|
|
38
|
+
'deno-relay-logs': 'function.logs',
|
|
39
|
+
'postgREST.logs.prod': 'postgREST.logs',
|
|
40
|
+
'postgres.logs': 'postgres.logs',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Reverse mapping for API calls
|
|
44
|
+
protected reverseSourceNameMap: Record<string, string> = {
|
|
45
|
+
'insforge.logs': 'cloudflare.logs.prod',
|
|
46
|
+
'function.logs': 'deno-relay-logs',
|
|
47
|
+
'postgREST.logs': 'postgREST.logs.prod',
|
|
48
|
+
'postgres.logs': 'postgres.logs',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Convert internal source name to display name
|
|
52
|
+
protected getDisplayName(sourceName: string): string {
|
|
53
|
+
return this.sourceNameMap[sourceName] || sourceName;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Convert display name back to internal source name
|
|
57
|
+
protected getInternalName(displayName: string): string {
|
|
58
|
+
return this.reverseSourceNameMap[displayName] || displayName;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
abstract initialize(): Promise<void>;
|
|
62
|
+
abstract getLogSources(): Promise<LogSource[]>;
|
|
63
|
+
abstract getLogsBySource(
|
|
64
|
+
sourceName: string,
|
|
65
|
+
limit?: number,
|
|
66
|
+
beforeTimestamp?: string
|
|
67
|
+
): Promise<{
|
|
68
|
+
logs: AnalyticsLogRecord[];
|
|
69
|
+
total: number;
|
|
70
|
+
tableName: string;
|
|
71
|
+
}>;
|
|
72
|
+
abstract getLogSourceStats(): Promise<LogSourceStats[]>;
|
|
73
|
+
abstract searchLogs(
|
|
74
|
+
query: string,
|
|
75
|
+
sourceName?: string,
|
|
76
|
+
limit?: number,
|
|
77
|
+
offset?: number
|
|
78
|
+
): Promise<{
|
|
79
|
+
logs: (AnalyticsLogRecord & { source: string })[];
|
|
80
|
+
total: number;
|
|
81
|
+
}>;
|
|
82
|
+
abstract close(): Promise<void>;
|
|
83
|
+
}
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CloudWatchLogsClient,
|
|
3
|
+
DescribeLogStreamsCommand,
|
|
4
|
+
FilterLogEventsCommand,
|
|
5
|
+
StartQueryCommand,
|
|
6
|
+
GetQueryResultsCommand,
|
|
7
|
+
} from '@aws-sdk/client-cloudwatch-logs';
|
|
8
|
+
import { LogSource, AnalyticsLogRecord, LogSourceStats } from '@/types/logs.js';
|
|
9
|
+
import logger from '@/utils/logger.js';
|
|
10
|
+
import { BaseAnalyticsProvider } from './base.provider.js';
|
|
11
|
+
|
|
12
|
+
export class CloudWatchProvider extends BaseAnalyticsProvider {
|
|
13
|
+
private cwClient: CloudWatchLogsClient | null = null;
|
|
14
|
+
private cwLogGroup: string | null = null;
|
|
15
|
+
private cwRegion: string | null = null;
|
|
16
|
+
|
|
17
|
+
initialize(): Promise<void> {
|
|
18
|
+
this.cwRegion = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-2';
|
|
19
|
+
this.cwLogGroup = process.env.CLOUDWATCH_LOG_GROUP || null;
|
|
20
|
+
|
|
21
|
+
if (!this.cwLogGroup) {
|
|
22
|
+
throw new Error('CLOUDWATCH_LOG_GROUP is required when using CloudWatch analytics');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const cloudwatchOpts: {
|
|
26
|
+
region: string;
|
|
27
|
+
credentials?: { accessKeyId: string; secretAccessKey: string };
|
|
28
|
+
} = { region: this.cwRegion };
|
|
29
|
+
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
|
30
|
+
cloudwatchOpts.credentials = {
|
|
31
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
32
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.cwClient = new CloudWatchLogsClient({ ...cloudwatchOpts });
|
|
37
|
+
logger.info(`Using analytics provider: ${this.cwLogGroup ? 'CloudWatch' : 'LocalDB/Postgres'}`);
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private getSuffixMapping(): Record<string, string> {
|
|
42
|
+
return {
|
|
43
|
+
'insforge.logs': process.env.CW_SUFFIX_INFORGE || 'insforge-vector',
|
|
44
|
+
'postgREST.logs': process.env.CW_SUFFIX_POSTGREST || 'postgrest-vector',
|
|
45
|
+
'postgres.logs': process.env.CW_SUFFIX_POSTGRES || 'postgres-vector',
|
|
46
|
+
'function.logs': process.env.CW_SUFFIX_FUNCTION || 'function-vector',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getLogSources(): Promise<LogSource[]> {
|
|
51
|
+
if (!this.cwLogGroup || !this.cwClient) {
|
|
52
|
+
throw new Error('CloudWatch not initialized');
|
|
53
|
+
}
|
|
54
|
+
const logGroup = this.cwLogGroup;
|
|
55
|
+
const client = this.cwClient;
|
|
56
|
+
const suffixMapping = this.getSuffixMapping();
|
|
57
|
+
|
|
58
|
+
const cmd = new DescribeLogStreamsCommand({ logGroupName: logGroup });
|
|
59
|
+
const result = await client.send(cmd);
|
|
60
|
+
const streams = result.logStreams || [];
|
|
61
|
+
|
|
62
|
+
const available: LogSource[] = [];
|
|
63
|
+
let idCounter = 1;
|
|
64
|
+
|
|
65
|
+
for (const [displayName, suffix] of Object.entries(suffixMapping)) {
|
|
66
|
+
const have = streams.some((s) => (s.logStreamName || '').includes(suffix));
|
|
67
|
+
if (have) {
|
|
68
|
+
available.push({ id: idCounter++, name: displayName, token: suffix });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return available;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async getLogsBySource(
|
|
76
|
+
sourceName: string,
|
|
77
|
+
limit: number = 100,
|
|
78
|
+
beforeTimestamp?: string
|
|
79
|
+
): Promise<{
|
|
80
|
+
logs: AnalyticsLogRecord[];
|
|
81
|
+
total: number;
|
|
82
|
+
tableName: string;
|
|
83
|
+
}> {
|
|
84
|
+
if (!this.cwLogGroup || !this.cwClient) {
|
|
85
|
+
throw new Error('CloudWatch not initialized');
|
|
86
|
+
}
|
|
87
|
+
const client = this.cwClient;
|
|
88
|
+
const logGroup = this.cwLogGroup;
|
|
89
|
+
const suffixMapping = this.getSuffixMapping();
|
|
90
|
+
|
|
91
|
+
const suffix =
|
|
92
|
+
suffixMapping[sourceName] || suffixMapping[this.getDisplayName(sourceName)] || '';
|
|
93
|
+
|
|
94
|
+
const dls = await client.send(new DescribeLogStreamsCommand({ logGroupName: logGroup }));
|
|
95
|
+
const streams = (dls.logStreams || [])
|
|
96
|
+
.map((s) => s.logStreamName || '')
|
|
97
|
+
.filter((name) => (suffix ? name.includes(suffix) : true));
|
|
98
|
+
|
|
99
|
+
// Use beforeTimestamp as the end time, default to now
|
|
100
|
+
const endMs = beforeTimestamp ? Date.parse(beforeTimestamp) : Date.now();
|
|
101
|
+
// Look back 24 hours from the endMs for the time window
|
|
102
|
+
const startMs = endMs - 24 * 60 * 60 * 1000;
|
|
103
|
+
|
|
104
|
+
// For getLogsBySource, we need to handle two cases:
|
|
105
|
+
// 1. No beforeTimestamp: get the most recent logs (need to fetch all to find the newest)
|
|
106
|
+
// 2. With beforeTimestamp: get logs before that timestamp
|
|
107
|
+
|
|
108
|
+
let events: Array<{
|
|
109
|
+
eventId?: string;
|
|
110
|
+
timestamp?: number;
|
|
111
|
+
message?: string;
|
|
112
|
+
logStreamName?: string;
|
|
113
|
+
}>;
|
|
114
|
+
|
|
115
|
+
if (!beforeTimestamp) {
|
|
116
|
+
// Case 1: Get the most recent logs - use reliable approach instead of Insights
|
|
117
|
+
// CloudWatch Insights can be inconsistent, so use FilterLogEvents with pagination
|
|
118
|
+
const allEvents: Array<{
|
|
119
|
+
eventId?: string;
|
|
120
|
+
timestamp?: number;
|
|
121
|
+
message?: string;
|
|
122
|
+
logStreamName?: string;
|
|
123
|
+
}> = [];
|
|
124
|
+
let nextToken: string | undefined;
|
|
125
|
+
|
|
126
|
+
do {
|
|
127
|
+
const fle = await client.send(
|
|
128
|
+
new FilterLogEventsCommand({
|
|
129
|
+
logGroupName: logGroup,
|
|
130
|
+
logStreamNames: streams.length > 0 ? streams.slice(0, 100) : undefined,
|
|
131
|
+
startTime: startMs,
|
|
132
|
+
endTime: endMs,
|
|
133
|
+
nextToken,
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const pageEvents = fle.events || [];
|
|
138
|
+
allEvents.push(...pageEvents);
|
|
139
|
+
nextToken = fle.nextToken;
|
|
140
|
+
|
|
141
|
+
// Safety break to avoid infinite loops
|
|
142
|
+
if (allEvents.length > 50000) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
} while (nextToken);
|
|
146
|
+
|
|
147
|
+
// Get the most recent 'limit' events (events are in chronological order)
|
|
148
|
+
events = allEvents.slice(-limit);
|
|
149
|
+
} else {
|
|
150
|
+
// Case 2: Get logs before the specified timestamp (pagination)
|
|
151
|
+
// Use CloudWatch Insights for efficient timestamp-based pagination
|
|
152
|
+
try {
|
|
153
|
+
const beforeMs = Date.parse(beforeTimestamp);
|
|
154
|
+
// Use a reasonable time window - look back up to 7 days
|
|
155
|
+
const maxLookbackMs = 7 * 24 * 60 * 60 * 1000;
|
|
156
|
+
const startMs = beforeMs - maxLookbackMs;
|
|
157
|
+
|
|
158
|
+
const insights = `fields @timestamp, @message, @logStream, @eventId
|
|
159
|
+
| filter @logStream like /${suffix}/
|
|
160
|
+
| filter @timestamp < ${beforeMs}
|
|
161
|
+
| sort @timestamp desc
|
|
162
|
+
| limit ${limit}`;
|
|
163
|
+
|
|
164
|
+
const startQuery = await client.send(
|
|
165
|
+
new StartQueryCommand({
|
|
166
|
+
logGroupName: logGroup,
|
|
167
|
+
startTime: Math.floor(startMs / 1000),
|
|
168
|
+
endTime: Math.floor(beforeMs / 1000),
|
|
169
|
+
queryString: insights,
|
|
170
|
+
limit,
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const qid = startQuery.queryId || '';
|
|
175
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
176
|
+
let results;
|
|
177
|
+
|
|
178
|
+
// Wait for query to complete
|
|
179
|
+
for (let i = 0; i < 15; i++) {
|
|
180
|
+
const r = await client.send(new GetQueryResultsCommand({ queryId: qid }));
|
|
181
|
+
if (r.status === 'Complete' || r.status === 'Failed' || r.status === 'Cancelled') {
|
|
182
|
+
results = r.results || [];
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
await sleep(300);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (results && results.length > 0) {
|
|
189
|
+
// Convert Insights results to our format
|
|
190
|
+
events = results.map((row) => {
|
|
191
|
+
const obj = Object.fromEntries(row.map((c) => [c.field || '', c.value || '']));
|
|
192
|
+
|
|
193
|
+
// Parse timestamp properly
|
|
194
|
+
let timestamp: number;
|
|
195
|
+
const timestampValue = obj['@timestamp'];
|
|
196
|
+
timestamp = parseInt(timestampValue);
|
|
197
|
+
if (isNaN(timestamp) || timestamp < 1000000000000) {
|
|
198
|
+
timestamp = Date.parse(timestampValue);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
eventId: obj['@eventId'] || `insights-${Date.now()}-${Math.random()}`,
|
|
203
|
+
timestamp: timestamp,
|
|
204
|
+
message: obj['@message'] || '',
|
|
205
|
+
logStreamName: obj['@logStream'] || '',
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Ensure proper sorting by timestamp (CloudWatch Insights sorting may not be reliable)
|
|
210
|
+
// Sort in ascending order (oldest first) to match CloudWatch's standard behavior
|
|
211
|
+
events.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
212
|
+
} else {
|
|
213
|
+
events = [];
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
// Fallback to FilterLogEvents with optimized approach
|
|
217
|
+
logger.warn('CloudWatch Insights failed for pagination, using FilterLogEvents fallback', {
|
|
218
|
+
error: error instanceof Error ? error.message : String(error),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const beforeMs = Date.parse(beforeTimestamp);
|
|
222
|
+
// Use a smaller time window for fallback - 6 hours instead of 24
|
|
223
|
+
const fallbackStartMs = beforeMs - 6 * 60 * 60 * 1000;
|
|
224
|
+
|
|
225
|
+
const fle = await client.send(
|
|
226
|
+
new FilterLogEventsCommand({
|
|
227
|
+
logGroupName: logGroup,
|
|
228
|
+
logStreamNames: streams.length > 0 ? streams.slice(0, 100) : undefined,
|
|
229
|
+
startTime: fallbackStartMs,
|
|
230
|
+
endTime: beforeMs,
|
|
231
|
+
limit: limit * 2, // Get a bit more to ensure we have enough
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const allEvents = fle.events || [];
|
|
236
|
+
// Get the most recent 'limit' events
|
|
237
|
+
events = allEvents.slice(-limit);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Keep CloudWatch's default order (oldest first, newest last)
|
|
241
|
+
const logs: AnalyticsLogRecord[] = events.map((e) => {
|
|
242
|
+
const message = e.message || '';
|
|
243
|
+
let parsed: Record<string, unknown> = {};
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(message);
|
|
246
|
+
} catch {
|
|
247
|
+
parsed = { message };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
id: e.eventId || `${e.logStreamName || ''}-${e.timestamp || ''}`,
|
|
252
|
+
// CloudWatch timestamp is in milliseconds
|
|
253
|
+
timestamp: e.timestamp ? new Date(e.timestamp).toISOString() : new Date().toISOString(),
|
|
254
|
+
event_message:
|
|
255
|
+
typeof parsed === 'object' && parsed && (parsed as Record<string, unknown>).msg
|
|
256
|
+
? String((parsed as Record<string, unknown>).msg)
|
|
257
|
+
: typeof message === 'string'
|
|
258
|
+
? message.slice(0, 500)
|
|
259
|
+
: String(message),
|
|
260
|
+
body: parsed as Record<string, unknown>,
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
logs,
|
|
266
|
+
total: logs.length,
|
|
267
|
+
tableName: `cloudwatch:${logGroup}`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async getLogSourceStats(): Promise<LogSourceStats[]> {
|
|
272
|
+
if (!this.cwLogGroup || !this.cwClient) {
|
|
273
|
+
throw new Error('CloudWatch not initialized');
|
|
274
|
+
}
|
|
275
|
+
const client = this.cwClient;
|
|
276
|
+
const logGroup = this.cwLogGroup;
|
|
277
|
+
const sources = await this.getLogSources();
|
|
278
|
+
const stats: LogSourceStats[] = [];
|
|
279
|
+
const suffixMapping = this.getSuffixMapping();
|
|
280
|
+
|
|
281
|
+
const dls = await client.send(new DescribeLogStreamsCommand({ logGroupName: logGroup }));
|
|
282
|
+
const streams = dls.logStreams || [];
|
|
283
|
+
|
|
284
|
+
for (const src of sources) {
|
|
285
|
+
const suffix = suffixMapping[src.name] || suffixMapping[this.getDisplayName(src.name)] || '';
|
|
286
|
+
const sourceStreams = streams
|
|
287
|
+
.map((s) => s.logStreamName || '')
|
|
288
|
+
.filter((name) => (suffix ? name.includes(suffix) : true));
|
|
289
|
+
|
|
290
|
+
let lastActivity = '';
|
|
291
|
+
|
|
292
|
+
if (sourceStreams.length > 0) {
|
|
293
|
+
try {
|
|
294
|
+
// Use EXACTLY the same approach as getLogsBySource to get consistent results
|
|
295
|
+
const endMs = Date.now();
|
|
296
|
+
const startMs = endMs - 24 * 60 * 60 * 1000; // Look back 24 hours (same as getLogsBySource)
|
|
297
|
+
|
|
298
|
+
// Use CloudWatch Insights to efficiently get the latest timestamp
|
|
299
|
+
try {
|
|
300
|
+
const end = Date.now();
|
|
301
|
+
const start = end - 24 * 60 * 60 * 1000; // Last 24 hours
|
|
302
|
+
|
|
303
|
+
const insights = `fields @timestamp | filter @logStream like /${suffix}/ | sort @timestamp desc | limit 1`;
|
|
304
|
+
|
|
305
|
+
const startQuery = await client.send(
|
|
306
|
+
new StartQueryCommand({
|
|
307
|
+
logGroupName: logGroup,
|
|
308
|
+
startTime: Math.floor(start / 1000),
|
|
309
|
+
endTime: Math.floor(end / 1000),
|
|
310
|
+
queryString: insights,
|
|
311
|
+
limit: 1,
|
|
312
|
+
})
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const qid = startQuery.queryId || '';
|
|
316
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
317
|
+
let results;
|
|
318
|
+
|
|
319
|
+
// Wait for query to complete (shorter timeout for stats)
|
|
320
|
+
for (let i = 0; i < 10; i++) {
|
|
321
|
+
const r = await client.send(new GetQueryResultsCommand({ queryId: qid }));
|
|
322
|
+
if (r.status === 'Complete' || r.status === 'Failed' || r.status === 'Cancelled') {
|
|
323
|
+
results = r.results || [];
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
await sleep(200);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (results && results.length > 0) {
|
|
330
|
+
const row = results[0];
|
|
331
|
+
const timestampField = row.find((field) => field.field === '@timestamp');
|
|
332
|
+
if (timestampField && timestampField.value) {
|
|
333
|
+
// CloudWatch Insights returns timestamp as string, try different parsing methods
|
|
334
|
+
let timestamp: number;
|
|
335
|
+
const value = timestampField.value;
|
|
336
|
+
|
|
337
|
+
// Try parsing as milliseconds first
|
|
338
|
+
timestamp = parseInt(value);
|
|
339
|
+
if (isNaN(timestamp) || timestamp < 1000000000000) {
|
|
340
|
+
// Check if it's a reasonable timestamp (after 2001)
|
|
341
|
+
// If that fails, try parsing as ISO string
|
|
342
|
+
timestamp = Date.parse(value);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!isNaN(timestamp) && timestamp > 1000000000000) {
|
|
346
|
+
// Ensure it's a valid recent timestamp
|
|
347
|
+
lastActivity = new Date(timestamp).toISOString();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
// Fallback to FilterLogEvents with limited pagination
|
|
353
|
+
logger.warn(`CloudWatch Insights failed for stats, using fallback for ${src.name}`, {
|
|
354
|
+
error: error instanceof Error ? error.message : String(error),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const fle = await client.send(
|
|
358
|
+
new FilterLogEventsCommand({
|
|
359
|
+
logGroupName: logGroup,
|
|
360
|
+
logStreamNames: sourceStreams.length > 0 ? sourceStreams.slice(0, 100) : undefined,
|
|
361
|
+
startTime: startMs,
|
|
362
|
+
endTime: endMs,
|
|
363
|
+
limit: 1000, // Reasonable limit for fallback
|
|
364
|
+
})
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const events = fle.events || [];
|
|
368
|
+
if (events.length > 0) {
|
|
369
|
+
const latestEvent = events[events.length - 1];
|
|
370
|
+
if (latestEvent.timestamp) {
|
|
371
|
+
lastActivity = new Date(latestEvent.timestamp).toISOString();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
// Fallback to stream lastIngestionTime if filtering fails
|
|
377
|
+
const last = streams
|
|
378
|
+
.filter((s) => (s.logStreamName || '').includes(suffix))
|
|
379
|
+
.reduce<number | null>((acc, s) => {
|
|
380
|
+
const t = s.lastIngestionTime ?? s.creationTime ?? null;
|
|
381
|
+
if (t === null) {
|
|
382
|
+
return acc;
|
|
383
|
+
}
|
|
384
|
+
if (acc === null) {
|
|
385
|
+
return t;
|
|
386
|
+
}
|
|
387
|
+
return Math.max(acc, t);
|
|
388
|
+
}, null);
|
|
389
|
+
|
|
390
|
+
if (last) {
|
|
391
|
+
lastActivity = new Date(last).toISOString();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
stats.push({
|
|
397
|
+
source: src.name,
|
|
398
|
+
count: 0, // CloudWatch doesn't provide easy count without querying
|
|
399
|
+
lastActivity,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return stats;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async searchLogs(
|
|
407
|
+
query: string,
|
|
408
|
+
sourceName?: string,
|
|
409
|
+
limit: number = 100,
|
|
410
|
+
_offset = 0 // CloudWatch doesn't support offset-based pagination
|
|
411
|
+
): Promise<{
|
|
412
|
+
logs: (AnalyticsLogRecord & { source: string })[];
|
|
413
|
+
total: number;
|
|
414
|
+
}> {
|
|
415
|
+
if (!this.cwLogGroup || !this.cwClient) {
|
|
416
|
+
throw new Error('CloudWatch not initialized');
|
|
417
|
+
}
|
|
418
|
+
const client = this.cwClient;
|
|
419
|
+
const logGroup = this.cwLogGroup;
|
|
420
|
+
const end = Date.now();
|
|
421
|
+
const start = end - 24 * 60 * 60 * 1000; // Default to last 24 hours for better performance
|
|
422
|
+
|
|
423
|
+
const escaped = query.replace(/"/g, '\\"');
|
|
424
|
+
let insights = `fields @timestamp, @message, @logStream | filter @message like /${escaped}/`;
|
|
425
|
+
|
|
426
|
+
if (sourceName) {
|
|
427
|
+
const suffixMapping = this.getSuffixMapping();
|
|
428
|
+
const suffix =
|
|
429
|
+
suffixMapping[sourceName] || suffixMapping[this.getDisplayName(sourceName)] || '';
|
|
430
|
+
if (suffix) {
|
|
431
|
+
insights += ` | filter @logStream like /${suffix}/`;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// CloudWatch Insights allows explicit sorting - keeping DESC for search results
|
|
436
|
+
insights += ` | sort @timestamp desc | limit ${limit}`;
|
|
437
|
+
|
|
438
|
+
const startQuery = await client.send(
|
|
439
|
+
new StartQueryCommand({
|
|
440
|
+
logGroupName: logGroup,
|
|
441
|
+
startTime: Math.floor(start / 1000),
|
|
442
|
+
endTime: Math.floor(end / 1000),
|
|
443
|
+
queryString: insights,
|
|
444
|
+
limit,
|
|
445
|
+
})
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const qid = startQuery.queryId || '';
|
|
449
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
450
|
+
let results;
|
|
451
|
+
|
|
452
|
+
for (let i = 0; i < 20; i++) {
|
|
453
|
+
const r = await client.send(new GetQueryResultsCommand({ queryId: qid }));
|
|
454
|
+
if (r.status === 'Complete' || r.status === 'Failed' || r.status === 'Cancelled') {
|
|
455
|
+
results = r.results || [];
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
await sleep(300);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const rows = results || [];
|
|
462
|
+
const toObj = (row: Array<{ field?: string; value?: string }>) =>
|
|
463
|
+
Object.fromEntries(row.map((c) => [c.field || '', c.value || '']));
|
|
464
|
+
|
|
465
|
+
const mapped: (AnalyticsLogRecord & { source: string })[] = rows.map((r) => {
|
|
466
|
+
const o = toObj(r);
|
|
467
|
+
const msg = o['@message'] || '';
|
|
468
|
+
let parsed: Record<string, unknown> = {};
|
|
469
|
+
try {
|
|
470
|
+
parsed = JSON.parse(msg);
|
|
471
|
+
} catch {
|
|
472
|
+
parsed = { message: msg };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const logStream: string = o['@logStream'] || '';
|
|
476
|
+
const source: string = logStream.includes('postgrest')
|
|
477
|
+
? 'postgREST.logs'
|
|
478
|
+
: logStream.includes('postgres')
|
|
479
|
+
? 'postgres.logs'
|
|
480
|
+
: logStream.includes('function')
|
|
481
|
+
? 'function.logs'
|
|
482
|
+
: 'insforge.logs';
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
id: `${o['@logStream']}-${o['@timestamp']}`,
|
|
486
|
+
// CloudWatch Insights returns timestamp as string in milliseconds
|
|
487
|
+
timestamp: o['@timestamp']
|
|
488
|
+
? new Date(parseInt(o['@timestamp'])).toISOString()
|
|
489
|
+
: new Date().toISOString(),
|
|
490
|
+
event_message:
|
|
491
|
+
typeof parsed === 'object' && (parsed as Record<string, unknown>).msg
|
|
492
|
+
? String((parsed as Record<string, unknown>).msg)
|
|
493
|
+
: typeof msg === 'string'
|
|
494
|
+
? msg.slice(0, 500)
|
|
495
|
+
: String(msg),
|
|
496
|
+
body: parsed as Record<string, unknown>,
|
|
497
|
+
source,
|
|
498
|
+
};
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
logs: mapped,
|
|
503
|
+
total: mapped.length,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async close(): Promise<void> {
|
|
508
|
+
// CloudWatch client doesn't need explicit closing
|
|
509
|
+
}
|
|
510
|
+
}
|