Accept autonomous run log streams
This commit is contained in:
@@ -84,6 +84,8 @@ Named log DTOs:
|
|||||||
|
|
||||||
Log ingest supports bounded batches, sequence ranges, checksum validation, retry-safe duplicate acknowledgement, latest sequence tracking, cursor query, and browser SSE fan-out from already-ingested platform logs. Log payloads must not carry artifact chunks, host paths, raw credentials, direct sockets, or unbounded inline data.
|
Log ingest supports bounded batches, sequence ranges, checksum validation, retry-safe duplicate acknowledgement, latest sequence tracking, cursor query, and browser SSE fan-out from already-ingested platform logs. Log payloads must not carry artifact chunks, host paths, raw credentials, direct sockets, or unbounded inline data.
|
||||||
|
|
||||||
|
Run-assigned Platform jobs use `job.<jobId>.<streamKey>` log stream IDs. Autonomous lifecycle bootstrap is Run-owned machine execution rather than a Platform job, so its durable process logs use `run.<runEndpointId>.<serverInstanceId>.<streamKey>`. Platform may auto-create those streams only after validating the active Run session and the server-to-Run binding. For retry compatibility, legacy spooled `job.autonomous-*.<streamKey>` batches are accepted as Run-owned autonomous streams without creating or completing a Platform job.
|
||||||
|
|
||||||
Log ingest is durable and independently retried. Artifact/file transfer backlog must not prevent log batch acknowledgement, duplicate acknowledgement, cursor state updates, or spool cleanup.
|
Log ingest is durable and independently retried. Artifact/file transfer backlog must not prevent log batch acknowledgement, duplicate acknowledgement, cursor state updates, or spool cleanup.
|
||||||
|
|
||||||
The platform stores log stream metadata through `repo.Store` and stores log bodies through the configured `LogBodyStore`. The default `file` backend persists platform metadata to `PLATFORM_METADATA_PATH` and appends log entries to segmented JSONL files under `PLATFORM_LOG_DIR`; the `memory` backend is only for tests and disposable local development. MySQL/Postgres are appropriate for platform metadata, stream state, retention policy, indexes, and audit records, but should not be the primary row-per-log-line store for hundreds or thousands of servers. Production log bodies should move behind the same boundary to append/query backends such as ClickHouse, Loki, OpenSearch/Elasticsearch, or object-storage segments with compact indexes.
|
The platform stores log stream metadata through `repo.Store` and stores log bodies through the configured `LogBodyStore`. The default `file` backend persists platform metadata to `PLATFORM_METADATA_PATH` and appends log entries to segmented JSONL files under `PLATFORM_LOG_DIR`; the `memory` backend is only for tests and disposable local development. MySQL/Postgres are appropriate for platform metadata, stream state, retention policy, indexes, and audit records, but should not be the primary row-per-log-line store for hundreds or thousands of servers. Production log bodies should move behind the same boundary to append/query backends such as ClickHouse, Loki, OpenSearch/Elasticsearch, or object-storage segments with compact indexes.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (svc *CoreService) IngestLogBatch(batch domain.LogBatchIngest) (domain.LogB
|
|||||||
stamp := svc.now()
|
stamp := svc.now()
|
||||||
stream, err := svc.store.LogStreams().Get(batch.LogStreamID)
|
stream, err := svc.store.LogStreams().Get(batch.LogStreamID)
|
||||||
if errors.Is(err, repo.ErrNotFound) {
|
if errors.Is(err, repo.ErrNotFound) {
|
||||||
if repairErr := svc.ensureJobLogStreamForBatch(batch, stamp); repairErr == nil {
|
if repairErr := svc.ensureLogStreamForBatch(batch, stamp); repairErr == nil {
|
||||||
stream, err = svc.store.LogStreams().Get(batch.LogStreamID)
|
stream, err = svc.store.LogStreams().Get(batch.LogStreamID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,6 +112,49 @@ func (svc *CoreService) ensureJobLogStreamForBatch(batch domain.LogBatchIngest,
|
|||||||
return svc.ensureJobLogStreams(job, stamp)
|
return svc.ensureJobLogStreams(job, stamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *CoreService) ensureLogStreamForBatch(batch domain.LogBatchIngest, stamp time.Time) error {
|
||||||
|
jobID, hasJobID := jobIDFromLogBatch(batch)
|
||||||
|
if hasJobID {
|
||||||
|
job, err := svc.store.Jobs().Get(jobID)
|
||||||
|
if err == nil {
|
||||||
|
if job.ServerInstanceID != batch.ServerInstanceID || job.RunEndpointID != batch.RunEndpointID {
|
||||||
|
return validationError("log batch job scope does not match stream")
|
||||||
|
}
|
||||||
|
return svc.ensureJobLogStreams(job, stamp)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, repo.ErrNotFound) || !strings.HasPrefix(jobID, "autonomous-") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return svc.ensureRunLogStreamForBatch(batch, stamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *CoreService) ensureRunLogStreamForBatch(batch domain.LogBatchIngest, stamp time.Time) error {
|
||||||
|
if batch.Source != domain.LogStreamSourceProcess && batch.Source != domain.LogStreamSourceManagementProgram {
|
||||||
|
return repo.ErrNotFound
|
||||||
|
}
|
||||||
|
if batch.LogStreamID != runLogStreamID(batch.RunEndpointID, batch.ServerInstanceID, batch.StreamKey) && !legacyAutonomousLogStream(batch) {
|
||||||
|
return repo.ErrNotFound
|
||||||
|
}
|
||||||
|
instance, err := svc.store.ServerInstances().Get(batch.ServerInstanceID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if instance.RunEndpointID != batch.RunEndpointID {
|
||||||
|
return validationError("server instance run endpoint must match log batch endpoint")
|
||||||
|
}
|
||||||
|
_, err = svc.CreateLogStream(domain.LogStream{ID: batch.LogStreamID, ServerInstanceID: batch.ServerInstanceID, Source: batch.Source, StreamKey: batch.StreamKey, StorageBackend: domain.LogStorageBackendLocalSegments, RetentionPolicy: "default", CreatedAt: stamp, UpdatedAt: stamp})
|
||||||
|
if errors.Is(err, repo.ErrDuplicate) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyAutonomousLogStream(batch domain.LogBatchIngest) bool {
|
||||||
|
jobID, ok := jobIDFromLogBatch(batch)
|
||||||
|
return ok && strings.HasPrefix(jobID, "autonomous-")
|
||||||
|
}
|
||||||
|
|
||||||
func jobIDFromLogBatch(batch domain.LogBatchIngest) (string, bool) {
|
func jobIDFromLogBatch(batch domain.LogBatchIngest) (string, bool) {
|
||||||
streamKey := strings.TrimSpace(batch.StreamKey)
|
streamKey := strings.TrimSpace(batch.StreamKey)
|
||||||
if streamKey == "" || !strings.HasPrefix(batch.LogStreamID, "job.") {
|
if streamKey == "" || !strings.HasPrefix(batch.LogStreamID, "job.") {
|
||||||
|
|||||||
@@ -233,6 +233,63 @@ func TestCoreServiceRepairsMissingDeclaredProcessLogStreamOnIngest(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCoreServiceAcceptsAutonomousRunLogStreamWithoutPlatformJob(t *testing.T) {
|
||||||
|
svc, sessionToken := newRegisteredLogIngestService(t)
|
||||||
|
entry := domain.LogEntry{Seq: 1, Timestamp: time.Date(2026, 7, 3, 12, 0, 1, 0, time.UTC), Level: "info", Line: "autonomous bootstrap output"}
|
||||||
|
streamID := runLogStreamID("run-local", "server-1", "scum.console.stdout")
|
||||||
|
ack, err := svc.IngestLogBatch(domain.LogBatchIngest{
|
||||||
|
RunEndpointID: "run-local",
|
||||||
|
SessionToken: sessionToken,
|
||||||
|
LogStreamID: streamID,
|
||||||
|
ServerInstanceID: "server-1",
|
||||||
|
StreamKey: "scum.console.stdout",
|
||||||
|
Source: domain.LogStreamSourceProcess,
|
||||||
|
FirstSeq: entry.Seq,
|
||||||
|
LastSeq: entry.Seq,
|
||||||
|
Compression: "none",
|
||||||
|
Checksum: validator.LogLineChecksum(entry.Line),
|
||||||
|
Entries: []domain.LogEntry{entry},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ingest autonomous run log batch: %v", err)
|
||||||
|
}
|
||||||
|
if !ack.Accepted || ack.LogStreamID != streamID || ack.LatestSeq != entry.Seq {
|
||||||
|
t.Fatalf("unexpected autonomous stream ack: %+v", ack)
|
||||||
|
}
|
||||||
|
stream, err := svc.GetLogStream(streamID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get autonomous stream: %v", err)
|
||||||
|
}
|
||||||
|
if stream.ServerInstanceID != "server-1" || stream.StreamKey != "scum.console.stdout" || stream.Source != domain.LogStreamSourceProcess {
|
||||||
|
t.Fatalf("unexpected autonomous stream metadata: %+v", stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreServiceAcceptsLegacyAutonomousJobLogStreamWithoutPlatformJob(t *testing.T) {
|
||||||
|
svc, sessionToken := newRegisteredLogIngestService(t)
|
||||||
|
entry := domain.LogEntry{Seq: 1, Timestamp: time.Date(2026, 7, 3, 12, 0, 1, 0, time.UTC), Level: "info", Line: "legacy autonomous bootstrap output"}
|
||||||
|
streamID := jobLogStreamID("autonomous-bootstrap-start", "scum.console.stdout")
|
||||||
|
ack, err := svc.IngestLogBatch(domain.LogBatchIngest{
|
||||||
|
RunEndpointID: "run-local",
|
||||||
|
SessionToken: sessionToken,
|
||||||
|
LogStreamID: streamID,
|
||||||
|
ServerInstanceID: "server-1",
|
||||||
|
StreamKey: "scum.console.stdout",
|
||||||
|
Source: domain.LogStreamSourceProcess,
|
||||||
|
FirstSeq: entry.Seq,
|
||||||
|
LastSeq: entry.Seq,
|
||||||
|
Compression: "none",
|
||||||
|
Checksum: validator.LogLineChecksum(entry.Line),
|
||||||
|
Entries: []domain.LogEntry{entry},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ingest legacy autonomous log batch: %v", err)
|
||||||
|
}
|
||||||
|
if !ack.Accepted || ack.LogStreamID != streamID || ack.LatestSeq != entry.Seq {
|
||||||
|
t.Fatalf("unexpected legacy autonomous stream ack: %+v", ack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCoreServiceRejectsOutOfOrderAndConflictingLogBatches(t *testing.T) {
|
func TestCoreServiceRejectsOutOfOrderAndConflictingLogBatches(t *testing.T) {
|
||||||
svc, sessionToken := newRegisteredLogIngestService(t)
|
svc, sessionToken := newRegisteredLogIngestService(t)
|
||||||
createLogStreamFixture(t, svc)
|
createLogStreamFixture(t, svc)
|
||||||
|
|||||||
@@ -2543,6 +2543,10 @@ func jobLogStreamID(jobID string, streamKey string) string {
|
|||||||
return fmt.Sprintf("job.%s.%s", jobID, streamKey)
|
return fmt.Sprintf("job.%s.%s", jobID, streamKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runLogStreamID(runEndpointID string, serverInstanceID string, streamKey string) string {
|
||||||
|
return fmt.Sprintf("run.%s.%s.%s", runEndpointID, serverInstanceID, streamKey)
|
||||||
|
}
|
||||||
|
|
||||||
func (svc *CoreService) GetJob(id string) (domain.Job, error) {
|
func (svc *CoreService) GetJob(id string) (domain.Job, error) {
|
||||||
job, err := svc.store.Jobs().Get(id)
|
job, err := svc.store.Jobs().Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user