1
1
package agent
2
2
3
3
import (
4
+ "bufio"
5
+ "bytes"
6
+ "io"
4
7
"net/http"
8
+ "os"
9
+ "path/filepath"
5
10
"sync"
6
11
"time"
7
12
@@ -11,7 +16,7 @@ import (
11
16
"github.com/coder/coder/codersdk"
12
17
)
13
18
14
- func (* agent ) apiHandler () http.Handler {
19
+ func (a * agent ) apiHandler () http.Handler {
15
20
r := chi .NewRouter ()
16
21
r .Get ("/" , func (rw http.ResponseWriter , r * http.Request ) {
17
22
httpapi .Write (r .Context (), rw , http .StatusOK , codersdk.Response {
@@ -22,6 +27,26 @@ func (*agent) apiHandler() http.Handler {
22
27
lp := & listeningPortsHandler {}
23
28
r .Get ("/api/v0/listening-ports" , lp .handler )
24
29
30
+ logs := & logsHandler {
31
+ logFiles : []* logFile {
32
+ {
33
+ name : codersdk .WorkspaceAgentLogAgent ,
34
+ path : filepath .Join (a .logDir , string (codersdk .WorkspaceAgentLogAgent )),
35
+ },
36
+ {
37
+ name : codersdk .WorkspaceAgentLogStartupScript ,
38
+ path : filepath .Join (a .logDir , string (codersdk .WorkspaceAgentLogStartupScript )),
39
+ },
40
+ },
41
+ }
42
+ r .Route ("/api/v0/logs" , func (r chi.Router ) {
43
+ r .Get ("/" , logs .list )
44
+ r .Route ("/{log}" , func (r chi.Router ) {
45
+ r .Get ("/" , logs .info )
46
+ r .Get ("/tail" , logs .tail )
47
+ })
48
+ })
49
+
25
50
return r
26
51
}
27
52
@@ -47,3 +72,216 @@ func (lp *listeningPortsHandler) handler(rw http.ResponseWriter, r *http.Request
47
72
Ports : ports ,
48
73
})
49
74
}
75
+
76
+ type logFile struct {
77
+ name codersdk.WorkspaceAgentLog
78
+ path string
79
+
80
+ mu sync.Mutex // Protects following.
81
+ lines int
82
+ offset int64
83
+ }
84
+
85
+ type logsHandler struct {
86
+ logFiles []* logFile
87
+ }
88
+
89
+ func (lh * logsHandler ) list (w http.ResponseWriter , r * http.Request ) {
90
+ ctx := r .Context ()
91
+ logs , ok := logFileInfo (w , r , lh .logFiles ... )
92
+ if ! ok {
93
+ return
94
+ }
95
+
96
+ httpapi .Write (ctx , w , http .StatusOK , logs )
97
+ }
98
+
99
+ func (lh * logsHandler ) info (w http.ResponseWriter , r * http.Request ) {
100
+ ctx := r .Context ()
101
+
102
+ logName := codersdk .WorkspaceAgentLog (chi .URLParam (r , "log" ))
103
+ if logName == "" {
104
+ httpapi .Write (ctx , w , http .StatusBadRequest , codersdk.Response {
105
+ Message : "Missing log URL parameter." ,
106
+ })
107
+ return
108
+ }
109
+
110
+ for _ , f := range lh .logFiles {
111
+ if f .name == logName {
112
+ logs , ok := logFileInfo (w , r , f )
113
+ if ! ok {
114
+ return
115
+ }
116
+
117
+ httpapi .Write (ctx , w , http .StatusOK , logs [0 ])
118
+ return
119
+ }
120
+ }
121
+
122
+ httpapi .Write (ctx , w , http .StatusNotFound , codersdk.Response {
123
+ Message : "Log file not found." ,
124
+ })
125
+ }
126
+
127
+ func (lh * logsHandler ) tail (w http.ResponseWriter , r * http.Request ) {
128
+ ctx := r .Context ()
129
+
130
+ logName := codersdk .WorkspaceAgentLog (chi .URLParam (r , "log" ))
131
+ if logName == "" {
132
+ httpapi .Write (ctx , w , http .StatusBadRequest , codersdk.Response {
133
+ Message : "Missing log URL parameter." ,
134
+ })
135
+ return
136
+ }
137
+
138
+ qp := r .URL .Query ()
139
+ parser := httpapi .NewQueryParamParser ()
140
+ offset := parser .Int (qp , 0 , "offset" )
141
+ limit := parser .Int (qp , 0 , "limit" )
142
+ if len (parser .Errors ) > 0 {
143
+ httpapi .Write (ctx , w , http .StatusBadRequest , codersdk.Response {
144
+ Message : "Query parameters have invalid values." ,
145
+ Validations : parser .Errors ,
146
+ })
147
+ return
148
+ }
149
+
150
+ var lf * logFile
151
+ for _ , f := range lh .logFiles {
152
+ if f .name == logName {
153
+ lf = f
154
+ break
155
+ }
156
+ }
157
+ if lf == nil {
158
+ httpapi .Write (ctx , w , http .StatusNotFound , codersdk.Response {
159
+ Message : "Log file not found." ,
160
+ })
161
+ return
162
+ }
163
+
164
+ f , err := os .Open (lf .path )
165
+ if err != nil {
166
+ if os .IsNotExist (err ) {
167
+ httpapi .Write (ctx , w , http .StatusNotFound , codersdk.Response {
168
+ Message : "Log file not found." ,
169
+ })
170
+ return
171
+ }
172
+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
173
+ Message : "Could not open log file." ,
174
+ Detail : err .Error (),
175
+ })
176
+ return
177
+ }
178
+ defer f .Close ()
179
+
180
+ var lines []string
181
+ fr := bufio .NewReader (f )
182
+ n := - 1
183
+ for {
184
+ b , err := fr .ReadBytes ('\n' )
185
+ if err != nil {
186
+ // Note, we skip incomplete lines with no newline.
187
+ if err == io .EOF {
188
+ break
189
+ }
190
+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
191
+ Message : "Could not read log file." ,
192
+ Detail : err .Error (),
193
+ })
194
+ return
195
+ }
196
+ n ++
197
+ if n < offset {
198
+ continue
199
+ }
200
+ b = bytes .TrimRight (b , "\r \n " )
201
+ lines = append (lines , string (b ))
202
+
203
+ if limit > 0 && len (lines ) >= limit {
204
+ break
205
+ }
206
+ }
207
+
208
+ httpapi .Write (ctx , w , http .StatusOK , codersdk.WorkspaceAgentLogTailResponse {
209
+ Offset : offset ,
210
+ Count : len (lines ),
211
+ Lines : lines ,
212
+ })
213
+ }
214
+
215
+ func logFileInfo (w http.ResponseWriter , r * http.Request , lf ... * logFile ) ([]codersdk.WorkspaceAgentLogInfo , bool ) {
216
+ ctx := r .Context ()
217
+
218
+ var logs []codersdk.WorkspaceAgentLogInfo
219
+ for _ , f := range lf {
220
+ size , lines , modified , exists , err := f .fileInfo ()
221
+ if err != nil {
222
+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
223
+ Message : "Could not gather log file info." ,
224
+ Detail : err .Error (),
225
+ })
226
+ return nil , false
227
+ }
228
+
229
+ logs = append (logs , codersdk.WorkspaceAgentLogInfo {
230
+ Name : f .name ,
231
+ Path : f .path ,
232
+ Size : size ,
233
+ Lines : lines ,
234
+ Exists : exists ,
235
+ Modified : modified ,
236
+ })
237
+ }
238
+
239
+ return logs , true
240
+ }
241
+
242
+ // fileInfo counts the number of lines in the log file and caches
243
+ // the logFile's line count and offset.
244
+ func (lf * logFile ) fileInfo () (size int64 , lines int , modified time.Time , exists bool , err error ) {
245
+ lf .mu .Lock ()
246
+ defer lf .mu .Unlock ()
247
+
248
+ f , err := os .Open (lf .path )
249
+ if err != nil {
250
+ if os .IsNotExist (err ) {
251
+ return 0 , 0 , time.Time {}, false , nil
252
+ }
253
+ return 0 , 0 , time.Time {}, false , err
254
+ }
255
+ defer f .Close ()
256
+
257
+ // Note, modified time will not be entirely accurate, but we rather
258
+ // give an old timestamp than one that is newer than when we counted
259
+ // the lines.
260
+ info , err := f .Stat ()
261
+ if err != nil {
262
+ return 0 , 0 , time.Time {}, false , err
263
+ }
264
+
265
+ _ , err = f .Seek (lf .offset , io .SeekStart )
266
+ if err != nil {
267
+ return 0 , 0 , time.Time {}, false , err
268
+ }
269
+
270
+ r := bufio .NewReader (f )
271
+ for {
272
+ b , err := r .ReadBytes ('\n' )
273
+ if err != nil {
274
+ // Note, we skip incomplete lines with no newline.
275
+ if err == io .EOF {
276
+ break
277
+ }
278
+ return 0 , 0 , time.Time {}, false , err
279
+ }
280
+ size += int64 (len (b ))
281
+ lines ++
282
+ }
283
+ lf .offset += size
284
+ lf .lines += lines
285
+
286
+ return lf .offset , lf .lines , info .ModTime (), true , nil
287
+ }
0 commit comments