Task 6.2 housekeeping — add BatchResultApplier to the class map per CLAUDE.md convention. Introduced in Task 2.2 as the @Transactional wrapper for atomic per-rule batch commits (instance writes + notification enqueues + cursor advance). Also refreshes GitNexus index stats auto-emitted into AGENTS.md / CLAUDE.md (8778 -> 8893 nodes, 22647 -> 23049 edges). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22 KiB
paths
| paths | |
|---|---|
|
App Module Key Classes
cameleer-server-app/src/main/java/com/cameleer/server/app/
URL taxonomy
User-facing data and config endpoints live under /api/v1/environments/{envSlug}/.... Env is a path segment, never a query param. The envSlug is resolved to an Environment bean via the @EnvPath argument resolver (web/EnvironmentPathResolver.java) — 404 on unknown slug.
Slugs are immutable after creation for both environments and apps. Slug regex: ^[a-z0-9][a-z0-9-]{0,63}$. Validated in EnvironmentService.create and AppService.createApp. Update endpoints (PUT) do not accept a slug field; Jackson drops it as an unknown property.
Flat-endpoint allow-list
These paths intentionally stay flat (no /environments/{envSlug} prefix). Every new endpoint should be env-scoped unless it appears here and the reason is documented.
| Path prefix | Why flat |
|---|---|
/api/v1/data/** |
Agent ingestion. JWT env claim is authoritative; URL-embedded env would invite spoofing. |
/api/v1/agents/register, /refresh, /{id}/heartbeat, /{id}/events (SSE), /{id}/deregister, /{id}/commands, /{id}/commands/{id}/ack, /{id}/replay |
Agent self-service; JWT-bound. |
/api/v1/agents/commands, /api/v1/agents/groups/{group}/commands |
Operator fan-out; target scope is explicit in query params. |
/api/v1/agents/config |
Agent-authoritative config read; JWT → registry → (app, env). |
/api/v1/admin/{users,roles,groups,oidc,license,audit,rbac/stats,claim-mappings,thresholds,sensitive-keys,usage,clickhouse,database,environments,outbound-connections} |
Truly cross-env admin. Env CRUD URLs use {envSlug}, not UUID. |
/api/v1/catalog, /api/v1/catalog/{applicationId} |
Cross-env discovery is the purpose. Env is an optional filter via ?environment=. |
/api/v1/executions/{execId}, /processors/** |
Exchange IDs are globally unique; permalinks. |
/api/v1/diagrams/{contentHash}/render, POST /api/v1/diagrams/render |
Content-addressed or stateless. |
/api/v1/alerts/notifications/{id}/retry |
Notification IDs are globally unique; no env routing needed. |
/api/v1/auth/** |
Pre-auth; no env context exists. |
/api/v1/health, /prometheus, /api-docs/**, /swagger-ui/** |
Server metadata. |
Tenant isolation invariant
ClickHouse is shared across tenants. Every ClickHouse query must filter by tenant_id (from CAMELEER_SERVER_TENANT_ID env var, resolved via TenantContext/config) in addition to environment. New controllers added under /environments/{envSlug}/... must preserve this — the env filter from the path does not replace the tenant filter.
User ID conventions
users.user_id stores the bare identifier:
- Local users:
<username>(e.g.admin,alice) - OIDC users:
oidc:<sub>(e.g.oidc:c7a93b…)
JWT subjects carry a user: namespace prefix (user:admin, user:oidc:<sub>) so JwtAuthenticationFilter can distinguish user tokens from agent tokens. All three write paths upsert the bare form:
UiAuthController.login— computesuserId = request.username(), signs withsubject = "user:" + userId.OidcAuthController.callback—userId = "oidc:" + oidcUser.subject(), signs withsubject = "user:" + userId.UserAdminController.createUser—userId = request.username().
Env-scoped read-path controllers (AlertController, AlertRuleController, AlertSilenceController, OutboundConnectionAdminController) strip "user:" from SecurityContextHolder.authentication.name before using it as an FK. All FKs to users(user_id) (e.g. alert_rules.created_by, outbound_connections.created_by, alert_reads.user_id, user_roles.user_id, user_groups.user_id) therefore reference the bare form. If you add a new controller that needs the acting user id for an FK insert, follow the same strip pattern.
controller/ — REST endpoints
Env-scoped (user-facing data & config)
AppController—/api/v1/environments/{envSlug}/apps. GET list / POST create / GET{appSlug}/ DELETE{appSlug}/ GET{appSlug}/versions/ POST{appSlug}/versions(JAR upload) / PUT{appSlug}/container-config. App slug uniqueness is per-env ((env, app_slug)is the natural key).CreateAppRequestbody has no env (path), validates slug regex.DeploymentController—/api/v1/environments/{envSlug}/apps/{appSlug}/deployments. GET list / POST create (body{ appVersionId }) / POST{id}/stop/ POST{id}/promote(body{ targetEnvironment: slug }— target app slug must exist in target env) / GET{id}/logs.ApplicationConfigController—/api/v1/environments/{envSlug}. GET/config(list), GET/PUT/apps/{appSlug}/config, GET/apps/{appSlug}/processor-routes, POST/apps/{appSlug}/config/test-expression. PUT also pushesCONFIG_UPDATEto LIVE agents in this env.AppSettingsController—/api/v1/environments/{envSlug}. GET/app-settings(list), GET/PUT/DELETE/apps/{appSlug}/settings. ADMIN/OPERATOR only.SearchController—/api/v1/environments/{envSlug}. GET/executions, POST/executions/search, GET/stats,/stats/timeseries,/stats/timeseries/by-app,/stats/timeseries/by-route,/stats/punchcard,/attributes/keys,/errors/top.LogQueryController— GET/api/v1/environments/{envSlug}/logs(filters: source (multi, comma-split, OR-joined), level (multi, comma-split, OR-joined), application, agentId, exchangeId, logger, q, time range; sort asc/desc). Cursor-paginated, returns{ data, nextCursor, hasMore, levelCounts }; cursor is base64url of"{timestampIso}|{insert_id_uuid}"— same-millisecond tiebreak via theinsert_idUUID column onlogs.RouteCatalogController— GET/api/v1/environments/{envSlug}/routes(merged route catalog from registry + ClickHouse; env filter unconditional).RouteMetricsController— GET/api/v1/environments/{envSlug}/routes/metrics, GET/api/v1/environments/{envSlug}/routes/metrics/processors.AgentListController— GET/api/v1/environments/{envSlug}/agents(registered agents with runtime metrics, filtered to env).AgentEventsController— GET/api/v1/environments/{envSlug}/agents/events(lifecycle events; cursor-paginated, returns{ data, nextCursor, hasMore }; order(timestamp DESC, insert_id DESC); cursor is base64url of"{timestampIso}|{insert_id_uuid}"—insert_idis a stable UUID column used as a same-millisecond tiebreak).AgentMetricsController— GET/api/v1/environments/{envSlug}/agents/{agentId}/metrics(JVM/Camel metrics). Rejects cross-env agents (404) as defence-in-depth.DiagramRenderController— GET/api/v1/environments/{envSlug}/apps/{appSlug}/routes/{routeId}/diagram(env-scoped lookup). Also GET/api/v1/diagrams/{contentHash}/render(flat — content hashes are globally unique).AlertRuleController—/api/v1/environments/{envSlug}/alerts/rules. GET list / POST create / GET{id}/ PUT{id}/ DELETE{id}/ POST{id}/enable/ POST{id}/disable/ POST{id}/render-preview/ POST{id}/test-evaluate. OPERATOR+ for mutations, VIEWER+ for reads. CRITICAL: attribute keys inExchangeMatchCondition.filter.attributesare validated at rule-save time against^[a-zA-Z0-9._-]+$— they are later inlined into ClickHouse SQL.AgentLifecycleConditionis allowlist-only — theAgentLifecycleEventTypeenum (REGISTERED / RE_REGISTERED / DEREGISTERED / WENT_STALE / WENT_DEAD / RECOVERED) plus the record compact ctor (non-emptyeventTypes,withinSeconds ≥ 1) do the validation; custom agent-emitted event types are tracked in backlog issue #145. Webhook validation: verifiesoutboundConnectionIdexists andisAllowedInEnvironment. Null notification templates default to""(NOT NULL constraint). Audit:ALERT_RULE_CHANGE.AlertController—/api/v1/environments/{envSlug}/alerts. GET list (inbox filtered by userId/groupIds/roleNames viaInAppInboxQuery; optional multi-valuestate,severity, tri-stateacked, tri-statereadquery params; soft-deleted rows always excluded) / GET/unread-count/ GET{id}/ POST{id}/ack/ POST{id}/read/ POST/bulk-read/ POST/bulk-ack(VIEWER+) / DELETE{id}(OPERATOR+, soft-delete) / POST/bulk-delete(OPERATOR+) / POST{id}/restore(OPERATOR+, clearsdeleted_at).requireLiveInstancehelper returns 404 on soft-deleted rows;restoreexplicitly fetches regardless ofdeleted_at.BulkIdsRequestis the shared body for bulk-read/ack/delete ({ instanceIds }).AlertDtoincludesreadAt;deletedAtis intentionally NOT on the wire. Inbox SQL:? = ANY(target_user_ids) OR target_group_ids && ? OR target_role_names && ?— requires at least one matching target (no broadcast concept).AlertSilenceController—/api/v1/environments/{envSlug}/alerts/silences. GET list / POST create / DELETE{id}. 422 ifendsAt <= startsAt. OPERATOR+ for mutations, VIEWER+ for list. Audit:ALERT_SILENCE_CHANGE.AlertNotificationController— Dual-path (no class-level prefix). GET/api/v1/environments/{envSlug}/alerts/{alertId}/notifications(VIEWER+); POST/api/v1/alerts/notifications/{id}/retry(OPERATOR+, flat — notification IDs globally unique). Retry resets attempts to 0 and setsnextAttemptAt = now.
Env admin (env-slug-parameterized, not env-scoped data)
EnvironmentAdminController—/api/v1/admin/environments. GET list / POST create / GET{envSlug}/ PUT{envSlug}/ DELETE{envSlug}/ PUT{envSlug}/default-container-config/ PUT{envSlug}/jar-retention. Slug immutable — PUT body has no slug field; any slug supplied is dropped by Jackson. Slug validated on POST.
Agent-only (JWT-authoritative, intentionally flat)
AgentRegistrationController— POST/register(requiresenvironmentIdin body; 400 if missing), POST/{id}/refresh(rejects tokens with noenvclaim), POST/{id}/heartbeat(env from body preferred, JWT fallback; 400 if neither), POST/{id}/deregister.AgentSseController— GET/{id}/events(SSE connection).AgentCommandController— POST/{agentId}/commands, POST/groups/{group}/commands, POST/commands(broadcast), POST/{agentId}/commands/{commandId}/ack, POST/{agentId}/replay.AgentConfigController— GET/api/v1/agents/config. Agent-authoritative config read: resolves (app, env) from JWT subject → registry (registry miss falls back to JWT env claim; no registry entry → 404 since application can't be derived).
Ingestion (agent-only, JWT-authoritative)
LogIngestionController— POST/api/v1/data/logs(acceptsList<LogEntry>; WARNs on missing identity, unregistered agents, empty payloads, buffer-full drops).EventIngestionController— POST/api/v1/data/events.ChunkIngestionController— POST/api/v1/data/executions. Accepts a singleExecutionChunkor an array (fields includeexchangeId,applicationId,instanceId,routeId,status,startTime,endTime,durationMs,chunkSeq,final,processors: FlatProcessorRecord[]). The accumulator merges non-final chunks by exchangeId and emits the merged envelope on the final chunk or on stale timeout. LegacyExecutionController/RouteExecutionshape is retired.MetricsController— POST/api/v1/data/metrics.DiagramController— POST/api/v1/data/diagrams(resolves applicationId + environment from the agent registry keyed on JWT subject; stamps both on the storedTaggedDiagram).
Cross-env discovery (flat)
CatalogController— GET/api/v1/catalog(merges managed apps + in-memory agents + CH stats; optional?environment=filter). DELETE/api/v1/catalog/{applicationId}(ADMIN: dismiss app, purge all CH data + PG record).
Admin (cross-env, flat)
UserAdminController— CRUD/api/v1/admin/users, POST/{id}/roles, POST/{id}/set-password.RoleAdminController— CRUD/api/v1/admin/roles.GroupAdminController— CRUD/api/v1/admin/groups.OidcConfigAdminController— GET/POST/api/v1/admin/oidc, POST/test.OutboundConnectionAdminController—/api/v1/admin/outbound-connections. GET list / POST create / GET{id}/ PUT{id}/ DELETE{id}/ POST{id}/test/ GET{id}/usage. RBAC: list/get/usage ADMIN|OPERATOR; mutations + test ADMIN.SensitiveKeysAdminController— GET/PUT/api/v1/admin/sensitive-keys. GET returns 200 or 204 if not configured. PUT accepts{ keys: [...] }with optional?pushToAgents=true. Fan-out iterates every distinct(application, environment)slice — intentional global baseline + per-env overrides.ClaimMappingAdminController— CRUD/api/v1/admin/claim-mappings, POST/test.LicenseAdminController— GET/POST/api/v1/admin/license.ThresholdAdminController— CRUD/api/v1/admin/thresholds.AuditLogController— GET/api/v1/admin/audit.RbacStatsController— GET/api/v1/admin/rbac/stats.UsageAnalyticsController— GET/api/v1/admin/usage(ClickHouseusage_events).ClickHouseAdminController— GET/api/v1/admin/clickhouse/**(conditional oninfrastructureendpointsflag).DatabaseAdminController— GET/api/v1/admin/database/**(conditional oninfrastructureendpointsflag).
Other (flat)
DetailController— GET/api/v1/executions/{executionId}+ processor snapshot endpoints.MetricsController— exposes/api/v1/metricsand/api/v1/prometheus(server-side Prometheus scrape endpoint).
runtime/ — Docker orchestration
DockerRuntimeOrchestrator— implements RuntimeOrchestrator; Docker Java client (zerodep transport), container lifecycleDeploymentExecutor— @Async staged deploy: PRE_FLIGHT -> PULL_IMAGE -> CREATE_NETWORK -> START_REPLICAS -> HEALTH_CHECK -> SWAP_TRAFFIC -> COMPLETE. Container names are{tenantId}-{envSlug}-{appSlug}-{replicaIndex}(globally unique on Docker daemon). Sets per-replicaCAMELEER_AGENT_INSTANCEIDenv var to{envSlug}-{appSlug}-{replicaIndex}.DockerNetworkManager— ensures bridge networks (cameleer-traefik, cameleer-env-{slug}), connects containersDockerEventMonitor— persistent Docker event stream listener (die, oom, start, stop), updates deployment statusTraefikLabelBuilder— generates Traefik Docker labels for path-based or subdomain routing. Also emitscameleer.replicaandcameleer.instance-idlabels per container for labels-first identity.PrometheusLabelBuilder— generates Prometheus Docker labels (prometheus.scrape/path/port) per runtime type fordocker_sd_configsauto-discoveryContainerLogForwarder— streams Docker container stdout/stderr to ClickHouse withsource='container'. One follow-stream thread per container, batches lines every 2s/50 lines viaClickHouseLogStore.insertBufferedBatch(). 60-second max capture timeout.DisabledRuntimeOrchestrator— no-op when runtime not enabled
metrics/ — Prometheus observability
ServerMetrics— centralized business metrics: gauges (agents by state, SSE connections, buffer depths), counters (ingestion drops, agent transitions, deployment outcomes, auth failures), timers (flush duration, deployment duration). Exposed via/api/v1/prometheus.
storage/ — PostgreSQL repositories (JdbcTemplate)
PostgresAppRepository,PostgresAppVersionRepository,PostgresEnvironmentRepositoryPostgresDeploymentRepository— includes JSONB replica_states, deploy_stage, findByContainerIdPostgresUserRepository,PostgresRoleRepository,PostgresGroupRepositoryPostgresAuditRepository,PostgresOidcConfigRepository,PostgresClaimMappingRepository,PostgresSensitiveKeysRepositoryPostgresAppSettingsRepository,PostgresApplicationConfigRepository,PostgresThresholdRepository. Bothapp_settingsandapplication_configare env-scoped (PK(app_id, environment)/(application, environment)); finders take(app, env)— no env-agnostic variants.
storage/ — ClickHouse stores
ClickHouseExecutionStore,ClickHouseMetricsStore,ClickHouseMetricsQueryStoreClickHouseStatsStore— pre-aggregated stats, punchcardClickHouseDiagramStore,ClickHouseAgentEventRepositoryClickHouseUsageTracker— usage_events for billingClickHouseRouteCatalogStore— persistent route catalog with first_seen cache, warm-loaded on startup
search/ — ClickHouse search and log stores
ClickHouseLogStore— log storage and query, MDC-based exchange/processor correlationClickHouseSearchIndex— full-text search
security/ — Spring Security
SecurityConfig— WebSecurityFilterChain, JWT filter, CORS, OIDC conditional./api/v1/admin/outbound-connections/**GETs permit OPERATOR in addition to ADMIN (defense-in-depth at controller level); mutations remain ADMIN-only. Alerting matchers: GET/environments/*/alerts/**VIEWER+; POST/PUT/DELETE rules and silences OPERATOR+; ack/read/bulk-read VIEWER+; POST/alerts/notifications/*/retryOPERATOR+.JwtAuthenticationFilter— OncePerRequestFilter, validates Bearer tokensJwtServiceImpl— HMAC-SHA256 JWT (Nimbus JOSE)UiAuthController—/api/v1/auth(login, refresh, me). Upsertsusers.user_id = request.username()(bare); signs JWTs withsubject = "user:" + userId.refresh/mestrip the"user:"prefix from incoming subjects viastripSubjectPrefix()before any DB/RBAC lookup.OidcAuthController—/api/v1/auth/oidc(login-uri, token-exchange, logout). Upsertsusers.user_id = "oidc:" + oidcUser.subject()(nouser:prefix); signs JWTs withsubject = "user:oidc:" + oidcUser.subject().applyClaimMappings+getSystemRoleNamescalls all use the bareoidc:<sub>form.OidcTokenExchanger— code -> tokens, role extraction from access_token then id_tokenOidcProviderHelper— OIDC discovery, JWK source cache
agent/ — Agent lifecycle
SseConnectionManager— manages per-agent SSE connections, delivers commandsAgentLifecycleMonitor— @Scheduled 10s, LIVE->STALE->DEAD transitionsSsePayloadSigner— Ed25519 signs SSE payloads for agent verification
retention/ — JAR cleanup
JarRetentionJob— @Scheduled 03:00 daily, per-environment retention, skips deployed versions
alerting/eval/ — Rule evaluation
AlertEvaluatorJob— @Scheduled tick driver; per-rule claim/release viaAlertRuleRepository, dispatches to per-kindConditionEvaluator, persists advanced cursor on release viaAlertRule.withEvalState.BatchResultApplier—@Componentthat wraps a single rule's tick outcome (EvalResult.Batch=firings+nextEvalState) in one@Transactionalboundary: instance upserts + notification enqueues + cursor advance commit atomically or roll back together. This is the exactly-once-per-exchange guarantee forPER_EXCHANGEfire mode.ConditionEvaluator— interface; per-kind implementations:ExchangeMatchEvaluator,AgentLifecycleEvaluator,AgentStateEvaluator,DeploymentStateEvaluator,JvmMetricEvaluator,LogPatternEvaluator,RouteMetricEvaluator.AlertStateTransitions— PER_EXCHANGE vs rule-level FSM helpers (fire/resolve/ack).PerKindCircuitBreaker— trips noisy per-kind evaluators;TickCache— per-tick shared lookups (apps, envs, silences).
http/ — Outbound HTTP client implementation
SslContextBuilder— composes SSL context fromOutboundHttpProperties+OutboundHttpRequestContext. Supports SYSTEM_DEFAULT (JDK roots + configured CA extras), TRUST_ALL (short-circuit no-op TrustManager), TRUST_PATHS (JDK roots + system extras + per-request extras). ThrowsIllegalArgumentException("CA file not found: ...")on missing PEM.ApacheOutboundHttpClientFactory— Apache HttpClient 5 impl ofOutboundHttpClientFactory. Memoizes clients perCacheKey(trustAll, caPaths, mode, connectTimeout, readTimeout). AppliesNoopHostnameVerifierwhen trust-all is active.config/OutboundHttpConfig—@ConfigurationProperties("cameleer.server.outbound-http"). Exposes beans:OutboundHttpProperties,SslContextBuilder,OutboundHttpClientFactory.@PostConstructlogs WARN on trust-all and throws if configured CA paths don't exist.
outbound/ — Admin-managed outbound connections (implementation)
crypto/SecretCipher— AES-GCM symmetric cipher with key derived via HMAC-SHA256(jwtSecret, "cameleer-outbound-secret-v1"). Ciphertext format: base64(IV(12 bytes) || GCM output with 128-bit tag).encryptthrowsIllegalStateException;decryptthrowsIllegalArgumentExceptionon tamper/wrong-key/malformed.storage/PostgresOutboundConnectionRepository— JdbcTemplate impl.save()upserts by id; JSONB serialization via ObjectMapper; UUID arrays viaConnectionCallback. Readscreated_by/updated_byas String (= users.user_id TEXT).OutboundConnectionServiceImpl— service layer. Tenant bound at construction viacameleer.server.tenant.idproperty. Uniqueness check viafindByName. Narrowing-envs guard: rejects update that removes envs while rules reference the connection (rulesReferencing stubbed in Plan 01, wired in Plan 02). Delete guard: rejects if referenced by rules.controller/OutboundConnectionAdminController— REST controller. Class-level@PreAuthorize("hasRole('ADMIN')")defaults; GETs relaxed to ADMIN|OPERATOR. Resolves acting user id via the user-id convention (strip"user:"fromauthentication.name→ matchesusers.user_idFK). Audit viaAuditCategory.OUTBOUND_CONNECTION_CHANGE.dto/OutboundConnectionRequest— Bean Validation:@NotBlankname,@Pattern("^https://.+")url,@NotNullmethod/tlsTrustMode/auth. Compact ctor throwsIllegalArgumentExceptionif TRUST_PATHS with empty paths list.dto/OutboundConnectionDto— response DTO.hmacSecretSet: booleaninstead of the ciphertext;authKind: OutboundAuthKindinstead of the full auth config.dto/OutboundConnectionTestResult— result of POST/{id}/test: status, latencyMs, responseSnippet (first 512 chars), tlsProtocol/cipherSuite/peerCertSubject (protocol is "TLS" stub; enriched in Plan 02 follow-up), error (nullable).config/OutboundBeanConfig— registersOutboundConnectionRepository,SecretCipher,OutboundConnectionServicebeans.
config/ — Spring beans
RuntimeOrchestratorAutoConfig— conditional Docker/Disabled orchestrator + NetworkManager + EventMonitorRuntimeBeanConfig— DeploymentExecutor, AppService, EnvironmentServiceSecurityBeanConfig— JwtService, Ed25519, BootstrapTokenValidatorStorageBeanConfig— all repositoriesClickHouseConfig— ClickHouse JdbcTemplate, schema initializer