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