Files
graph/x/log_writer.go
zeekay f5aaeb712e Hanzo Graph — the engine, on zapdb, ZAP-native
Derived from Dgraph (github.com/dgraph-io/dgraph) at commit 64804bd2, Apache-2.0.
See NOTICE.

WHAT THIS IS. The graph engine as a Go library: posting lists, the DQL parser,
the type and tokenizer system, the index and schema layers, and the wire types.
39,170 lines of the upstream's 166,337.

THE STORE is github.com/luxfi/zapdb, a Badger fork that kept the package name,
the subpackages (options, pb, y) and the API — Open, OpenManaged, DB, Options —
so the 74 import sites are aliased rather than rewritten.

THE CODEC is ZAP. zapdb serializes with ZAP, not protobuf: its pb package builds
the ZAP types by default and the protobuf ones only under `-tags grpc`, a path
that does not compile (table/builder.go calls pb.Marshal, which that tag
removes). One call site in posting needed converting; every other type here is
Hanzo's own protobuf and is untouched.

NO TRANSPORT. An embedded engine dials nothing, so nothing here imports
google.golang.org/grpc. Removed: 20 transport functions in x, the generated
service stubs for Raft, Zero and Worker (1,657 lines), and the JWT helper —
identity belongs to IAM. The core called none of them.

NOT KEPT: worker, conn, raftwal, the cluster commands, the GraphQL layer and the
test harnesses. `query`, the DQL execution engine, went with them because it
depends on worker; parsing DQL is available here, executing it is not.

The history of the upstream project lives at github.com/dgraph-io/dgraph. This
repository begins at the derivation because three quarters of that history
describes code this fork does not carry.
2026-08-20 21:26:27 -07:00

442 lines
9.7 KiB
Go

