// Package logservice exposes the narrow, group-restricted tera-logd protocol. package logservice import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "io" "net" "time" "tera-clone/services/tera-api/internal/logstorage" ) const ( DefaultSocketPath = "/run/tera-logd/log-store.sock" maxRequestBytes = 96 << 10 maxResponseBytes = 40 << 20 pageSize = 500 defaultOperationTimeout = 30 * time.Second ) var ErrUnavailable = errors.New("tera-logd unavailable") type UnavailableError struct{ Reason string } func (e *UnavailableError) Error() string { if e == nil || e.Reason == "" { return ErrUnavailable.Error() } return ErrUnavailable.Error() + ": " + e.Reason } func (e *UnavailableError) Unwrap() error { return ErrUnavailable } // Journal is the API-side persistence boundary. Implementations must return // only verified records and must not silently fall back to rootfs storage. type Journal interface { Append(class string, timestamp time.Time, value any) error Read(class string) ([]json.RawMessage, error) ApplyRetention(logstorage.Retention) error Snapshot() (logstorage.Snapshot, error) } type PortLogCleaner interface { ClearPort(channel string) (int, error) PortClearRevision() (uint64, error) } type SystemLogCleaner interface { ClearSystem() (int, error) SystemClearRevision() (uint64, error) } // SequencedJournal exposes the authoritative envelope sequence for consumers // that maintain a bounded in-memory view and only need records appended since // their last refresh. type SequencedJournal interface { Journal AppendSequence(class string, timestamp time.Time, value any) (uint64, error) ReadAfter(class string, afterSequence uint64) ([]SequencedPayload, error) } type SequencedPayload struct { Sequence uint64 Payload json.RawMessage } type command struct { Action string `json:"action"` Class string `json:"class,omitempty"` Timestamp time.Time `json:"timestamp,omitempty"` Payload json.RawMessage `json:"payload,omitempty"` AfterSequence uint64 `json:"after_sequence,omitempty"` Limit int `json:"limit,omitempty"` Retention logstorage.Retention `json:"retention,omitempty"` Channel string `json:"channel,omitempty"` } type response struct { Records []logstorage.Envelope `json:"records,omitempty"` Snapshot *logstorage.Snapshot `json:"snapshot,omitempty"` Code string `json:"code,omitempty"` Error string `json:"error,omitempty"` Deleted int `json:"deleted"` PortClearRevision uint64 `json:"port_clear_revision"` SystemClearRevision uint64 `json:"system_clear_revision"` } type Server struct{ Store *logstorage.Store } func (s Server) Serve(listener net.Listener) error { if s.Store == nil { return errors.New("log storage is required") } for { connection, err := listener.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { return nil } if temporary, ok := err.(interface{ Temporary() bool }); ok && temporary.Temporary() { continue } return err } go s.handle(connection) } } func (s Server) handle(connection net.Conn) { defer connection.Close() _ = connection.SetDeadline(time.Now().Add(defaultOperationTimeout)) payload, err := bufio.NewReader(io.LimitReader(connection, maxRequestBytes+1)).ReadBytes('\n') if err != nil && !errors.Is(err, io.EOF) { s.write(connection, response{Code: "validation_failed", Error: "invalid log service request"}) return } if len(payload) == 0 || len(payload) > maxRequestBytes || payload[len(payload)-1] != '\n' { s.write(connection, response{Code: "validation_failed", Error: "invalid log service request"}) return } decoder := json.NewDecoder(bytes.NewReader(payload)) decoder.DisallowUnknownFields() var value command if err := decoder.Decode(&value); err != nil { s.write(connection, response{Code: "validation_failed", Error: "invalid log service request"}) return } var extra any if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) { s.write(connection, response{Code: "validation_failed", Error: "log service request must contain one JSON value"}) return } switch value.Action { case "clear_system": if value.Class != logstorage.ClassSystem || value.Channel != "" { err = errors.New("only system events may be cleared; audit logs are preserved") break } var deleted int deleted, err = s.Store.ClearSystem() if err == nil { s.write(connection, response{Deleted: deleted}) return } case "system_clear_revision": s.write(connection, response{SystemClearRevision: s.Store.SystemClearRevision()}) return case "clear_port": if value.Class != logstorage.ClassPort { err = errors.New("only port communication logs may be cleared") break } var deleted int deleted, err = s.Store.ClearPort(value.Channel) if err == nil { s.write(connection, response{Deleted: deleted}) return } case "port_clear_revision": s.write(connection, response{PortClearRevision: s.Store.PortClearRevision()}) return case "append": if len(value.Payload) == 0 { err = errors.New("append payload is required") } else { var record logstorage.Envelope record, err = s.Store.Append(value.Class, value.Timestamp, value.Payload) if err == nil { s.write(connection, response{Records: []logstorage.Envelope{record}}) return } } case "read": if value.Limit == 0 { value.Limit = pageSize } var records []logstorage.Envelope records, err = s.Store.Page(value.Class, value.AfterSequence, value.Limit) if err == nil { s.write(connection, response{Records: records}) return } case "retention": err = s.Store.ApplyRetention(value.Retention) case "snapshot": var snapshot logstorage.Snapshot snapshot, err = s.Store.Snapshot() if err == nil { s.write(connection, response{Snapshot: &snapshot}) return } case "rebuild_index": err = s.Store.RebuildIndex() default: err = errors.New("log service action is not supported") } if err != nil { code := "storage_failed" if errors.Is(err, logstorage.ErrCapacityExhausted) { code = "capacity_exhausted" } else if stringsHasValidationMeaning(err.Error()) { code = "validation_failed" } s.write(connection, response{Code: code, Error: err.Error()}) return } s.write(connection, response{}) } func stringsHasValidationMeaning(message string) bool { return bytes.Contains([]byte(message), []byte("must be")) || bytes.Contains([]byte(message), []byte("required")) || bytes.Contains([]byte(message), []byte("not supported")) } func (Server) write(connection net.Conn, value response) { _ = json.NewEncoder(connection).Encode(value) } type Client struct { path string timeout time.Duration } func NewClient(path string) *Client { if path == "" { path = DefaultSocketPath } return &Client{path: path, timeout: defaultOperationTimeout} } func (c *Client) Append(class string, timestamp time.Time, value any) error { _, err := c.AppendSequence(class, timestamp, value) return err } func (c *Client) AppendSequence(class string, timestamp time.Time, value any) (uint64, error) { payload, err := json.Marshal(value) if err != nil { return 0, err } result, err := c.call(command{Action: "append", Class: class, Timestamp: timestamp, Payload: payload}) if err != nil { return 0, err } if len(result.Records) != 1 || result.Records[0].Sequence == 0 { return 0, errors.New("tera-logd returned no append sequence") } return result.Records[0].Sequence, nil } func (c *Client) Read(class string) ([]json.RawMessage, error) { records, err := c.ReadAfter(class, 0) if err != nil { return nil, err } result := make([]json.RawMessage, 0, len(records)) for _, record := range records { result = append(result, append(json.RawMessage(nil), record.Payload...)) } return result, nil } func (c *Client) ReadAfter(class string, afterSequence uint64) ([]SequencedPayload, error) { result := make([]SequencedPayload, 0) after := afterSequence for { value, err := c.call(command{Action: "read", Class: class, AfterSequence: after, Limit: pageSize}) if err != nil { return nil, err } for _, record := range value.Records { if record.Sequence <= after { return nil, errors.New("tera-logd returned a non-increasing sequence") } after = record.Sequence result = append(result, SequencedPayload{Sequence: record.Sequence, Payload: append(json.RawMessage(nil), record.Payload...)}) } if len(value.Records) < pageSize { return result, nil } } } func (c *Client) ApplyRetention(retention logstorage.Retention) error { _, err := c.call(command{Action: "retention", Retention: retention}) return err } func (c *Client) Snapshot() (logstorage.Snapshot, error) { value, err := c.call(command{Action: "snapshot"}) if err != nil { return logstorage.Snapshot{}, err } if value.Snapshot == nil { return logstorage.Snapshot{}, errors.New("tera-logd returned no storage snapshot") } return *value.Snapshot, nil } func (c *Client) ClearPort(channel string) (int, error) { value, err := c.call(command{Action: "clear_port", Class: logstorage.ClassPort, Channel: channel}) return value.Deleted, err } func (c *Client) ClearSystem() (int, error) { value, err := c.call(command{Action: "clear_system", Class: logstorage.ClassSystem}) return value.Deleted, err } func (c *Client) SystemClearRevision() (uint64, error) { value, err := c.call(command{Action: "system_clear_revision"}) return value.SystemClearRevision, err } func (c *Client) PortClearRevision() (uint64, error) { value, err := c.call(command{Action: "port_clear_revision"}) return value.PortClearRevision, err } func (c *Client) call(command command) (response, error) { dialer := net.Dialer{Timeout: c.timeout} connection, err := dialer.Dial("unix", c.path) if err != nil { return response{}, &UnavailableError{Reason: "local socket is not reachable"} } defer connection.Close() _ = connection.SetDeadline(time.Now().Add(c.timeout)) if err := json.NewEncoder(connection).Encode(command); err != nil { return response{}, &UnavailableError{Reason: "request could not be sent"} } var value response decoder := json.NewDecoder(io.LimitReader(connection, maxResponseBytes+1)) if err := decoder.Decode(&value); err != nil { return response{}, &UnavailableError{Reason: "service returned no valid response"} } if value.Error != "" { return response{}, fmt.Errorf("tera-logd %s: %s", value.Code, value.Error) } return value, nil }