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
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 }
|
|
|