You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

359 lines
11 KiB

// 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 FactoryResetter interface{ FactoryReset() 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 "factory_reset":
if value.Class != "" || value.Channel != "" || len(value.Payload) != 0 {
err = errors.New("factory reset accepts no selector or payload")
break
}
err = s.Store.FactoryReset()
if err == nil {
s.write(connection, response{})
return
}
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
}
func (c *Client) FactoryReset() error { _, err := c.call(command{Action: "factory_reset"}); return err }