승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
208 lines
5.6 KiB
Go
208 lines
5.6 KiB
Go
package workspace
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"sync/atomic"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
const workspaceObservationLogKey = "node_workspace_observation"
|
|
|
|
type workspaceObservationEvent string
|
|
|
|
const (
|
|
workspaceObservationTool workspaceObservationEvent = "tool"
|
|
workspaceObservationCleanup workspaceObservationEvent = "cleanup"
|
|
)
|
|
|
|
// workspaceObservation is deliberately raw-free. It carries no request id,
|
|
// workspace ref, path, command id, environment, content, stdout, stderr, or
|
|
// error text. Correlation is generated at Open and is not derived from any
|
|
// caller-controlled identity.
|
|
type workspaceObservation struct {
|
|
event workspaceObservationEvent
|
|
correlation string
|
|
operation string
|
|
outcome string
|
|
errorCode string
|
|
durationMS int64
|
|
truncated bool
|
|
processCount int32
|
|
artifactCount int32
|
|
}
|
|
|
|
type workspaceObserver interface {
|
|
Emit(workspaceObservation) error
|
|
}
|
|
|
|
type workspaceNoopObserver struct{}
|
|
|
|
func (workspaceNoopObserver) Emit(workspaceObservation) error { return nil }
|
|
|
|
type workspaceSafeObserver struct {
|
|
inner workspaceObserver
|
|
failures atomic.Int64
|
|
}
|
|
|
|
func (o *workspaceSafeObserver) Emit(observation workspaceObservation) error {
|
|
if o == nil || o.inner == nil {
|
|
return nil
|
|
}
|
|
defer func() {
|
|
if recover() != nil {
|
|
o.failures.Add(1)
|
|
}
|
|
}()
|
|
if err := o.inner.Emit(observation); err != nil {
|
|
o.failures.Add(1)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *workspaceSafeObserver) failureCount() int64 {
|
|
if o == nil {
|
|
return 0
|
|
}
|
|
return o.failures.Load()
|
|
}
|
|
|
|
type zapWorkspaceObserver struct {
|
|
logger *zap.Logger
|
|
}
|
|
|
|
func newZapWorkspaceObserver(logger *zap.Logger) workspaceObserver {
|
|
if logger == nil {
|
|
return workspaceNoopObserver{}
|
|
}
|
|
return &zapWorkspaceObserver{logger: logger}
|
|
}
|
|
|
|
func (o *zapWorkspaceObserver) Emit(observation workspaceObservation) error {
|
|
if o == nil || o.logger == nil {
|
|
return nil
|
|
}
|
|
o.logger.Info(workspaceObservationLogKey,
|
|
zap.String("correlation", observation.correlation),
|
|
zap.String("event", string(observation.event)),
|
|
zap.String("operation", observation.operation),
|
|
zap.String("outcome", observation.outcome),
|
|
zap.String("error_code", observation.errorCode),
|
|
zap.Int64("duration_ms", observation.durationMS),
|
|
zap.Bool("truncated", observation.truncated),
|
|
zap.Int32("process_count", observation.processCount),
|
|
zap.Int32("artifact_count", observation.artifactCount),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
var workspaceCorrelationFallback atomic.Uint64
|
|
|
|
func newWorkspaceCorrelation() string {
|
|
var value [12]byte
|
|
if _, err := rand.Read(value[:]); err == nil {
|
|
return "ws-" + hex.EncodeToString(value[:])
|
|
}
|
|
return "ws-fallback-" + formatWorkspaceFallback(workspaceCorrelationFallback.Add(1))
|
|
}
|
|
|
|
func formatWorkspaceFallback(value uint64) string {
|
|
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
if value == 0 {
|
|
return "0"
|
|
}
|
|
var encoded [13]byte
|
|
index := len(encoded)
|
|
for value > 0 {
|
|
index--
|
|
encoded[index] = alphabet[value%36]
|
|
value /= 36
|
|
}
|
|
return string(encoded[index:])
|
|
}
|
|
|
|
func workspaceOperationName(operation iop.WorkspaceOperation) string {
|
|
switch operation {
|
|
case iop.WorkspaceOperation_WORKSPACE_OPERATION_READ:
|
|
return "read"
|
|
case iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST:
|
|
return "list"
|
|
case iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE:
|
|
return "write"
|
|
case iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE:
|
|
return "delete"
|
|
case iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND:
|
|
return "command"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func workspaceOutcome(status iop.WorkspaceStatus) string {
|
|
switch status {
|
|
case iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS:
|
|
return "success"
|
|
case iop.WorkspaceStatus_WORKSPACE_STATUS_TIMEOUT:
|
|
return "timeout"
|
|
case iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED:
|
|
return "cancelled"
|
|
case iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED:
|
|
return "unsupported"
|
|
case iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR:
|
|
return "error"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func workspaceErrorCode(code iop.WorkspaceErrorCode) string {
|
|
switch code {
|
|
case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_UNSPECIFIED:
|
|
return "none"
|
|
case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY:
|
|
return "not_ready"
|
|
case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_UNSUPPORTED:
|
|
return "unsupported"
|
|
case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST:
|
|
return "invalid_request"
|
|
case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND:
|
|
return "not_found"
|
|
case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_TIMEOUT:
|
|
return "timeout"
|
|
case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_CANCELLED:
|
|
return "cancelled"
|
|
case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL:
|
|
return "internal"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func (r *Runtime) observeTool(correlation string, operation iop.WorkspaceOperation, result Result) {
|
|
if r == nil || r.observer == nil {
|
|
return
|
|
}
|
|
r.observer.Emit(workspaceObservation{
|
|
event: workspaceObservationTool, correlation: correlation,
|
|
operation: workspaceOperationName(operation), outcome: workspaceOutcome(result.Status),
|
|
errorCode: workspaceErrorCode(result.Code), durationMS: result.DurationMS, truncated: result.Truncated,
|
|
})
|
|
}
|
|
|
|
func (r *Runtime) observeCleanup(req *Request, result CleanupResult, durationMS int64) {
|
|
if r == nil || r.observer == nil {
|
|
return
|
|
}
|
|
correlation := ""
|
|
if req != nil {
|
|
correlation = req.correlation
|
|
}
|
|
r.observer.Emit(workspaceObservation{
|
|
event: workspaceObservationCleanup, correlation: correlation, operation: "cleanup",
|
|
outcome: workspaceOutcome(result.Status), errorCode: workspaceErrorCode(result.Code), durationMS: durationMS,
|
|
processCount: result.CleanedProcesses, artifactCount: result.CleanedArtifacts,
|
|
})
|
|
}
|