Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
353 lines
13 KiB
Go
353 lines
13 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func init() {
|
|
cmdFilerMetaScan.Run = runFilerMetaScan // break init cycle
|
|
}
|
|
|
|
var cmdFilerMetaScan = &Command{
|
|
UsageLine: "filer.meta.scan -pathPrefix=/some/dir [-since=...] [-until=...]",
|
|
Short: "audit what happened under a directory, one line per change",
|
|
Long: `Replay the filer's metadata log for one directory and print one line per change.
|
|
|
|
Unlike filer.meta.tail this stops when it reaches the end of the requested
|
|
range instead of following, so it can be piped straight into grep or awk.
|
|
|
|
Each line is:
|
|
|
|
<time> <OP> <path> <details>
|
|
|
|
OP is CREATE, DELETE, UPDATE or RENAME. Times are printed with the offset of
|
|
the machine running the command.
|
|
|
|
Ranges may be given as absolute timestamps or as durations before now:
|
|
|
|
weed filer.meta.scan -pathPrefix=/buckets/b -since="2026-08-03 00:00:00" -until="2026-08-03 06:00:00"
|
|
weed filer.meta.scan -pathPrefix=/buckets/b -timeAgo=48h -untilTimeAgo=24h
|
|
weed filer.meta.scan -pathPrefix=/buckets/b -timeAgo=1h -follow
|
|
|
|
-since and -until are read in the machine's timezone unless -tz is given:
|
|
|
|
weed filer.meta.scan -pathPrefix=/buckets/b -since="2026-08-03 01:00:00" -tz=America/Sao_Paulo
|
|
|
|
On a versioned bucket an object is stored as <key>.versions/v_<versionId>.
|
|
Those are reported against <key> with the version id in the details, so
|
|
-name matches the key a client would ask for rather than the internal path:
|
|
|
|
weed filer.meta.scan -pathPrefix=/buckets/b -timeAgo=720h -name=summary.xml
|
|
weed filer.meta.scan -pathPrefix=/buckets/b -timeAgo=720h -op=DELETE
|
|
|
|
Pass -raw to report the stored paths instead.
|
|
|
|
Persisted ranges are read straight from the volume servers, so the filer
|
|
hands out log chunk ids instead of decoding and filtering the whole range
|
|
itself — worth having on a busy cluster, where that decode is the expensive
|
|
part and is charged to the filer no matter how narrow the prefix is. It
|
|
needs a route to the volume servers; without one the scan falls back to
|
|
reading through the filer, and -directRead=false forces that path.
|
|
|
|
`,
|
|
}
|
|
|
|
var (
|
|
scanFiler = cmdFilerMetaScan.Flag.String("filer", "localhost:8888", "filer hostname:port")
|
|
scanPathPrefix = cmdFilerMetaScan.Flag.String("pathPrefix", "/", "directory to audit; filtered on the filer, so keep it as tight as possible")
|
|
scanSince = cmdFilerMetaScan.Flag.String("since", "", "start time, \"2006-01-02 15:04:05\" or RFC3339")
|
|
scanUntil = cmdFilerMetaScan.Flag.String("until", "", "stop time, same formats as -since; defaults to now")
|
|
scanTimeAgo = cmdFilerMetaScan.Flag.Duration("timeAgo", 0, "start this long before now, e.g. \"48h\"; ignored when -since is set")
|
|
scanUntilTimeAgo = cmdFilerMetaScan.Flag.Duration("untilTimeAgo", 0, "stop this long before now; ignored when -until is set")
|
|
scanTz = cmdFilerMetaScan.Flag.String("tz", "", "timezone for -since/-until, e.g. \"America/Sao_Paulo\" or \"UTC\"; defaults to this machine's")
|
|
scanName = cmdFilerMetaScan.Flag.String("name", "", "only report paths containing this text, case-insensitive")
|
|
scanOp = cmdFilerMetaScan.Flag.String("op", "", "only report these operations, comma-separated: CREATE,DELETE,UPDATE,RENAME")
|
|
scanRaw = cmdFilerMetaScan.Flag.Bool("raw", false, "report stored paths instead of resolving .versions/v_<id> back to the object key")
|
|
scanFollow = cmdFilerMetaScan.Flag.Bool("follow", false, "keep following after reaching the end of the range")
|
|
scanDirectRead = cmdFilerMetaScan.Flag.Bool("directRead", true, "read log chunks straight from the volume servers, so the filer does not decode the range; needs volume server access, falls back automatically")
|
|
)
|
|
|
|
// scanFilerClient adapts a filer address to filer_pb.FilerClient so the chunk
|
|
// reader can resolve a log chunk's volume locations.
|
|
type scanFilerClient struct {
|
|
address pb.ServerAddress
|
|
grpcDialOption grpc.DialOption
|
|
signature int32
|
|
}
|
|
|
|
func (c *scanFilerClient) WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error {
|
|
return pb.WithFilerClient(streamingMode, c.signature, c.address, c.grpcDialOption, fn)
|
|
}
|
|
|
|
func (c *scanFilerClient) AdjustedUrl(location *filer_pb.Location) string { return location.Url }
|
|
|
|
func (c *scanFilerClient) GetDataCenter() string { return "" }
|
|
|
|
const scanTimeLayout = "2006-01-02 15:04:05"
|
|
|
|
// parseScanTime accepts either the human layout or RFC3339. Without an explicit
|
|
// zone in the string, loc decides — which is the difference between finding the
|
|
// window and missing it by the UTC offset.
|
|
func parseScanTime(value string, loc *time.Location) (time.Time, error) {
|
|
if t, err := time.Parse(time.RFC3339, value); err == nil {
|
|
return t, nil
|
|
}
|
|
t, err := time.ParseInLocation(scanTimeLayout, value, loc)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("parse %q: want %q or RFC3339", value, scanTimeLayout)
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// scanEvent is one metadata change reduced to what a path audit needs.
|
|
type scanEvent struct {
|
|
tsNs int64
|
|
op string
|
|
path string
|
|
details string
|
|
}
|
|
|
|
const versionsSuffix = ".versions"
|
|
|
|
// logicalPath maps a stored path back to the object key a client would use. A
|
|
// versioned object lives at <key>.versions/v_<id>, so both the container and the
|
|
// version files are reported against <key>; the version id moves to the details.
|
|
// kind distinguishes the container from the versions inside it, since a change to
|
|
// the container is a pointer flip rather than a write of object data.
|
|
func logicalPath(dir, name string) (path, version, kind string) {
|
|
full := dir + "/" + name
|
|
if *scanRaw {
|
|
return full, "", ""
|
|
}
|
|
if strings.HasPrefix(name, "v_") && strings.HasSuffix(dir, versionsSuffix) {
|
|
return strings.TrimSuffix(dir, versionsSuffix), strings.TrimPrefix(name, "v_"), ""
|
|
}
|
|
if strings.HasSuffix(name, versionsSuffix) {
|
|
return strings.TrimSuffix(full, versionsSuffix), "", "versions-container"
|
|
}
|
|
return full, "", ""
|
|
}
|
|
|
|
func describeEntry(entry *filer_pb.Entry, version, kind string) string {
|
|
var parts []string
|
|
if version != "" {
|
|
parts = append(parts, "version="+version)
|
|
}
|
|
if kind != "" {
|
|
parts = append(parts, kind)
|
|
}
|
|
if entry != nil {
|
|
// A retraction is stored as a zero-length version carrying this flag.
|
|
// Reporting it as an ordinary write would read as "the object was
|
|
// written" when it means the opposite.
|
|
if marker, ok := entry.Extended[s3_constants.ExtDeleteMarkerKey]; ok && string(marker) == "true" {
|
|
parts = append(parts, "delete-marker")
|
|
} else if entry.IsDirectory {
|
|
if kind == "" {
|
|
parts = append(parts, "dir")
|
|
}
|
|
} else {
|
|
parts = append(parts, fmt.Sprintf("size=%d", filer.FileSize(entry)))
|
|
if n := len(entry.GetChunks()); n > 0 {
|
|
parts = append(parts, fmt.Sprintf("chunks=%d", n))
|
|
}
|
|
}
|
|
}
|
|
if len(parts) == 0 {
|
|
return "-"
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
// toScanEvent classifies one notification. Which of old/new is present is what
|
|
// distinguishes the operations; when both are, the path tells a rename from an
|
|
// in-place update.
|
|
func toScanEvent(resp *filer_pb.SubscribeMetadataResponse) *scanEvent {
|
|
n := resp.EventNotification
|
|
switch {
|
|
case n.OldEntry == nil && n.NewEntry == nil:
|
|
return nil
|
|
|
|
case n.OldEntry == nil:
|
|
path, version, kind := logicalPath(n.NewParentPath, n.NewEntry.Name)
|
|
return &scanEvent{resp.TsNs, "CREATE", path, describeEntry(n.NewEntry, version, kind)}
|
|
|
|
case n.NewEntry == nil:
|
|
path, version, kind := logicalPath(resp.Directory, n.OldEntry.Name)
|
|
return &scanEvent{resp.TsNs, "DELETE", path, describeEntry(n.OldEntry, version, kind)}
|
|
|
|
default:
|
|
oldPath, oldVersion, _ := logicalPath(resp.Directory, n.OldEntry.Name)
|
|
newPath, newVersion, newKind := logicalPath(n.NewParentPath, n.NewEntry.Name)
|
|
if oldPath == newPath && oldVersion == newVersion {
|
|
return &scanEvent{resp.TsNs, "UPDATE", newPath, describeEntry(n.NewEntry, newVersion, newKind)}
|
|
}
|
|
details := describeEntry(n.NewEntry, newVersion, newKind) + " from=" + oldPath
|
|
if oldVersion != "" {
|
|
details += "#" + oldVersion
|
|
}
|
|
return &scanEvent{resp.TsNs, "RENAME", newPath, details}
|
|
}
|
|
}
|
|
|
|
func runFilerMetaScan(cmd *Command, args []string) bool {
|
|
|
|
loc := time.Local
|
|
if *scanTz != "" {
|
|
parsed, err := time.LoadLocation(*scanTz)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "-tz: %v\n", err)
|
|
return false
|
|
}
|
|
loc = parsed
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
var startTs time.Time
|
|
switch {
|
|
case *scanSince != "":
|
|
parsed, err := parseScanTime(*scanSince, loc)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "-since: %v\n", err)
|
|
return false
|
|
}
|
|
startTs = parsed
|
|
case *scanTimeAgo > 0:
|
|
startTs = now.Add(-*scanTimeAgo)
|
|
default:
|
|
fmt.Fprintln(os.Stderr, "need a start: pass -since or -timeAgo")
|
|
return false
|
|
}
|
|
|
|
// Default the end to now so the command terminates; following is opt-in.
|
|
stopTs := now
|
|
switch {
|
|
case *scanUntil != "":
|
|
parsed, err := parseScanTime(*scanUntil, loc)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "-until: %v\n", err)
|
|
return false
|
|
}
|
|
stopTs = parsed
|
|
case *scanUntilTimeAgo > 0:
|
|
stopTs = now.Add(-*scanUntilTimeAgo)
|
|
}
|
|
|
|
if !stopTs.After(startTs) && !*scanFollow {
|
|
fmt.Fprintf(os.Stderr, "empty range: start %s is not before stop %s\n",
|
|
startTs.Format(time.RFC3339), stopTs.Format(time.RFC3339))
|
|
return false
|
|
}
|
|
|
|
wantOps := map[string]bool{}
|
|
for _, op := range strings.Split(*scanOp, ",") {
|
|
if op = strings.ToUpper(strings.TrimSpace(op)); op != "" {
|
|
wantOps[op] = true
|
|
}
|
|
}
|
|
nameFilter := strings.ToLower(*scanName)
|
|
|
|
var stopTsNs int64
|
|
if !*scanFollow {
|
|
stopTsNs = stopTs.UnixNano()
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "scanning %s from %s to %s\n", *scanPathPrefix,
|
|
startTs.Format(time.RFC3339), stopTs.Format(time.RFC3339))
|
|
|
|
util.LoadSecurityConfiguration()
|
|
grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client")
|
|
|
|
filerAddress := pb.ServerAddress(*scanFiler)
|
|
matched := 0
|
|
|
|
emit := func(resp *filer_pb.SubscribeMetadataResponse) error {
|
|
if filer_pb.IsEmpty(resp) {
|
|
return nil
|
|
}
|
|
event := toScanEvent(resp)
|
|
if event == nil {
|
|
return nil
|
|
}
|
|
if len(wantOps) > 0 && !wantOps[event.op] {
|
|
return nil
|
|
}
|
|
if nameFilter != "" && !strings.Contains(strings.ToLower(event.path), nameFilter) {
|
|
return nil
|
|
}
|
|
matched++
|
|
fmt.Printf("%s\t%s\t%s\t%s\n",
|
|
time.Unix(0, event.tsNs).Format(time.RFC3339), event.op, event.path, event.details)
|
|
return nil
|
|
}
|
|
|
|
scan := func(directRead bool) error {
|
|
option := &pb.MetadataFollowOption{
|
|
ClientName: "scan",
|
|
ClientId: util.RandomInt32(),
|
|
ClientEpoch: 0,
|
|
SelfSignature: 0,
|
|
PathPrefix: *scanPathPrefix,
|
|
StartTsNs: startTs.UnixNano(),
|
|
StopTsNs: stopTsNs,
|
|
EventErrorType: pb.TrivialOnError,
|
|
}
|
|
if directRead {
|
|
// The server hands out log chunk fids and this reads them straight
|
|
// from the volume servers, so the filer never decodes the range.
|
|
// The prefix filter is re-applied client-side by ReadLogFileRefs,
|
|
// so the result is the same either way.
|
|
client := &scanFilerClient{address: filerAddress, grpcDialOption: grpcDialOption, signature: option.SelfSignature}
|
|
lookupFn := filer.LookupFn(client)
|
|
option.LogFileReaderFn = func(chunks []*filer_pb.FileChunk) (io.ReadCloser, error) {
|
|
return filer.NewChunkStreamReaderFromLookup(context.Background(), lookupFn, chunks), nil
|
|
}
|
|
}
|
|
return pb.FollowMetadata(filerAddress, grpcDialOption, option, emit)
|
|
}
|
|
|
|
followErr := scan(*scanDirectRead)
|
|
if *scanDirectRead && matched == 0 {
|
|
// Two ways direct read can come back with nothing: it failed outright
|
|
// (reading chunks needs a route to the volume servers that the filer
|
|
// does not), or it succeeded and found none. The second is the
|
|
// dangerous one — for an audit an empty answer reads as "nothing
|
|
// happened here", so it has to be confirmed rather than trusted.
|
|
// Re-running is safe precisely because nothing was printed; after
|
|
// partial output a replay would duplicate lines instead.
|
|
if followErr != nil {
|
|
fmt.Fprintf(os.Stderr, "direct read failed (%v); retrying through the filer\n", followErr)
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, "direct read found nothing; confirming through the filer")
|
|
}
|
|
followErr = scan(false)
|
|
if followErr == nil && matched > 0 {
|
|
fmt.Fprintf(os.Stderr, "warning: direct read missed %d change(s) the filer returned; "+
|
|
"please report this, and pass -directRead=false meanwhile\n", matched)
|
|
}
|
|
}
|
|
|
|
if followErr != nil {
|
|
fmt.Fprintf(os.Stderr, "scan %s: %v\n", *scanFiler, followErr)
|
|
return false
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "%d matching change(s)\n", matched)
|
|
return true
|
|
}
|