/*
* SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc.
* SPDX-License-Identifier: Apache-2.0
*/
package x
import (
"bufio"
"compress/gzip"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/golang/glog"
"github.com/luxfi/zapdb/y"
"github.com/dgraph-io/ristretto/v2/z"
)
const (
backupTimeFormat = "2006-01-02T15-04-05.000"
bufferSize = 256 * 1024
flushInterval = 10 * time.Second
// old logs before https://github.com/dgraph-io/dgraph/pull/8323 contain deprecated verification text in header
VerificationTextDeprecated = "Hello World"
VerificationText = "dlroW olloH"
)
// This is done to ensure LogWriter always implement io.WriterCloser
var _ io.WriteCloser = (*LogWriter)(nil)
type LogWriter struct {
FilePath string
MaxSize int64
MaxAge int64 // number of days
Compress bool
EncryptionKey []byte
mu sync.Mutex
size int64
file *os.File
writer *bufio.Writer
closer *z.Closer
// To manage order of cleaning old logs files
manageChannel chan bool
}
func (l *LogWriter) Init() (*LogWriter, error) {
if l == nil {
return nil, nil
}
l.manageOldLogs()
if err := l.open(); err != nil {
return nil, fmt.Errorf("not able to create new file %v", err)
}
l.closer = z.NewCloser(2)
l.manageChannel = make(chan bool, 1)
go func() {
defer l.closer.Done()
for {
select {
case <-l.manageChannel:
l.manageOldLogs()
case <-l.closer.HasBeenClosed():
return
}
}
}()
go l.flushPeriodic()
return l, nil
}
func (l *LogWriter) Write(p []byte) (int, error) {
if l == nil {
return 0, nil
}
l.mu.Lock()
defer l.mu.Unlock()
if l.size+int64(len(p)) >= l.MaxSize*1024*1024 {
if err := l.rotate(); err != nil {
return 0, err
}
}
// if encryption is enabled store the data in encrypted way
// encrypted writes will be preceded by the following header
// #################################################################
// ##### [16]byte iv + [4]byte uint32(len(p)) + [:]byte p #####
// #################################################################
if l.EncryptionKey != nil {
iv := make([]byte, 16)
if _, err := rand.Read(iv); err != nil {
return 0, err
}
lengthHeader := make([]byte, 4)
binary.BigEndian.PutUint32(lengthHeader, uint32(len(p)))
cipherText, err := encrypt(l.EncryptionKey, iv, p)
if err != nil {
return 0, err
}
allocation := append(append(iv, lengthHeader...), cipherText...)
n, err := l.writer.Write(allocation)
l.size = l.size + int64(n)
return n, err
}
n, err := l.writer.Write(p)
l.size = l.size + int64(n)
return n, err
}
func (l *LogWriter) Close() error {
if l == nil {
return nil
}
// close all go routines first before acquiring the lock to avoid contention
l.closer.SignalAndWait()
l.mu.Lock()
defer l.mu.Unlock()
if l.file == nil {
return nil
}
l.flush()
close(l.manageChannel)
_ = l.file.Close()
l.writer = nil
l.file = nil
return nil
}
// flushPeriodic periodically flushes the log file buffers.
func (l *LogWriter) flushPeriodic() {
if l == nil {
return
}
ticker := time.Tick(flushInterval)
defer l.closer.Done()
for {
select {
case <-ticker:
l.mu.Lock()
l.flush()
l.mu.Unlock()
case <-l.closer.HasBeenClosed():
return
}
}
}
// LogWriter should be locked while calling this
func (l *LogWriter) flush() {
if l == nil {
return
}
_ = l.writer.Flush()
_ = l.file.Sync()
}
func encrypt(key, iv, src []byte) ([]byte, error) {
ivCopy := make([]byte, 16)
copy(ivCopy, iv[:])
cipher, err := y.XORBlockAllocate(src, key, ivCopy)
if err != nil {
return nil, err
}
return cipher, nil
}
// used to verify client has correct key and can decrypt audit log header
func decrypt(key, iv, src []byte) ([]byte, error) {
ivCopy := make([]byte, 16)
copy(ivCopy, iv[:]) // todo: do we need to copy here?
plainText, err := y.XORBlockAllocate(src, key, ivCopy)
if err != nil {
return nil, err
}
return plainText, nil
}
func (l *LogWriter) rotate() error {
if l == nil {
return nil
}
l.flush()
if err := l.file.Close(); err != nil {
return err
}
if _, err := os.Stat(l.FilePath); err == nil {
// move the existing file
newname := backupName(l.FilePath)
if err := os.Rename(l.FilePath, newname); err != nil {
return fmt.Errorf("can't rename log file: %s", err)
}
}
l.manageChannel <- true
return l.open()
}
func (l *LogWriter) open() error {
if l == nil {
return nil
}
if err := os.MkdirAll(filepath.Dir(l.FilePath), 0755); err != nil {
return err
}
size := func() int64 {
info, err := os.Stat(l.FilePath)
if err != nil {
return 0
}
return info.Size()
}
openNew := func() error {
f, err := os.OpenFile(l.FilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
if err != nil {
return err
}
l.file = f
l.writer = bufio.NewWriterSize(l.file, bufferSize)
if l.EncryptionKey != nil {
iv := make([]byte, 16)
if _, err := rand.Read(iv); err != nil { // cve fix is here
return err
}
lengthInput := make([]byte, 4)
binary.BigEndian.PutUint32(lengthInput, uint32(len(VerificationText))) // header has 16+4 bytes now
bytes, err := encrypt(l.EncryptionKey, iv, []byte(VerificationText))
if err != nil {
return err
}
cipher := append(append(iv, lengthInput...), bytes...)
if _, err = l.writer.Write(cipher); err != nil {
return err
}
}
l.size = size()
return nil
}
info, err := os.Stat(l.FilePath)
if err != nil { // if any error try to open new log file itself
return openNew()
}
// encryption is enabled and file is corrupted as not able to read the IV
if l.EncryptionKey != nil && info.Size() < 12 {
return openNew()
}
f, err := os.OpenFile(l.FilePath, os.O_APPEND|os.O_RDWR, os.ModePerm)
if err != nil {
return openNew()
}
l.file = f
if l.EncryptionKey != nil {
// initialize byte slice for iv
iv := make([]byte, 16)
// If not able to read the iv, then this file might be corrupted.
// open the new file in that case
if _, err = l.file.ReadAt(iv, 0); err != nil {
_ = l.file.Close()
return openNew()
}
ct := make([]byte, len(VerificationText)) // size=11
// veritification text starts at offset 20
if _, err := f.ReadAt(ct, 20); err != nil {
_ = f.Close()
return openNew()
}
t, err := decrypt(l.EncryptionKey, iv, ct)
if err != nil || string(t) != VerificationText {
// different encryption key. Better to open new file here
_ = f.Close()
return openNew()
}
}
l.writer = bufio.NewWriterSize(l.file, bufferSize)
l.size = size()
return nil
}
func backupName(name string) string {
dir := filepath.Dir(name)
prefix, ext := prefixAndExt(name)
timestamp := time.Now().UTC().Format(backupTimeFormat)
return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext))
}
func compress(src string) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
glog.Warningf("error while closing fd: %v", err)
}
}()
gzf, err := os.OpenFile(src+".gz", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm)
if err != nil {
return err
}
defer func() {
if err := gzf.Close(); err != nil {
glog.Warningf("error closing file: %v", err)
}
}()
gz := gzip.NewWriter(gzf)
defer func() {
if err := gz.Close(); err != nil {
glog.Warningf("error closing gzip writer: %v", err)
}
}()
if _, err := io.Copy(gz, f); err != nil {
if err := os.Remove(src + ".gz"); err != nil {
glog.Warningf("error deleting file [%v]: %v", src, err)
}
return err
}
// close the descriptors because we need to delete the file
if err := f.Close(); err != nil {
return err
}
return os.Remove(src)
}
// this should be called in a serial order
func (l *LogWriter) manageOldLogs() {
if l == nil {
return
}
toRemove, toKeep, err := processOldLogFiles(l.FilePath, l.MaxAge)
if err != nil {
return
}
for _, f := range toRemove {
errRemove := os.Remove(filepath.Join(filepath.Dir(l.FilePath), f))
if err == nil && errRemove != nil {
err = errRemove
}
}
// if compression enabled do compress
if l.Compress {
for _, f := range toKeep {
// already compressed no need
if strings.HasSuffix(f, ".gz") {
continue
}
fn := filepath.Join(filepath.Dir(l.FilePath), f)
errCompress := compress(fn)
if err == nil && errCompress != nil {
err = errCompress
}
}
}
if err != nil {
fmt.Printf("error while managing old log files %+v\n", err)
}
}
// prefixAndExt extracts the filename and extension from a filepath.
// eg. prefixAndExt("/home/foo/file.ext") would return ("file", ".ext").
func prefixAndExt(file string) (prefix, ext string) {
filename := filepath.Base(file)
ext = filepath.Ext(filename)
prefix = filename[:len(filename)-len(ext)]
return prefix, ext
}
func processOldLogFiles(fp string, maxAge int64) ([]string, []string, error) {
dir := filepath.Dir(fp)
files, err := os.ReadDir(dir)
if err != nil {
return nil, nil, fmt.Errorf("can't read log file directory: %s", err)
}
defPrefix, defExt := prefixAndExt(fp)
// check only for old files. Those files have - before the time
defPrefix = defPrefix + "-"
toRemove := make([]string, 0)
toKeep := make([]string, 0)
diff := 24 * time.Hour * time.Duration(maxAge)
cutoff := time.Now().Add(-diff)
for _, f := range files {
if f.IsDir() || // f is directory
!strings.HasPrefix(f.Name(), defPrefix) || // f doesn't start with prefix
!(strings.HasSuffix(f.Name(), defExt) || strings.HasSuffix(f.Name(), defExt+".gz")) {
continue
}
_, e := prefixAndExt(fp)
tsString := f.Name()[len(defPrefix) : len(f.Name())-len(e)]
ts, err := time.Parse(backupTimeFormat, tsString)
if err != nil {
continue
}
if ts.Before(cutoff) {
toRemove = append(toRemove, f.Name())
} else {
toKeep = append(toKeep, f.Name())
}
}
return toRemove, toKeep, nil
}