go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/build/logging.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package build 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "io" 22 "strings" 23 "sync" 24 25 bbpb "go.chromium.org/luci/buildbucket/proto" 26 "go.chromium.org/luci/common/logging" 27 "go.chromium.org/luci/logdog/client/butlerlib/streamclient" 28 "go.chromium.org/luci/logdog/common/types" 29 ) 30 31 // Loggable is the common interface for build entities which have log data 32 // associated with them. 33 // 34 // Implemented by State and Step. 35 // 36 // Logs all have a name which is an arbitrary bit of text to identify the log to 37 // human users (it will appear as the link on the build UI page). In particular 38 // it does NOT need to conform to the LogDog stream name alphabet. 39 // 40 // The log name "log" is reserved, and will automatically capture all logging 41 // outputs generated with the "go.chromium.org/luci/common/logging" API. 42 type Loggable interface { 43 // Log creates a new log stream (by default, line-oriented text) with the 44 // given name. 45 // 46 // To uphold the requirements of the Build proto message, duplicate log names 47 // will be deduplicated with the same algorithm used for deduplicating step 48 // names. 49 // 50 // To create a binary stream, pass streamclient.Binary() as one of the 51 // options. 52 // 53 // The stream will close when the associated object (step or build) is End'd. 54 Log(name string, opts ...streamclient.Option) *Log 55 56 // LogDatagram creates a new datagram-oriented log stream with the given name. 57 // 58 // To uphold the requirements of the Build proto message, duplicate log names 59 // will be deduplicated with the same algorithm used for deduplicating step 60 // names. 61 // 62 // The stream will close when the associated object (step or build) is End'd. 63 LogDatagram(name string, opts ...streamclient.Option) streamclient.DatagramWriter 64 } 65 66 type loggingWriter struct { 67 mu sync.Mutex 68 buf *bytes.Buffer 69 logf func(string) 70 } 71 72 var _ io.WriteCloser = (*loggingWriter)(nil) 73 74 func makeLoggingWriter(ctx context.Context, name string) io.WriteCloser { 75 ctx = logging.SetField(ctx, "build.logname", name) 76 targetLevel := logging.Info 77 if strings.HasPrefix(name, "$") { 78 targetLevel = logging.Debug 79 } 80 if !logging.IsLogging(ctx, targetLevel) { 81 return nopStream{} 82 } 83 84 rawLogFn := logging.Get(ctx).LogCall 85 return &loggingWriter{ 86 buf: &bytes.Buffer{}, 87 logf: func(line string) { 88 rawLogFn(targetLevel, 0, "%s", []any{line}) 89 }, 90 } 91 } 92 93 func (l *loggingWriter) Write(bs []byte) (n int, err error) { 94 l.mu.Lock() 95 defer l.mu.Unlock() 96 97 if n, err = l.buf.Write(bs); err != nil { 98 return 99 } 100 l.drainLines() 101 return 102 } 103 104 func (l *loggingWriter) drainLines() { 105 maybeReadLine := func() (string, bool) { 106 i := bytes.IndexByte(l.buf.Bytes(), '\n') 107 if i < 0 { 108 return "", false 109 } 110 line := make([]byte, i+1) 111 l.buf.Read(line) // cannot panic 112 return string(line), true 113 } 114 115 for { 116 line, ok := maybeReadLine() 117 if !ok { 118 return 119 } 120 l.logf(line) 121 } 122 } 123 124 func (l *loggingWriter) Close() error { 125 l.mu.Lock() 126 defer l.mu.Unlock() 127 128 l.drainLines() 129 if l.buf.Len() > 0 { 130 l.logf(l.buf.String()) 131 } 132 return nil 133 } 134 135 type nopStream struct{} 136 137 var _ io.WriteCloser = nopStream{} 138 139 func (n nopStream) Write(dg []byte) (int, error) { return len(dg), nil } 140 func (n nopStream) Close() error { return nil } 141 142 type nopDatagramStream struct{} 143 144 var _ streamclient.DatagramStream = nopDatagramStream{} 145 146 func (n nopDatagramStream) WriteDatagram(dg []byte) error { return nil } 147 func (n nopDatagramStream) Close() error { return nil } 148 149 // Log represents a step or build log. It can be written to directly, 150 // and also provides additional information about the log itself. 151 // 152 // The creator of the Log is responsible for cleaning up any resources 153 // associated with it (e.g. the Step or State this was created from). 154 type Log struct { 155 io.Writer 156 157 ref *bbpb.Log 158 namespace types.StreamName 159 infra *bbpb.BuildInfra_LogDog 160 } 161 162 // UILink returns a URL to this log fit for surfacing in the LUCI UI. 163 // 164 // This may return an empty string if there's no available LogDog infra being 165 // logged to, for instance in testing or during local execution where logdog 166 // streams are not sunk to the actual logdog service. 167 func (l *Log) UILink() string { 168 if l.infra == nil { 169 return "" 170 } 171 stream := types.StreamName(l.ref.Url) 172 return fmt.Sprintf("https://%s/logs/%s/%s/+/%s%s", l.infra.Hostname, l.infra.Project, l.infra.Prefix, l.namespace, stream) 173 }