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,206 @@ 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 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 not found." ,
160
+ })
161
+ return
162
+ }
163
+
164
+ f , err := os .Open (lf .path )
165
+ if err != nil {
166
+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
167
+ Message : "Could not open log file." ,
168
+ Detail : err .Error (),
169
+ })
170
+ return
171
+ }
172
+ defer f .Close ()
173
+
174
+ var lines []string
175
+ fr := bufio .NewReader (f )
176
+ n := - 1
177
+ for {
178
+ b , err := fr .ReadBytes ('\n' )
179
+ if err != nil {
180
+ // Note, we skip incomplete lines with no newline.
181
+ if err == io .EOF {
182
+ break
183
+ }
184
+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
185
+ Message : "Could not read log file." ,
186
+ Detail : err .Error (),
187
+ })
188
+ return
189
+ }
190
+ n ++
191
+ if n < int (offset ) {
192
+ continue
193
+ }
194
+ b = bytes .TrimRight (b , "\r \n " )
195
+ lines = append (lines , string (b ))
196
+
197
+ if limit > 0 && len (lines ) >= int (limit ) {
198
+ break
199
+ }
200
+ }
201
+
202
+ httpapi .Write (ctx , w , http .StatusOK , codersdk.WorkspaceAgentLogTailResponse {
203
+ Offset : int (offset ),
204
+ Count : len (lines ),
205
+ Lines : lines ,
206
+ })
207
+ }
208
+
209
+ func logFileInfo (w http.ResponseWriter , r * http.Request , lf ... * logFile ) ([]codersdk.WorkspaceAgentLogInfo , bool ) {
210
+ ctx := r .Context ()
211
+
212
+ var logs []codersdk.WorkspaceAgentLogInfo
213
+ for _ , f := range lf {
214
+ size , lines , modified , err := f .fileInfo ()
215
+ if err != nil {
216
+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
217
+ Message : "Could not gather log file info." ,
218
+ Detail : err .Error (),
219
+ })
220
+ return nil , false
221
+ }
222
+
223
+ logs = append (logs , codersdk.WorkspaceAgentLogInfo {
224
+ Name : f .name ,
225
+ Path : f .path ,
226
+ Size : size ,
227
+ Lines : lines ,
228
+ Modified : modified ,
229
+ })
230
+ }
231
+
232
+ return logs , true
233
+ }
234
+
235
+ // fileInfo counts the number of lines in the log file and caches
236
+ // the logFile's line count and offset.
237
+ func (lf * logFile ) fileInfo () (size int64 , lines int , modified time.Time , err error ) {
238
+ lf .mu .Lock ()
239
+ defer lf .mu .Unlock ()
240
+
241
+ f , err := os .Open (lf .path )
242
+ if err != nil {
243
+ return 0 , 0 , time.Time {}, err
244
+ }
245
+ defer f .Close ()
246
+
247
+ // Note, modified time will not be entirely accurate, but we rather
248
+ // give an old timestamp than one that is newer than when we counted
249
+ // the lines.
250
+ info , err := f .Stat ()
251
+ if err != nil {
252
+ return 0 , 0 , time.Time {}, err
253
+ }
254
+
255
+ _ , err = f .Seek (lf .offset , io .SeekStart )
256
+ if err != nil {
257
+ return 0 , 0 , time.Time {}, err
258
+ }
259
+
260
+ r := bufio .NewReader (f )
261
+ for {
262
+ b , err := r .ReadBytes ('\n' )
263
+ if err != nil {
264
+ // Note, we skip incomplete lines with no newline.
265
+ if err == io .EOF {
266
+ break
267
+ }
268
+ return 0 , 0 , time.Time {}, err
269
+ }
270
+ size += int64 (len (b ))
271
+ lines ++
272
+ }
273
+ lf .offset += size
274
+ lf .lines += lines
275
+
276
+ return lf .offset , lf .lines , info .ModTime (), nil
277
+ }
0 commit comments