Fork of Tangled for Furgit integration

knotserver/xrpc: Add detailed logging

This commit was primarily produced by a large language model. While it
has been tested and reviewed and no major flaws were discovered, a full
review has not been performed. Do not merge.

runxiyu.tngl.sh f0d2860e 38e0781a

verified
+339 -138
+21 -15
knotserver/xrpc/create_repo.go
··· 21 21 ) 22 22 23 23 func (h *Xrpc) CreateRepo(w http.ResponseWriter, r *http.Request) { 24 - l := h.Logger.With("handler", "NewRepo") 25 - fail := func(e xrpcerr.XrpcError) { 26 - l.Error("failed", "kind", e.Tag, "error", e.Message) 24 + log := h.newEndpointLog(r, "CreateRepo") 25 + fail := func(msg string, err error, e xrpcerr.XrpcError, attrs ...any) { 26 + if err == nil { 27 + err = errors.New(e.Message) 28 + } 29 + log.Error(msg, err, append(attrs, "kind", e.Tag)...) 27 30 writeError(w, e, http.StatusBadRequest) 28 31 } 29 32 30 33 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 31 34 if !ok { 32 - fail(xrpcerr.MissingActorDidError) 35 + fail("missing actor did", nil, xrpcerr.MissingActorDidError) 33 36 return 34 37 } 35 38 36 39 isMember, err := h.Enforcer.IsRepoCreateAllowed(actorDid.String(), rbac.ThisServer) 37 40 if err != nil { 38 - fail(xrpcerr.GenericError(err)) 41 + fail("repo create allowed check failed", err, xrpcerr.GenericError(err)) 39 42 return 40 43 } 41 44 if !isMember { 42 - fail(xrpcerr.AccessControlError(actorDid.String())) 45 + fail("insufficient permissions", fmt.Errorf("actor lacks permission"), xrpcerr.AccessControlError(actorDid.String())) 43 46 return 44 47 } 45 48 46 49 var data tangled.RepoCreate_Input 47 50 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 48 - fail(xrpcerr.GenericError(err)) 51 + fail("failed to decode request", err, xrpcerr.GenericError(err)) 49 52 return 50 53 } 51 54 52 55 rkey := data.Rkey 56 + log = log.With("actorDid", actorDid.String(), "rkey", rkey) 53 57 54 58 ident, err := h.Resolver.ResolveIdent(r.Context(), actorDid.String()) 55 59 if err != nil || ident.Handle.IsInvalidHandle() { 56 - fail(xrpcerr.GenericError(err)) 60 + fail("failed to resolve handle", err, xrpcerr.GenericError(err)) 57 61 return 58 62 } 59 63 ··· 63 67 64 68 resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) 65 69 if err != nil { 66 - fail(xrpcerr.GenericError(err)) 70 + fail("failed to fetch repo record", err, xrpcerr.GenericError(err)) 67 71 return 68 72 } 69 73 70 74 repo := resp.Value.Val.(*tangled.Repo) 75 + log = log.With("repo", repo.Name) 71 76 72 77 defaultBranch := h.Config.Repo.MainBranch 73 78 if data.DefaultBranch != nil && *data.DefaultBranch != "" { 74 79 defaultBranch = *data.DefaultBranch 75 80 } 81 + log = log.With("defaultBranch", defaultBranch) 76 82 77 83 if err := validateRepoName(repo.Name); err != nil { 78 - l.Error("creating repo", "error", err.Error()) 79 - fail(xrpcerr.GenericError(err)) 84 + fail("creating repo", err, xrpcerr.GenericError(err)) 80 85 return 81 86 } 82 87 ··· 86 91 if data.Source != nil && *data.Source != "" { 87 92 err = git.Fork(repoPath, *data.Source) 88 93 if err != nil { 89 - l.Error("forking repo", "error", err.Error()) 94 + log.Error("forking repo", err, "source", *data.Source) 90 95 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 91 96 return 92 97 } 93 98 } else { 94 99 err = git.InitBare(repoPath, defaultBranch) 95 100 if err != nil { 96 - l.Error("initializing bare repo", "error", err.Error()) 101 + log.Error("initializing bare repo", err) 97 102 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 98 - fail(xrpcerr.RepoExistsError("repository already exists")) 103 + fail("repository already exists", err, xrpcerr.RepoExistsError("repository already exists")) 99 104 return 100 105 } else { 101 106 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) ··· 107 112 // add perms for this user to access the repo 108 113 err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, relativeRepoPath) 109 114 if err != nil { 110 - l.Error("adding repo permissions", "error", err.Error()) 115 + log.Error("adding repo permissions", err) 111 116 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 112 117 return 113 118 } ··· 121 126 ) 122 127 123 128 w.WriteHeader(http.StatusOK) 129 + log.Success("repo created", "repoPath", relativeRepoPath, "sourceFork", data.Source != nil && *data.Source != "") 124 130 } 125 131 126 132 func validateRepoName(name string) error {
+19 -12
knotserver/xrpc/delete_branch.go
··· 2 2 3 3 import ( 4 4 "encoding/json" 5 + "errors" 5 6 "fmt" 6 7 "net/http" 7 8 ··· 17 18 ) 18 19 19 20 func (x *Xrpc) DeleteBranch(w http.ResponseWriter, r *http.Request) { 20 - l := x.Logger 21 - fail := func(e xrpcerr.XrpcError) { 22 - l.Error("failed", "kind", e.Tag, "error", e.Message) 21 + log := x.newEndpointLog(r, "DeleteBranch") 22 + fail := func(msg string, err error, e xrpcerr.XrpcError, attrs ...any) { 23 + if err == nil { 24 + err = errors.New(e.Message) 25 + } 26 + log.Error(msg, err, append(attrs, "kind", e.Tag)...) 23 27 writeError(w, e, http.StatusBadRequest) 24 28 } 25 29 26 30 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 27 31 if !ok { 28 - fail(xrpcerr.MissingActorDidError) 32 + fail("missing actor did", nil, xrpcerr.MissingActorDidError) 29 33 return 30 34 } 31 35 32 36 var data tangled.RepoDeleteBranch_Input 33 37 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 34 - fail(xrpcerr.GenericError(err)) 38 + fail("failed to decode request", err, xrpcerr.GenericError(err)) 35 39 return 36 40 } 41 + log = log.With("actorDid", actorDid.String(), "branch", data.Branch) 37 42 38 43 // unfortunately we have to resolve repo-at here 39 44 repoAt, err := syntax.ParseATURI(data.Repo) 40 45 if err != nil { 41 - fail(xrpcerr.InvalidRepoError(data.Repo)) 46 + fail("invalid repo at-uri", err, xrpcerr.InvalidRepoError(data.Repo)) 42 47 return 43 48 } 44 49 45 50 // resolve this aturi to extract the repo record 46 51 ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) 47 52 if err != nil || ident.Handle.IsInvalidHandle() { 48 - fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 53 + fail("failed to resolve handle", err, xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 49 54 return 50 55 } 51 56 52 57 xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} 53 58 resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 54 59 if err != nil { 55 - fail(xrpcerr.GenericError(err)) 60 + fail("failed to fetch repo record", err, xrpcerr.GenericError(err)) 56 61 return 57 62 } 58 63 59 64 repo := resp.Value.Val.(*tangled.Repo) 60 65 didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 61 66 if err != nil { 62 - fail(xrpcerr.GenericError(err)) 67 + fail("failed to build repo path", err, xrpcerr.GenericError(err)) 63 68 return 64 69 } 70 + log = log.With("repo", didPath) 65 71 66 72 if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 67 - l.Error("insufficent permissions", "did", actorDid.String(), "repo", didPath) 73 + log.Error("insufficent permissions", err, "did", actorDid.String()) 68 74 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 69 75 return 70 76 } ··· 72 78 path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 73 79 gr, err := git.PlainOpen(path) 74 80 if err != nil { 75 - fail(xrpcerr.GenericError(err)) 81 + fail("failed to open repository", err, xrpcerr.GenericError(err)) 76 82 return 77 83 } 78 84 79 85 err = gr.DeleteBranch(data.Branch) 80 86 if err != nil { 81 - l.Error("deleting branch", "error", err.Error(), "branch", data.Branch) 87 + log.Error("deleting branch", err, "branch", data.Branch) 82 88 writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError) 83 89 return 84 90 } 85 91 86 92 w.WriteHeader(http.StatusOK) 93 + log.Success("branch deleted") 87 94 }
+20 -13
knotserver/xrpc/delete_repo.go
··· 2 2 3 3 import ( 4 4 "encoding/json" 5 + "errors" 5 6 "fmt" 6 7 "net/http" 7 8 "os" ··· 17 18 ) 18 19 19 20 func (x *Xrpc) DeleteRepo(w http.ResponseWriter, r *http.Request) { 20 - l := x.Logger.With("handler", "DeleteRepo") 21 - fail := func(e xrpcerr.XrpcError) { 22 - l.Error("failed", "kind", e.Tag, "error", e.Message) 21 + log := x.newEndpointLog(r, "DeleteRepo") 22 + fail := func(msg string, err error, e xrpcerr.XrpcError, attrs ...any) { 23 + if err == nil { 24 + err = errors.New(e.Message) 25 + } 26 + log.Error(msg, err, append(attrs, "kind", e.Tag)...) 23 27 writeError(w, e, http.StatusBadRequest) 24 28 } 25 29 26 30 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 27 31 if !ok { 28 - fail(xrpcerr.MissingActorDidError) 32 + fail("missing actor did", nil, xrpcerr.MissingActorDidError) 29 33 return 30 34 } 31 35 32 36 var data tangled.RepoDelete_Input 33 37 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 34 - fail(xrpcerr.GenericError(err)) 38 + fail("failed to decode request", err, xrpcerr.GenericError(err)) 35 39 return 36 40 } 37 41 ··· 40 44 rkey := data.Rkey 41 45 42 46 if did == "" || name == "" { 43 - fail(xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 47 + fail("missing required fields", fmt.Errorf("did and name are required"), xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 44 48 return 45 49 } 50 + log = log.With("actorDid", actorDid.String(), "did", did, "name", name, "rkey", rkey) 46 51 47 52 ident, err := x.Resolver.ResolveIdent(r.Context(), actorDid.String()) 48 53 if err != nil || ident.Handle.IsInvalidHandle() { 49 - fail(xrpcerr.GenericError(err)) 54 + fail("failed to resolve handle", err, xrpcerr.GenericError(err)) 50 55 return 51 56 } 52 57 ··· 57 62 // ensure that the record does not exists 58 63 _, err = comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) 59 64 if err == nil { 60 - fail(xrpcerr.RecordExistsError(rkey)) 65 + fail("record still exists", fmt.Errorf("record %s exists", rkey), xrpcerr.RecordExistsError(rkey)) 61 66 return 62 67 } 63 68 64 69 relativeRepoPath := filepath.Join(did, name) 65 70 isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath) 66 71 if err != nil { 67 - fail(xrpcerr.GenericError(err)) 72 + fail("failed permission check", err, xrpcerr.GenericError(err)) 68 73 return 69 74 } 70 75 if !isDeleteAllowed { 71 - fail(xrpcerr.AccessControlError(actorDid.String())) 76 + fail("delete not allowed", fmt.Errorf("delete not allowed"), xrpcerr.AccessControlError(actorDid.String())) 72 77 return 73 78 } 79 + log = log.With("repo", relativeRepoPath) 74 80 75 81 repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 76 82 if err != nil { 77 - fail(xrpcerr.GenericError(err)) 83 + fail("failed to build repo path", err, xrpcerr.GenericError(err)) 78 84 return 79 85 } 80 86 81 87 err = os.RemoveAll(repoPath) 82 88 if err != nil { 83 - l.Error("deleting repo", "error", err.Error()) 89 + log.Error("deleting repo", err) 84 90 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 85 91 return 86 92 } 87 93 88 94 err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath) 89 95 if err != nil { 90 - l.Error("failed to delete repo from enforcer", "error", err.Error()) 96 + log.Error("failed to delete repo from enforcer", err) 91 97 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 92 98 return 93 99 } 94 100 95 101 w.WriteHeader(http.StatusOK) 102 + log.Success("repo deleted") 96 103 }
+22 -15
knotserver/xrpc/fork_status.go
··· 2 2 3 3 import ( 4 4 "encoding/json" 5 + "errors" 5 6 "fmt" 6 7 "net/http" 7 8 "path/filepath" ··· 16 17 ) 17 18 18 19 func (x *Xrpc) ForkStatus(w http.ResponseWriter, r *http.Request) { 19 - l := x.Logger.With("handler", "ForkStatus") 20 - fail := func(e xrpcerr.XrpcError) { 21 - l.Error("failed", "kind", e.Tag, "error", e.Message) 20 + log := x.newEndpointLog(r, "ForkStatus") 21 + fail := func(msg string, err error, e xrpcerr.XrpcError, attrs ...any) { 22 + if err == nil { 23 + err = errors.New(e.Message) 24 + } 25 + log.Error(msg, err, append(attrs, "kind", e.Tag)...) 22 26 writeError(w, e, http.StatusBadRequest) 23 27 } 24 28 25 29 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 26 30 if !ok { 27 - fail(xrpcerr.MissingActorDidError) 31 + fail("missing actor did", nil, xrpcerr.MissingActorDidError) 28 32 return 29 33 } 30 34 31 35 var data tangled.RepoForkStatus_Input 32 36 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 33 - fail(xrpcerr.GenericError(err)) 37 + fail("failed to decode request", err, xrpcerr.GenericError(err)) 34 38 return 35 39 } 36 40 ··· 40 44 hiddenRef := data.HiddenRef 41 45 42 46 if did == "" || source == "" || branch == "" || hiddenRef == "" { 43 - fail(xrpcerr.GenericError(fmt.Errorf("did, source, branch, and hiddenRef are required"))) 47 + fail("missing required fields", fmt.Errorf("did, source, branch, and hiddenRef are required"), xrpcerr.GenericError(fmt.Errorf("did, source, branch, and hiddenRef are required"))) 44 48 return 45 49 } 50 + log = log.With("actorDid", actorDid.String(), "did", did, "source", source, "branch", branch, "hiddenRef", hiddenRef) 46 51 47 52 var name string 48 53 if data.Name != "" { ··· 52 57 } 53 58 54 59 relativeRepoPath := filepath.Join(did, name) 60 + log = log.With("repo", relativeRepoPath) 55 61 56 62 if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 57 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 63 + log.Error("insufficient permissions", err, "did", actorDid.String()) 58 64 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 59 65 return 60 66 } 61 67 62 68 repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 63 69 if err != nil { 64 - fail(xrpcerr.GenericError(err)) 70 + fail("failed to build repo path", err, xrpcerr.GenericError(err)) 65 71 return 66 72 } 67 73 68 74 gr, err := git.PlainOpen(repoPath) 69 75 if err != nil { 70 - fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 76 + fail("failed to open repository", err, xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 71 77 return 72 78 } 73 79 74 80 forkCommit, err := gr.ResolveRevision(branch) 75 81 if err != nil { 76 - l.Error("error resolving ref revision", "msg", err.Error()) 77 - fail(xrpcerr.GenericError(fmt.Errorf("error resolving revision %s: %w", branch, err))) 82 + log.Error("error resolving ref revision", err) 83 + fail("error resolving branch revision", fmt.Errorf("error resolving revision %s: %w", branch, err), xrpcerr.GenericError(fmt.Errorf("error resolving revision %s: %w", branch, err))) 78 84 return 79 85 } 80 86 81 87 sourceCommit, err := gr.ResolveRevision(hiddenRef) 82 88 if err != nil { 83 - l.Error("error resolving hidden ref revision", "msg", err.Error()) 84 - fail(xrpcerr.GenericError(fmt.Errorf("error resolving revision %s: %w", hiddenRef, err))) 89 + log.Error("error resolving hidden ref revision", err) 90 + fail("error resolving hidden ref revision", fmt.Errorf("error resolving revision %s: %w", hiddenRef, err), xrpcerr.GenericError(fmt.Errorf("error resolving revision %s: %w", hiddenRef, err))) 85 91 return 86 92 } 87 93 ··· 89 95 if forkCommit.Hash.String() != sourceCommit.Hash.String() { 90 96 isAncestor, err := forkCommit.IsAncestor(sourceCommit) 91 97 if err != nil { 92 - l.Error("error checking ancestor relationship", "error", err.Error()) 93 - fail(xrpcerr.GenericError(fmt.Errorf("error resolving whether %s is ancestor of %s: %w", branch, hiddenRef, err))) 98 + log.Error("error checking ancestor relationship", err) 99 + fail("error checking ancestor", fmt.Errorf("error resolving whether %s is ancestor of %s: %w", branch, hiddenRef, err), xrpcerr.GenericError(fmt.Errorf("error resolving whether %s is ancestor of %s: %w", branch, hiddenRef, err))) 94 100 return 95 101 } 96 102 ··· 108 114 w.Header().Set("Content-Type", "application/json") 109 115 w.WriteHeader(http.StatusOK) 110 116 json.NewEncoder(w).Encode(response) 117 + log.Success("fork status calculated", "status", status) 111 118 }
+17 -10
knotserver/xrpc/fork_sync.go
··· 2 2 3 3 import ( 4 4 "encoding/json" 5 + "errors" 5 6 "fmt" 6 7 "net/http" 7 8 "path/filepath" ··· 15 16 ) 16 17 17 18 func (x *Xrpc) ForkSync(w http.ResponseWriter, r *http.Request) { 18 - l := x.Logger.With("handler", "ForkSync") 19 - fail := func(e xrpcerr.XrpcError) { 20 - l.Error("failed", "kind", e.Tag, "error", e.Message) 19 + log := x.newEndpointLog(r, "ForkSync") 20 + fail := func(msg string, err error, e xrpcerr.XrpcError, attrs ...any) { 21 + if err == nil { 22 + err = errors.New(e.Message) 23 + } 24 + log.Error(msg, err, append(attrs, "kind", e.Tag)...) 21 25 writeError(w, e, http.StatusBadRequest) 22 26 } 23 27 24 28 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 25 29 if !ok { 26 - fail(xrpcerr.MissingActorDidError) 30 + fail("missing actor did", nil, xrpcerr.MissingActorDidError) 27 31 return 28 32 } 29 33 30 34 var data tangled.RepoForkSync_Input 31 35 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 32 - fail(xrpcerr.GenericError(err)) 36 + fail("failed to decode request", err, xrpcerr.GenericError(err)) 33 37 return 34 38 } 35 39 ··· 38 42 branch := data.Branch 39 43 40 44 if did == "" || name == "" { 41 - fail(xrpcerr.GenericError(fmt.Errorf("did, name are required"))) 45 + fail("missing required fields", fmt.Errorf("did, name are required"), xrpcerr.GenericError(fmt.Errorf("did, name are required"))) 42 46 return 43 47 } 48 + log = log.With("actorDid", actorDid.String(), "did", did, "name", name, "branch", branch) 44 49 45 50 relativeRepoPath := filepath.Join(did, name) 51 + log = log.With("repo", relativeRepoPath) 46 52 47 53 if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 48 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 54 + log.Error("insufficient permissions", err, "did", actorDid.String()) 49 55 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 50 56 return 51 57 } 52 58 53 59 repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 54 60 if err != nil { 55 - fail(xrpcerr.GenericError(err)) 61 + fail("failed to build repo path", err, xrpcerr.GenericError(err)) 56 62 return 57 63 } 58 64 59 65 gr, err := git.Open(repoPath, branch) 60 66 if err != nil { 61 - fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 67 + fail("failed to open repository", err, xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 62 68 return 63 69 } 64 70 65 71 err = gr.Sync() 66 72 if err != nil { 67 - l.Error("error syncing repo fork", "error", err.Error()) 73 + log.Error("error syncing repo fork", err) 68 74 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 69 75 return 70 76 } 71 77 72 78 w.WriteHeader(http.StatusOK) 79 + log.Success("fork sync completed") 73 80 }
+22 -15
knotserver/xrpc/hidden_ref.go
··· 2 2 3 3 import ( 4 4 "encoding/json" 5 + "errors" 5 6 "fmt" 6 7 "net/http" 7 8 ··· 16 17 ) 17 18 18 19 func (x *Xrpc) HiddenRef(w http.ResponseWriter, r *http.Request) { 19 - l := x.Logger.With("handler", "HiddenRef") 20 - fail := func(e xrpcerr.XrpcError) { 21 - l.Error("failed", "kind", e.Tag, "error", e.Message) 22 - writeError(w, e, http.StatusBadRequest) 20 + log := x.newEndpointLog(r, "HiddenRef") 21 + fail := func(status int, msg string, err error, e xrpcerr.XrpcError, attrs ...any) { 22 + if err == nil { 23 + err = errors.New(e.Message) 24 + } 25 + log.Error(msg, err, append(attrs, "kind", e.Tag)...) 26 + writeError(w, e, status) 23 27 } 24 28 25 29 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 26 30 if !ok { 27 - fail(xrpcerr.MissingActorDidError) 31 + fail(http.StatusBadRequest, "missing actor did", nil, xrpcerr.MissingActorDidError) 28 32 return 29 33 } 30 34 31 35 var data tangled.RepoHiddenRef_Input 32 36 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 33 - fail(xrpcerr.GenericError(err)) 37 + fail(http.StatusBadRequest, "failed to decode request", err, xrpcerr.GenericError(err)) 34 38 return 35 39 } 36 40 ··· 39 43 repoAtUri := data.Repo 40 44 41 45 if forkRef == "" || remoteRef == "" || repoAtUri == "" { 42 - fail(xrpcerr.GenericError(fmt.Errorf("forkRef, remoteRef, and repo are required"))) 46 + fail(http.StatusBadRequest, "missing required fields", fmt.Errorf("forkRef, remoteRef, and repo are required"), xrpcerr.GenericError(fmt.Errorf("forkRef, remoteRef, and repo are required"))) 43 47 return 44 48 } 49 + log = log.With("actorDid", actorDid.String(), "forkRef", forkRef, "remoteRef", remoteRef, "repoAt", repoAtUri) 45 50 46 51 repoAt, err := syntax.ParseATURI(repoAtUri) 47 52 if err != nil { 48 - fail(xrpcerr.InvalidRepoError(repoAtUri)) 53 + fail(http.StatusBadRequest, "invalid repo at-uri", err, xrpcerr.InvalidRepoError(repoAtUri)) 49 54 return 50 55 } 51 56 52 57 ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) 53 58 if err != nil || ident.Handle.IsInvalidHandle() { 54 - fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 59 + fail(http.StatusBadRequest, "failed to resolve handle", err, xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 55 60 return 56 61 } 57 62 58 63 xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} 59 64 resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 60 65 if err != nil { 61 - fail(xrpcerr.GenericError(err)) 66 + fail(http.StatusBadRequest, "failed to fetch repo record", err, xrpcerr.GenericError(err)) 62 67 return 63 68 } 64 69 65 70 repo := resp.Value.Val.(*tangled.Repo) 66 71 didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 67 72 if err != nil { 68 - fail(xrpcerr.GenericError(err)) 73 + fail(http.StatusBadRequest, "failed to build repo path", err, xrpcerr.GenericError(err)) 69 74 return 70 75 } 76 + log = log.With("repo", didPath) 71 77 72 78 if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 73 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", didPath) 79 + log.Error("insufficient permissions", err, "did", actorDid.String()) 74 80 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 75 81 return 76 82 } 77 83 78 84 repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 79 85 if err != nil { 80 - fail(xrpcerr.GenericError(err)) 86 + fail(http.StatusBadRequest, "failed to build repo path", err, xrpcerr.GenericError(err)) 81 87 return 82 88 } 83 89 84 90 gr, err := git.PlainOpen(repoPath) 85 91 if err != nil { 86 - fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 92 + fail(http.StatusBadRequest, "failed to open repository", err, xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 87 93 return 88 94 } 89 95 90 96 err = gr.TrackHiddenRemoteRef(forkRef, remoteRef) 91 97 if err != nil { 92 - l.Error("error tracking hidden remote ref", "error", err.Error()) 98 + log.Error("error tracking hidden remote ref", err) 93 99 writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError) 94 100 return 95 101 } ··· 101 107 w.Header().Set("Content-Type", "application/json") 102 108 w.WriteHeader(http.StatusOK) 103 109 json.NewEncoder(w).Encode(response) 110 + log.Success("hidden ref tracked") 104 111 }
+5 -1
knotserver/xrpc/list_keys.go
··· 9 9 ) 10 10 11 11 func (x *Xrpc) ListKeys(w http.ResponseWriter, r *http.Request) { 12 + log := x.newEndpointLog(r, "ListKeys") 13 + 12 14 cursor := r.URL.Query().Get("cursor") 13 15 14 16 limit := 100 // default ··· 17 19 limit = l 18 20 } 19 21 } 22 + log = log.With("cursor", cursor, "limit", limit) 20 23 21 24 keys, nextCursor, err := x.Db.GetPublicKeysPaginated(limit, cursor) 22 25 if err != nil { 23 - x.Logger.Error("failed to get public keys", "error", err) 26 + log.Error("failed to get public keys", err) 24 27 writeError(w, xrpcerr.NewXrpcError( 25 28 xrpcerr.WithTag("InternalServerError"), 26 29 xrpcerr.WithMessage("failed to retrieve public keys"), ··· 46 49 } 47 50 48 51 writeJson(w, response) 52 + log.Success("public keys returned", "count", len(publicKeys), "nextCursor", nextCursor) 49 53 }
+49
knotserver/xrpc/logging.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + "time" 7 + ) 8 + 9 + type endpointLog struct { 10 + logger *slog.Logger 11 + start time.Time 12 + } 13 + 14 + func (x *Xrpc) newEndpointLog(r *http.Request, handler string, attrs ...any) *endpointLog { 15 + baseAttrs := []any{ 16 + "handler", handler, 17 + "method", r.Method, 18 + "path", r.URL.Path, 19 + } 20 + baseAttrs = append(baseAttrs, attrs...) 21 + 22 + return &endpointLog{ 23 + logger: x.Logger.With(baseAttrs...), 24 + start: time.Now(), 25 + } 26 + } 27 + 28 + func (l *endpointLog) With(attrs ...any) *endpointLog { 29 + return &endpointLog{ 30 + logger: l.logger.With(attrs...), 31 + start: l.start, 32 + } 33 + } 34 + 35 + func (l *endpointLog) Success(msg string, attrs ...any) { 36 + l.logger.Info(msg, append(attrs, "duration", time.Since(l.start))...) 37 + } 38 + 39 + func (l *endpointLog) Info(msg string, attrs ...any) { 40 + l.logger.Info(msg, append(attrs, "duration", time.Since(l.start))...) 41 + } 42 + 43 + func (l *endpointLog) Warn(msg string, attrs ...any) { 44 + l.logger.Warn(msg, append(attrs, "duration", time.Since(l.start))...) 45 + } 46 + 47 + func (l *endpointLog) Error(msg string, err error, attrs ...any) { 48 + l.logger.Error(msg, append(attrs, "error", err, "duration", time.Since(l.start))...) 49 + }
+19 -12
knotserver/xrpc/merge.go
··· 17 17 ) 18 18 19 19 func (x *Xrpc) Merge(w http.ResponseWriter, r *http.Request) { 20 - l := x.Logger.With("handler", "Merge") 21 - fail := func(e xrpcerr.XrpcError) { 22 - l.Error("failed", "kind", e.Tag, "error", e.Message) 23 - writeError(w, e, http.StatusBadRequest) 20 + log := x.newEndpointLog(r, "Merge") 21 + fail := func(status int, msg string, err error, e xrpcerr.XrpcError, attrs ...any) { 22 + if err == nil { 23 + err = errors.New(e.Message) 24 + } 25 + log.Error(msg, err, append(attrs, "kind", e.Tag)...) 26 + writeError(w, e, status) 24 27 } 25 28 26 29 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 27 30 if !ok { 28 - fail(xrpcerr.MissingActorDidError) 31 + fail(http.StatusBadRequest, "missing actor did", nil, xrpcerr.MissingActorDidError) 29 32 return 30 33 } 31 34 32 35 var data tangled.RepoMerge_Input 33 36 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 34 - fail(xrpcerr.GenericError(err)) 37 + fail(http.StatusBadRequest, "failed to decode merge request", err, xrpcerr.GenericError(err)) 35 38 return 36 39 } 40 + log = log.With("actorDid", actorDid.String(), "branch", data.Branch) 37 41 38 42 did := data.Did 39 43 name := data.Name 40 44 41 45 if did == "" || name == "" { 42 - fail(xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 46 + fail(http.StatusBadRequest, "missing required fields", fmt.Errorf("did and name are required"), xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 43 47 return 44 48 } 45 49 46 50 relativeRepoPath, err := securejoin.SecureJoin(did, name) 47 51 if err != nil { 48 - fail(xrpcerr.GenericError(err)) 52 + fail(http.StatusBadRequest, "failed to build repo path", err, xrpcerr.GenericError(err)) 49 53 return 50 54 } 55 + log = log.With("repo", relativeRepoPath) 51 56 52 57 if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 53 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 58 + log.Error("insufficient permissions", err, "did", actorDid.String()) 54 59 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 55 60 return 56 61 } 57 62 58 63 repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 59 64 if err != nil { 60 - fail(xrpcerr.GenericError(err)) 65 + fail(http.StatusBadRequest, "failed to build repo path", err, xrpcerr.GenericError(err)) 61 66 return 62 67 } 63 68 64 69 gr, err := git.Open(repoPath, data.Branch) 65 70 if err != nil { 66 - fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 71 + fail(http.StatusBadRequest, "failed to open repository", err, xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 67 72 return 68 73 } 69 74 ··· 96 101 Reason: conflict.Reason, 97 102 } 98 103 } 104 + log.Warn("merge conflict", "conflicts", conflicts) 99 105 100 106 conflictErr := xrpcerr.NewXrpcError( 101 107 xrpcerr.WithTag("MergeConflict"), ··· 104 110 writeError(w, conflictErr, http.StatusConflict) 105 111 return 106 112 } else { 107 - l.Error("failed to merge", "error", err.Error()) 113 + log.Error("failed to merge", err) 108 114 writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError) 109 115 return 110 116 } 111 117 } 112 118 113 119 w.WriteHeader(http.StatusOK) 120 + log.Success("merge completed") 114 121 }
+15 -10
knotserver/xrpc/merge_check.go
··· 13 13 ) 14 14 15 15 func (x *Xrpc) MergeCheck(w http.ResponseWriter, r *http.Request) { 16 - l := x.Logger.With("handler", "MergeCheck") 17 - fail := func(e xrpcerr.XrpcError) { 18 - l.Error("failed", "kind", e.Tag, "error", e.Message) 16 + log := x.newEndpointLog(r, "MergeCheck") 17 + fail := func(msg string, err error, e xrpcerr.XrpcError) { 18 + if err == nil { 19 + err = errors.New(e.Message) 20 + } 21 + log.Error(msg, err, "kind", e.Tag) 19 22 writeError(w, e, http.StatusBadRequest) 20 23 } 21 24 22 25 var data tangled.RepoMergeCheck_Input 23 26 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 24 - fail(xrpcerr.GenericError(err)) 27 + fail("failed to decode merge check request", err, xrpcerr.GenericError(err)) 25 28 return 26 29 } 27 30 ··· 29 32 name := data.Name 30 33 31 34 if did == "" || name == "" { 32 - fail(xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 35 + fail("missing required fields", fmt.Errorf("did and name are required"), xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 33 36 return 34 37 } 35 38 36 39 relativeRepoPath, err := securejoin.SecureJoin(did, name) 37 40 if err != nil { 38 - fail(xrpcerr.GenericError(err)) 41 + fail("failed to build repo path", err, xrpcerr.GenericError(err)) 39 42 return 40 43 } 44 + log = log.With("repo", relativeRepoPath, "branch", data.Branch) 41 45 42 46 repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 43 47 if err != nil { 44 - fail(xrpcerr.GenericError(err)) 48 + fail("failed to build repo path", err, xrpcerr.GenericError(err)) 45 49 return 46 50 } 47 51 48 52 gr, err := git.Open(repoPath, data.Branch) 49 53 if err != nil { 50 - fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 54 + fail("failed to open repository", err, xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err))) 51 55 return 52 56 } 53 57 ··· 79 83 errMsg := err.Error() 80 84 response.Error = &errMsg 81 85 } 82 - } 83 86 84 - l.Debug("merge check response", "isConflicted", response.Is_conflicted, "err", response.Error, "conflicts", response.Conflicts) 87 + log.Warn("merge check conflicts detected", "conflicts", response.Conflicts, "error", response.Error) 88 + } 85 89 86 90 w.Header().Set("Content-Type", "application/json") 87 91 w.WriteHeader(http.StatusOK) 88 92 json.NewEncoder(w).Encode(response) 93 + log.Success("merge check completed", "isConflicted", response.Is_conflicted, "conflictCount", len(response.Conflicts)) 89 94 }
+5
knotserver/xrpc/owner.go
··· 1 1 package xrpc 2 2 3 3 import ( 4 + "fmt" 4 5 "net/http" 5 6 6 7 "tangled.org/core/api/tangled" ··· 8 9 ) 9 10 10 11 func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) { 12 + log := x.newEndpointLog(r, "Owner") 13 + 11 14 owner := x.Config.Server.Owner 12 15 if owner == "" { 16 + log.Error("owner not configured", fmt.Errorf("owner empty")) 13 17 writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError) 14 18 return 15 19 } ··· 19 23 } 20 24 21 25 writeJson(w, response) 26 + log.Success("owner returned", "owner", owner) 22 27 }
+11 -2
knotserver/xrpc/repo_archive.go
··· 13 13 ) 14 14 15 15 func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) { 16 + log := x.newEndpointLog(r, "RepoArchive") 17 + 16 18 repo := r.URL.Query().Get("repo") 17 19 repoPath, err := x.parseRepoParam(repo) 18 20 if err != nil { 21 + log.Error("invalid repo parameter", err, "repo", repo) 19 22 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 20 23 return 21 24 } ··· 27 30 if format == "" { 28 31 format = "tar.gz" // default 29 32 } 33 + log = log.With("repo", repo, "ref", ref, "format", format) 30 34 31 35 prefix := r.URL.Query().Get("prefix") 32 36 33 37 if format != "tar.gz" { 38 + log.Error("unsupported archive format", fmt.Errorf("unsupported format"), "format", format) 34 39 writeError(w, xrpcerr.NewXrpcError( 35 40 xrpcerr.WithTag("InvalidRequest"), 36 41 xrpcerr.WithMessage("only tar.gz format is supported"), ··· 40 45 41 46 gr, err := git.Open(repoPath, ref) 42 47 if err != nil { 48 + log.Error("failed to open repo", err, "repoPath", repoPath) 43 49 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 44 50 return 45 51 } ··· 57 63 } 58 64 59 65 filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) 66 + log = log.With("filename", filename, "archivePrefix", archivePrefix) 60 67 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 61 68 w.Header().Set("Content-Type", "application/gzip") 62 69 ··· 67 74 if err != nil { 68 75 // once we start writing to the body we can't report error anymore 69 76 // so we are only left with logging the error 70 - x.Logger.Error("writing tar file", "error", err.Error()) 77 + log.Error("writing tar file", err) 71 78 return 72 79 } 73 80 ··· 75 82 if err != nil { 76 83 // once we start writing to the body we can't report error anymore 77 84 // so we are only left with logging the error 78 - x.Logger.Error("flushing", "error", err.Error()) 85 + log.Error("flushing", err) 79 86 return 80 87 } 88 + 89 + log.Success("archive generated") 81 90 }
+13 -2
knotserver/xrpc/repo_blob.go
··· 15 15 ) 16 16 17 17 func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) { 18 + log := x.newEndpointLog(r, "RepoBlob") 19 + 18 20 repo := r.URL.Query().Get("repo") 19 21 repoPath, err := x.parseRepoParam(repo) 20 22 if err != nil { 23 + log.Error("invalid repo parameter", err, "repo", repo) 21 24 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 22 25 return 23 26 } ··· 26 29 // ref can be empty (git.Open handles this) 27 30 28 31 treePath := r.URL.Query().Get("path") 32 + log = log.With("repo", repo, "ref", ref, "path", treePath) 29 33 if treePath == "" { 34 + log.Error("missing path parameter", fmt.Errorf("missing path parameter")) 30 35 writeError(w, xrpcerr.NewXrpcError( 31 36 xrpcerr.WithTag("InvalidRequest"), 32 37 xrpcerr.WithMessage("missing path parameter"), ··· 35 40 } 36 41 37 42 raw := r.URL.Query().Get("raw") == "true" 43 + log = log.With("raw", raw) 38 44 39 45 gr, err := git.Open(repoPath, ref) 40 46 if err != nil { 47 + log.Error("failed to open repo", err) 41 48 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 42 49 return 43 50 } ··· 56 63 Branch: &submodule.Branch, 57 64 }, 58 65 } 66 + log.Success("returned submodule metadata", "submodule", submodule.Name) 59 67 writeJson(w, response) 60 68 return 61 69 } 62 70 63 71 contents, err := gr.RawContent(treePath) 64 72 if err != nil { 65 - x.Logger.Error("file content", "error", err.Error(), "treePath", treePath) 73 + log.Error("file content", err, "treePath", treePath) 66 74 writeError(w, xrpcerr.NewXrpcError( 67 75 xrpcerr.WithTag("FileNotFound"), 68 76 xrpcerr.WithMessage("file not found at the specified path"), ··· 83 91 switch { 84 92 case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 85 93 if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 94 + log.Info("client cache hit for raw content", "etag", eTag) 86 95 w.WriteHeader(http.StatusNotModified) 87 96 return 88 97 } ··· 100 109 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 101 110 102 111 default: 103 - x.Logger.Error("attempted to serve disallowed file type", "mimetype", mimeType) 112 + log.Error("attempted to serve disallowed file type", fmt.Errorf("disallowed"), "mimetype", mimeType) 104 113 writeError(w, xrpcerr.NewXrpcError( 105 114 xrpcerr.WithTag("InvalidRequest"), 106 115 xrpcerr.WithMessage("only image, video, and text files can be accessed directly"), ··· 108 117 return 109 118 } 110 119 w.Write(contents) 120 + log.Success("served raw blob", "mimeType", mimeType, "etag", eTag) 111 121 return 112 122 } 113 123 ··· 143 153 } 144 154 145 155 writeJson(w, response) 156 + log.Success("blob metadata returned", "mimeType", mimeType, "isBinary", isBinary) 146 157 } 147 158 148 159 // isTextualMimeType returns true if the MIME type represents textual content
+11 -3
knotserver/xrpc/repo_branch.go
··· 1 1 package xrpc 2 2 3 3 import ( 4 + "fmt" 4 5 "net/http" 5 6 "net/url" 6 7 "time" ··· 11 12 ) 12 13 13 14 func (x *Xrpc) RepoBranch(w http.ResponseWriter, r *http.Request) { 15 + log := x.newEndpointLog(r, "RepoBranch") 16 + 14 17 repo := r.URL.Query().Get("repo") 15 18 repoPath, err := x.parseRepoParam(repo) 16 19 if err != nil { 20 + log.Error("invalid repo parameter", err, "repo", repo) 17 21 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 18 22 return 19 23 } 20 24 21 25 name := r.URL.Query().Get("name") 22 26 if name == "" { 27 + log.Error("missing branch name", fmt.Errorf("missing name parameter")) 23 28 writeError(w, xrpcerr.NewXrpcError( 24 29 xrpcerr.WithTag("InvalidRequest"), 25 30 xrpcerr.WithMessage("missing name parameter"), ··· 28 33 } 29 34 30 35 branchName, _ := url.PathUnescape(name) 36 + log = log.With("repo", repo, "branch", branchName) 31 37 32 38 gr, err := git.PlainOpen(repoPath) 33 39 if err != nil { 40 + log.Error("failed to open repo", err, "repoPath", repoPath) 34 41 writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent) 35 42 return 36 43 } 37 44 38 45 ref, err := gr.Branch(branchName) 39 46 if err != nil { 40 - x.Logger.Error("getting branch", "error", err.Error()) 47 + log.Error("getting branch", err) 41 48 writeError(w, xrpcerr.NewXrpcError( 42 49 xrpcerr.WithTag("BranchNotFound"), 43 50 xrpcerr.WithMessage("branch not found"), ··· 47 54 48 55 commit, err := gr.Commit(ref.Hash()) 49 56 if err != nil { 50 - x.Logger.Error("getting commit object", "error", err.Error()) 57 + log.Error("getting commit object", err) 51 58 writeError(w, xrpcerr.NewXrpcError( 52 59 xrpcerr.WithTag("BranchNotFound"), 53 60 xrpcerr.WithMessage("failed to get commit object"), ··· 58 65 defaultBranch, err := gr.FindMainBranch() 59 66 isDefault := false 60 67 if err != nil { 61 - x.Logger.Error("getting default branch", "error", err.Error()) 68 + log.Warn("getting default branch", "error", err) 62 69 } else if defaultBranch == branchName { 63 70 isDefault = true 64 71 } ··· 82 89 } 83 90 84 91 writeJson(w, response) 92 + log.Success("branch metadata returned", "isDefault", isDefault) 85 93 }
+6
knotserver/xrpc/repo_branches.go
··· 10 10 ) 11 11 12 12 func (x *Xrpc) RepoBranches(w http.ResponseWriter, r *http.Request) { 13 + log := x.newEndpointLog(r, "RepoBranches") 14 + 13 15 repo := r.URL.Query().Get("repo") 14 16 repoPath, err := x.parseRepoParam(repo) 15 17 if err != nil { 18 + log.Error("invalid repo parameter", err, "repo", repo) 16 19 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 17 20 return 18 21 } ··· 27 30 // } 28 31 29 32 limit := 500 33 + log = log.With("repo", repo, "cursor", cursor, "limit", limit) 30 34 31 35 gr, err := git.PlainOpen(repoPath) 32 36 if err != nil { 37 + log.Error("failed to open repo", err, "repoPath", repoPath) 33 38 writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent) 34 39 return 35 40 } ··· 53 58 } 54 59 55 60 writeJson(w, response) 61 + log.Success("branches returned", "count", len(paginatedBranches), "offset", offset) 56 62 }
+12 -4
knotserver/xrpc/repo_compare.go
··· 11 11 ) 12 12 13 13 func (x *Xrpc) RepoCompare(w http.ResponseWriter, r *http.Request) { 14 + log := x.newEndpointLog(r, "RepoCompare") 15 + 14 16 repo := r.URL.Query().Get("repo") 15 17 repoPath, err := x.parseRepoParam(repo) 16 18 if err != nil { 19 + log.Error("invalid repo parameter", err, "repo", repo) 17 20 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 18 21 return 19 22 } 20 23 21 24 rev1 := r.URL.Query().Get("rev1") 22 25 if rev1 == "" { 26 + log.Error("missing rev1 parameter", fmt.Errorf("missing rev1")) 23 27 writeError(w, xrpcerr.NewXrpcError( 24 28 xrpcerr.WithTag("InvalidRequest"), 25 29 xrpcerr.WithMessage("missing rev1 parameter"), ··· 29 33 30 34 rev2 := r.URL.Query().Get("rev2") 31 35 if rev2 == "" { 36 + log.Error("missing rev2 parameter", fmt.Errorf("missing rev2")) 32 37 writeError(w, xrpcerr.NewXrpcError( 33 38 xrpcerr.WithTag("InvalidRequest"), 34 39 xrpcerr.WithMessage("missing rev2 parameter"), 35 40 ), http.StatusBadRequest) 36 41 return 37 42 } 43 + log = log.With("repo", repo, "rev1", rev1, "rev2", rev2) 38 44 39 45 gr, err := git.PlainOpen(repoPath) 40 46 if err != nil { 47 + log.Error("failed to open repo", err, "repoPath", repoPath) 41 48 writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent) 42 49 return 43 50 } 44 51 45 52 commit1, err := gr.ResolveRevision(rev1) 46 53 if err != nil { 47 - x.Logger.Error("error resolving revision 1", "msg", err.Error()) 54 + log.Error("error resolving revision 1", err) 48 55 writeError(w, xrpcerr.NewXrpcError( 49 56 xrpcerr.WithTag("RevisionNotFound"), 50 57 xrpcerr.WithMessage(fmt.Sprintf("error resolving revision %s", rev1)), ··· 54 61 55 62 commit2, err := gr.ResolveRevision(rev2) 56 63 if err != nil { 57 - x.Logger.Error("error resolving revision 2", "msg", err.Error()) 64 + log.Error("error resolving revision 2", err) 58 65 writeError(w, xrpcerr.NewXrpcError( 59 66 xrpcerr.WithTag("RevisionNotFound"), 60 67 xrpcerr.WithMessage(fmt.Sprintf("error resolving revision %s", rev2)), ··· 64 71 65 72 rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2) 66 73 if err != nil { 67 - x.Logger.Error("error comparing revisions", "msg", err.Error()) 74 + log.Error("error comparing revisions", err) 68 75 writeError(w, xrpcerr.NewXrpcError( 69 76 xrpcerr.WithTag("CompareError"), 70 77 xrpcerr.WithMessage("error comparing revisions"), ··· 78 85 if len(formatPatch) >= 2 { 79 86 diffTree, err := gr.DiffTree(commit1, commit2) 80 87 if err != nil { 81 - x.Logger.Error("error comparing revisions", "msg", err.Error()) 88 + log.Error("error building combined patch", err) 82 89 } else { 83 90 combinedPatch = diffTree.Diff 84 91 combinedPatchRaw = diffTree.Patch ··· 95 102 } 96 103 97 104 writeJson(w, response) 105 + log.Success("compare response generated", "rev1", commit1.Hash.String(), "rev2", commit2.Hash.String()) 98 106 }
+7 -1
knotserver/xrpc/repo_diff.go
··· 9 9 ) 10 10 11 11 func (x *Xrpc) RepoDiff(w http.ResponseWriter, r *http.Request) { 12 + log := x.newEndpointLog(r, "RepoDiff") 13 + 12 14 repo := r.URL.Query().Get("repo") 13 15 repoPath, err := x.parseRepoParam(repo) 14 16 if err != nil { 17 + log.Error("invalid repo parameter", err, "repo", repo) 15 18 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 16 19 return 17 20 } 18 21 19 22 ref := r.URL.Query().Get("ref") 23 + log = log.With("repo", repo, "ref", ref) 20 24 // ref can be empty (git.Open handles this) 21 25 22 26 gr, err := git.Open(repoPath, ref) 23 27 if err != nil { 28 + log.Error("failed to open repo", err, "repoPath", repoPath) 24 29 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 25 30 return 26 31 } 27 32 28 33 diff, err := gr.Diff() 29 34 if err != nil { 30 - x.Logger.Error("getting diff", "error", err.Error()) 35 + log.Error("getting diff", err) 31 36 writeError(w, xrpcerr.RefNotFoundError, http.StatusInternalServerError) 32 37 return 33 38 } ··· 38 43 } 39 44 40 45 writeJson(w, response) 46 + log.Success("diff generated") 41 47 }
+11 -1
knotserver/xrpc/repo_get_default_branch.go
··· 10 10 ) 11 11 12 12 func (x *Xrpc) RepoGetDefaultBranch(w http.ResponseWriter, r *http.Request) { 13 + log := x.newEndpointLog(r, "RepoGetDefaultBranch") 14 + 13 15 repo := r.URL.Query().Get("repo") 14 16 repoPath, err := x.parseRepoParam(repo) 15 17 if err != nil { 18 + log.Error("invalid repo parameter", err, "repo", repo) 16 19 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 17 20 return 18 21 } 19 22 20 23 gr, err := git.PlainOpen(repoPath) 24 + if err != nil { 25 + log.Error("failed to open repo", err, "repoPath", repoPath) 26 + writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent) 27 + return 28 + } 29 + log = log.With("repo", repo) 21 30 22 31 branch, err := gr.FindMainBranch() 23 32 if err != nil { 24 - x.Logger.Error("getting default branch", "error", err.Error()) 33 + log.Error("getting default branch", err) 25 34 writeError(w, xrpcerr.NewXrpcError( 26 35 xrpcerr.WithTag("InvalidRequest"), 27 36 xrpcerr.WithMessage("failed to get default branch"), ··· 36 45 } 37 46 38 47 writeJson(w, response) 48 + log.Success("default branch returned", "branch", branch) 39 49 }
+7 -2
knotserver/xrpc/repo_languages.go
··· 12 12 ) 13 13 14 14 func (x *Xrpc) RepoLanguages(w http.ResponseWriter, r *http.Request) { 15 + log := x.newEndpointLog(r, "RepoLanguages") 16 + 15 17 repo := r.URL.Query().Get("repo") 16 18 repoPath, err := x.parseRepoParam(repo) 17 19 if err != nil { 20 + log.Error("invalid repo parameter", err, "repo", repo) 18 21 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 19 22 return 20 23 } 21 24 22 25 ref := r.URL.Query().Get("ref") 26 + log = log.With("repo", repo, "ref", ref) 23 27 24 28 gr, err := git.Open(repoPath, ref) 25 29 if err != nil { 26 - x.Logger.Error("opening repo", "error", err.Error()) 30 + log.Error("opening repo", err) 27 31 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 28 32 return 29 33 } ··· 33 37 34 38 sizes, err := gr.AnalyzeLanguages(ctx) 35 39 if err != nil { 36 - x.Logger.Error("failed to analyze languages", "error", err.Error()) 40 + log.Error("failed to analyze languages", err) 37 41 writeError(w, xrpcerr.NewXrpcError( 38 42 xrpcerr.WithTag("InvalidRequest"), 39 43 xrpcerr.WithMessage("failed to analyze repository languages"), ··· 73 77 } 74 78 75 79 writeJson(w, response) 80 + log.Success("language analysis returned", "languages", len(apiLanguages), "totalFiles", len(sizes)) 76 81 }
+8 -2
knotserver/xrpc/repo_log.go
··· 10 10 ) 11 11 12 12 func (x *Xrpc) RepoLog(w http.ResponseWriter, r *http.Request) { 13 + log := x.newEndpointLog(r, "RepoLog") 14 + 13 15 repo := r.URL.Query().Get("repo") 14 16 repoPath, err := x.parseRepoParam(repo) 15 17 if err != nil { 18 + log.Error("invalid repo parameter", err, "repo", repo) 16 19 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 17 20 return 18 21 } ··· 28 31 limit = l 29 32 } 30 33 } 34 + log = log.With("repo", repo, "ref", ref, "path", path, "cursor", cursor, "limit", limit) 31 35 32 36 gr, err := git.Open(repoPath, ref) 33 37 if err != nil { 38 + log.Error("failed to open repo", err, "repoPath", repoPath) 34 39 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 35 40 return 36 41 } ··· 44 49 45 50 commits, err := gr.Commits(offset, limit) 46 51 if err != nil { 47 - x.Logger.Error("fetching commits", "error", err.Error()) 52 + log.Error("fetching commits", err, "offset", offset, "limit", limit) 48 53 writeError(w, xrpcerr.NewXrpcError( 49 54 xrpcerr.WithTag("PathNotFound"), 50 55 xrpcerr.WithMessage("failed to read commit log"), ··· 54 59 55 60 total, err := gr.TotalCommits() 56 61 if err != nil { 57 - x.Logger.Error("fetching total commits", "error", err.Error()) 62 + log.Error("fetching total commits", err) 58 63 writeError(w, xrpcerr.NewXrpcError( 59 64 xrpcerr.WithTag("InternalServerError"), 60 65 xrpcerr.WithMessage("failed to fetch total commits"), ··· 78 83 response.Log = true 79 84 80 85 writeJson(w, response) 86 + log.Success("commit log returned", "count", len(commits), "page", response.Page, "total", total) 81 87 }
+7 -2
knotserver/xrpc/repo_tags.go
··· 13 13 ) 14 14 15 15 func (x *Xrpc) RepoTags(w http.ResponseWriter, r *http.Request) { 16 + log := x.newEndpointLog(r, "RepoTags") 17 + 16 18 repo := r.URL.Query().Get("repo") 17 19 repoPath, err := x.parseRepoParam(repo) 18 20 if err != nil { 21 + log.Error("invalid repo parameter", err, "repo", repo) 19 22 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 20 23 return 21 24 } ··· 28 31 limit = l 29 32 } 30 33 } 34 + log = log.With("repo", repo, "cursor", cursor, "limit", limit) 31 35 32 36 gr, err := git.PlainOpen(repoPath) 33 37 if err != nil { 34 - x.Logger.Error("failed to open", "error", err) 38 + log.Error("failed to open repo", err, "repoPath", repoPath) 35 39 writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent) 36 40 return 37 41 } 38 42 39 43 tags, err := gr.Tags() 40 44 if err != nil { 41 - x.Logger.Warn("getting tags", "error", err.Error()) 45 + log.Warn("getting tags", "error", err) 42 46 tags = []object.Tag{} 43 47 } 44 48 ··· 83 87 } 84 88 85 89 writeJson(w, response) 90 + log.Success("tags returned", "count", len(paginatedTags), "offset", offset) 86 91 }
+8 -3
knotserver/xrpc/repo_tree.go
··· 13 13 ) 14 14 15 15 func (x *Xrpc) RepoTree(w http.ResponseWriter, r *http.Request) { 16 + log := x.newEndpointLog(r, "RepoTree") 17 + 16 18 ctx := r.Context() 17 19 18 20 repo := r.URL.Query().Get("repo") 19 21 repoPath, err := x.parseRepoParam(repo) 20 22 if err != nil { 23 + log.Error("invalid repo parameter", err, "repo", repo) 21 24 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 22 25 return 23 26 } ··· 26 29 // ref can be empty (git.Open handles this) 27 30 28 31 path := r.URL.Query().Get("path") 32 + log = log.With("repo", repo, "ref", ref, "path", path) 29 33 // path can be empty (defaults to root) 30 34 31 35 gr, err := git.Open(repoPath, ref) 32 36 if err != nil { 33 - x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref) 37 + log.Error("failed to open git repository", err, "repoPath", repoPath) 34 38 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 35 39 return 36 40 } 37 41 38 42 files, err := gr.FileTree(ctx, path) 39 43 if err != nil { 40 - x.Logger.Error("failed to get file tree", "error", err, "path", path) 44 + log.Error("failed to get file tree", err) 41 45 writeError(w, xrpcerr.NewXrpcError( 42 46 xrpcerr.WithTag("PathNotFound"), 43 47 xrpcerr.WithMessage("failed to read repository tree"), ··· 52 56 if markup.IsReadmeFile(file.Name) { 53 57 contents, err := gr.RawContent(filepath.Join(path, file.Name)) 54 58 if err != nil { 55 - x.Logger.Error("failed to read contents of file", "path", path, "file", file.Name) 59 + log.Warn("failed to read contents of file", "path", path, "file", file.Name, "error", err) 56 60 } 57 61 58 62 if utf8.Valid(contents) { ··· 108 112 } 109 113 110 114 writeJson(w, response) 115 + log.Success("tree returned", "fileCount", len(treeEntries), "readmeFound", readmeFileName != "") 111 116 }
+20 -13
knotserver/xrpc/set_default_branch.go
··· 2 2 3 3 import ( 4 4 "encoding/json" 5 + "errors" 5 6 "fmt" 6 7 "net/http" 7 8 ··· 19 20 const ActorDid string = "ActorDid" 20 21 21 22 func (x *Xrpc) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 22 - l := x.Logger 23 - fail := func(e xrpcerr.XrpcError) { 24 - l.Error("failed", "kind", e.Tag, "error", e.Message) 25 - writeError(w, e, http.StatusBadRequest) 23 + log := x.newEndpointLog(r, "SetDefaultBranch") 24 + fail := func(status int, msg string, err error, e xrpcerr.XrpcError, attrs ...any) { 25 + if err == nil { 26 + err = errors.New(e.Message) 27 + } 28 + log.Error(msg, err, append(attrs, "kind", e.Tag)...) 29 + writeError(w, e, status) 26 30 } 27 31 28 32 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 29 33 if !ok { 30 - fail(xrpcerr.MissingActorDidError) 34 + fail(http.StatusBadRequest, "missing actor did", nil, xrpcerr.MissingActorDidError) 31 35 return 32 36 } 33 37 34 38 var data tangled.RepoSetDefaultBranch_Input 35 39 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 36 - fail(xrpcerr.GenericError(err)) 40 + fail(http.StatusBadRequest, "failed to decode request", err, xrpcerr.GenericError(err)) 37 41 return 38 42 } 43 + log = log.With("actorDid", actorDid.String(), "requestedDefault", data.DefaultBranch) 39 44 40 45 // unfortunately we have to resolve repo-at here 41 46 repoAt, err := syntax.ParseATURI(data.Repo) 42 47 if err != nil { 43 - fail(xrpcerr.InvalidRepoError(data.Repo)) 48 + fail(http.StatusBadRequest, "invalid repo at-uri", err, xrpcerr.InvalidRepoError(data.Repo)) 44 49 return 45 50 } 46 51 47 52 // resolve this aturi to extract the repo record 48 53 ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) 49 54 if err != nil || ident.Handle.IsInvalidHandle() { 50 - fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 55 + fail(http.StatusBadRequest, "failed to resolve handle", err, xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 51 56 return 52 57 } 53 58 54 59 xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} 55 60 resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 56 61 if err != nil { 57 - fail(xrpcerr.GenericError(err)) 62 + fail(http.StatusBadRequest, "failed to fetch repo record", err, xrpcerr.GenericError(err)) 58 63 return 59 64 } 60 65 61 66 repo := resp.Value.Val.(*tangled.Repo) 62 67 didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 63 68 if err != nil { 64 - fail(xrpcerr.GenericError(err)) 69 + fail(http.StatusBadRequest, "failed to build repo path", err, xrpcerr.GenericError(err)) 65 70 return 66 71 } 72 + log = log.With("repo", didPath) 67 73 68 74 if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 69 - l.Error("insufficent permissions", "did", actorDid.String()) 75 + log.Error("insufficent permissions", err, "did", actorDid.String()) 70 76 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 71 77 return 72 78 } ··· 74 80 path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 75 81 gr, err := git.PlainOpen(path) 76 82 if err != nil { 77 - fail(xrpcerr.GenericError(err)) 83 + fail(http.StatusBadRequest, "failed to open repository", err, xrpcerr.GenericError(err)) 78 84 return 79 85 } 80 86 81 87 err = gr.SetDefaultBranch(data.DefaultBranch) 82 88 if err != nil { 83 - l.Error("setting default branch", "error", err.Error()) 89 + log.Error("setting default branch", err, "branch", data.DefaultBranch) 84 90 writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError) 85 91 return 86 92 } 87 93 88 94 w.WriteHeader(http.StatusOK) 95 + log.Success("default branch updated") 89 96 }
+4
knotserver/xrpc/version.go
··· 12 12 var version string 13 13 14 14 func (x *Xrpc) Version(w http.ResponseWriter, r *http.Request) { 15 + log := x.newEndpointLog(r, "Version") 16 + 15 17 if version == "" { 16 18 info, ok := debug.ReadBuildInfo() 17 19 if !ok { 20 + log.Error("failed to read build info", fmt.Errorf("build info unavailable")) 18 21 http.Error(w, "failed to read build info", http.StatusInternalServerError) 19 22 return 20 23 } ··· 57 60 } 58 61 59 62 writeJson(w, response) 63 + log.Success("version returned", "version", version) 60 64 }