Browse code

Timestamps for docker logs.

Fixes #1165
Docker-DCO-1.1-Signed-off-by: Alexandr Morozov <lk4d4math@gmail.com> (github: LK4D4)

Alexandr Morozov authored on 2014/04/03 04:26:06
Showing 10 changed files
... ...
@@ -1583,6 +1583,7 @@ func (cli *DockerCli) CmdDiff(args ...string) error {
1583 1583
 func (cli *DockerCli) CmdLogs(args ...string) error {
1584 1584
 	cmd := cli.Subcmd("logs", "CONTAINER", "Fetch the logs of a container")
1585 1585
 	follow := cmd.Bool([]string{"f", "-follow"}, false, "Follow log output")
1586
+	times := cmd.Bool([]string{"t", "-timestamps"}, false, "Show timestamps")
1586 1587
 	if err := cmd.Parse(args); err != nil {
1587 1588
 		return nil
1588 1589
 	}
... ...
@@ -1603,14 +1604,16 @@ func (cli *DockerCli) CmdLogs(args ...string) error {
1603 1603
 	}
1604 1604
 
1605 1605
 	v := url.Values{}
1606
-	v.Set("logs", "1")
1607 1606
 	v.Set("stdout", "1")
1608 1607
 	v.Set("stderr", "1")
1608
+	if *times {
1609
+		v.Set("timestamps", "1")
1610
+	}
1609 1611
 	if *follow && container.State.Running {
1610
-		v.Set("stream", "1")
1612
+		v.Set("follow", "1")
1611 1613
 	}
1612 1614
 
1613
-	if err := cli.hijack("POST", "/containers/"+name+"/attach?"+v.Encode(), container.Config.Tty, nil, cli.out, cli.err, nil); err != nil {
1615
+	if err := cli.streamHelper("GET", "/containers/"+name+"/logs?"+v.Encode(), container.Config.Tty, nil, cli.out, cli.err, nil); err != nil {
1614 1616
 		return err
1615 1617
 	}
1616 1618
 	return nil
... ...
@@ -130,6 +130,10 @@ func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo b
130 130
 }
131 131
 
132 132
 func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, headers map[string][]string) error {
133
+	return cli.streamHelper(method, path, true, in, out, nil, headers)
134
+}
135
+
136
+func (cli *DockerCli) streamHelper(method, path string, setRawTerminal bool, in io.Reader, stdout, stderr io.Writer, headers map[string][]string) error {
133 137
 	if (method == "POST" || method == "PUT") && in == nil {
134 138
 		in = bytes.NewReader([]byte{})
135 139
 	}
... ...
@@ -184,9 +188,16 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, h
184 184
 	}
185 185
 
186 186
 	if api.MatchesContentType(resp.Header.Get("Content-Type"), "application/json") {
187
-		return utils.DisplayJSONMessagesStream(resp.Body, out, cli.terminalFd, cli.isTerminal)
187
+		return utils.DisplayJSONMessagesStream(resp.Body, stdout, cli.terminalFd, cli.isTerminal)
188 188
 	}
189
-	if _, err := io.Copy(out, resp.Body); err != nil {
189
+	if stdout != nil || stderr != nil {
190
+		// When TTY is ON, use regular copy
191
+		if setRawTerminal {
192
+			_, err = io.Copy(stdout, resp.Body)
193
+		} else {
194
+			_, err = utils.StdCopy(stdout, stderr, resp.Body)
195
+		}
196
+		utils.Debugf("[stream] End of stdout")
190 197
 		return err
191 198
 	}
192 199
 	return nil
... ...
@@ -328,6 +328,48 @@ func getContainersJSON(eng *engine.Engine, version version.Version, w http.Respo
328 328
 	return nil
329 329
 }
330 330
 
331
+func getContainersLogs(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
332
+	if err := parseForm(r); err != nil {
333
+		return err
334
+	}
335
+	if vars == nil {
336
+		return fmt.Errorf("Missing parameter")
337
+	}
338
+
339
+	var (
340
+		job    = eng.Job("inspect", vars["name"], "container")
341
+		c, err = job.Stdout.AddEnv()
342
+	)
343
+	if err != nil {
344
+		return err
345
+	}
346
+	if err = job.Run(); err != nil {
347
+		return err
348
+	}
349
+
350
+	var outStream, errStream io.Writer
351
+	outStream = utils.NewWriteFlusher(w)
352
+
353
+	if c.GetSubEnv("Config") != nil && !c.GetSubEnv("Config").GetBool("Tty") && version.GreaterThanOrEqualTo("1.6") {
354
+		errStream = utils.NewStdWriter(outStream, utils.Stderr)
355
+		outStream = utils.NewStdWriter(outStream, utils.Stdout)
356
+	} else {
357
+		errStream = outStream
358
+	}
359
+
360
+	job = eng.Job("logs", vars["name"])
361
+	job.Setenv("follow", r.Form.Get("follow"))
362
+	job.Setenv("stdout", r.Form.Get("stdout"))
363
+	job.Setenv("stderr", r.Form.Get("stderr"))
364
+	job.Setenv("timestamps", r.Form.Get("timestamps"))
365
+	job.Stdout.Add(outStream)
366
+	job.Stderr.Set(errStream)
367
+	if err := job.Run(); err != nil {
368
+		fmt.Fprintf(outStream, "Error: %s\n", err)
369
+	}
370
+	return nil
371
+}
372
+
331 373
 func postImagesTag(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
332 374
 	if err := parseForm(r); err != nil {
333 375
 		return err
... ...
@@ -1017,6 +1059,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st
1017 1017
 			"/containers/{name:.*}/changes":   getContainersChanges,
1018 1018
 			"/containers/{name:.*}/json":      getContainersByName,
1019 1019
 			"/containers/{name:.*}/top":       getContainersTop,
1020
+			"/containers/{name:.*}/logs":      getContainersLogs,
1020 1021
 			"/containers/{name:.*}/attach/ws": wsContainersAttach,
1021 1022
 		},
1022 1023
 		"POST": {
... ...
@@ -473,6 +473,18 @@ func (container *Container) StderrPipe() (io.ReadCloser, error) {
473 473
 	return utils.NewBufReader(reader), nil
474 474
 }
475 475
 
476
+func (container *Container) StdoutLogPipe() io.ReadCloser {
477
+	reader, writer := io.Pipe()
478
+	container.stdout.AddWriter(writer, "stdout")
479
+	return utils.NewBufReader(reader)
480
+}
481
+
482
+func (container *Container) StderrLogPipe() io.ReadCloser {
483
+	reader, writer := io.Pipe()
484
+	container.stderr.AddWriter(writer, "stderr")
485
+	return utils.NewBufReader(reader)
486
+}
487
+
476 488
 func (container *Container) buildHostnameAndHostsFiles(IP string) {
477 489
 	container.HostnamePath = path.Join(container.root, "hostname")
478 490
 	ioutil.WriteFile(container.HostnamePath, []byte(container.Config.Hostname+"\n"), 0644)
... ...
@@ -45,6 +45,10 @@ You can still call an old version of the api using
45 45
 You can now use the `-until` parameter to close connection
46 46
 after timestamp.
47 47
 
48
+`GET /containers/(id)/logs`
49
+
50
+This url is prefered method for getting container logs now.
51
+
48 52
 ### v1.10
49 53
 
50 54
 #### Full Documentation
... ...
@@ -300,6 +300,42 @@ List processes running inside the container `id`
300 300
     -   **404** – no such container
301 301
     -   **500** – server error
302 302
 
303
+### Get container logs
304
+
305
+`GET /containers/(id)/logs`
306
+
307
+Get stdout and stderr logs from the container ``id``
308
+
309
+    **Example request**:
310
+
311
+       GET /containers/4fa6e0f0c678/logs?stderr=1&stdout=1&timestamps=1&follow=1 HTTP/1.1
312
+
313
+    **Example response**:
314
+
315
+       HTTP/1.1 200 OK
316
+       Content-Type: application/vnd.docker.raw-stream
317
+
318
+       {{ STREAM }}
319
+
320
+    Query Parameters:
321
+
322
+     
323
+
324
+    -   **follow** – 1/True/true or 0/False/false, return stream.
325
+        Default false
326
+    -   **stdout** – 1/True/true or 0/False/false, if logs=true, return
327
+        stdout log. Default false
328
+    -   **stderr** – 1/True/true or 0/False/false, if logs=true, return
329
+        stderr log. Default false
330
+    -   **timestamps** – 1/True/true or 0/False/false, if logs=true, print
331
+        timestamps for every log line. Default false
332
+
333
+    Status Codes:
334
+
335
+    -   **200** – no error
336
+    -   **404** – no such container
337
+    -   **500** – server error
338
+
303 339
 ### Inspect changes on a container's filesystem
304 340
 
305 341
 `GET /containers/(id)/changes`
... ...
@@ -649,13 +649,14 @@ Fetch the logs of a container
649 649
     Usage: docker logs [OPTIONS] CONTAINER
650 650
 
651 651
     -f, --follow=false: Follow log output
652
+    -t, --timestamps=false: Show timestamps
652 653
 
653 654
 The `docker logs` command batch-retrieves all logs
654 655
 present at the time of execution.
655 656
 
656
-The `docker logs --follow` command combines `docker logs` and `docker
657
-attach`: it will first return all logs from the beginning and then
658
-continue streaming new output from the container'sstdout and stderr.
657
+The ``docker logs --follow`` command will first return all logs from the
658
+beginning and then continue streaming new output from the container's stdout
659
+and stderr.
659 660
 
660 661
 ## port
661 662
 
... ...
@@ -3,7 +3,10 @@ package main
3 3
 import (
4 4
 	"fmt"
5 5
 	"os/exec"
6
+	"regexp"
7
+	"strings"
6 8
 	"testing"
9
+	"time"
7 10
 )
8 11
 
9 12
 // This used to work, it test a log of PageSize-1 (gh#4851)
... ...
@@ -74,3 +77,95 @@ func TestLogsContainerMuchBiggerThanPage(t *testing.T) {
74 74
 
75 75
 	logDone("logs - logs container running echo much bigger than page size")
76 76
 }
77
+
78
+func TestLogsTimestamps(t *testing.T) {
79
+	testLen := 100
80
+	runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("for i in $(seq 1 %d); do echo =; done;", testLen))
81
+
82
+	out, _, _, err := runCommandWithStdoutStderr(runCmd)
83
+	errorOut(err, t, fmt.Sprintf("run failed with errors: %v", err))
84
+
85
+	cleanedContainerID := stripTrailingCharacters(out)
86
+	exec.Command(dockerBinary, "wait", cleanedContainerID).Run()
87
+
88
+	logsCmd := exec.Command(dockerBinary, "logs", "-t", cleanedContainerID)
89
+	out, _, _, err = runCommandWithStdoutStderr(logsCmd)
90
+	errorOut(err, t, fmt.Sprintf("failed to log container: %v %v", out, err))
91
+
92
+	lines := strings.Split(out, "\n")
93
+
94
+	if len(lines) != testLen+1 {
95
+		t.Fatalf("Expected log %d lines, received %d\n", testLen+1, len(lines))
96
+	}
97
+
98
+	ts := regexp.MustCompile(`^\[.*?\]`)
99
+
100
+	for _, l := range lines {
101
+		if l != "" {
102
+			_, err := time.Parse("["+time.StampMilli+"]", ts.FindString(l))
103
+			if err != nil {
104
+				t.Fatalf("Failed to parse timestamp from %v: %v", l, err)
105
+			}
106
+		}
107
+	}
108
+
109
+	deleteContainer(cleanedContainerID)
110
+
111
+	logDone("logs - logs with timestamps")
112
+}
113
+
114
+func TestLogsSeparateStderr(t *testing.T) {
115
+	msg := "stderr_log"
116
+	runCmd := exec.Command(dockerBinary, "run", "-d", "busybox", "sh", "-c", fmt.Sprintf("echo %s 1>&2", msg))
117
+
118
+	out, _, _, err := runCommandWithStdoutStderr(runCmd)
119
+	errorOut(err, t, fmt.Sprintf("run failed with errors: %v", err))
120
+
121
+	cleanedContainerID := stripTrailingCharacters(out)
122
+	exec.Command(dockerBinary, "wait", cleanedContainerID).Run()
123
+
124
+	logsCmd := exec.Command(dockerBinary, "logs", cleanedContainerID)
125
+	stdout, stderr, _, err := runCommandWithStdoutStderr(logsCmd)
126
+	errorOut(err, t, fmt.Sprintf("failed to log container: %v %v", out, err))
127
+
128
+	if stdout != "" {
129
+		t.Fatalf("Expected empty stdout stream, got %v", stdout)
130
+	}
131
+
132
+	stderr = strings.TrimSpace(stderr)
133
+	if stderr != msg {
134
+		t.Fatalf("Expected %v in stderr stream, got %v", msg, stderr)
135
+	}
136
+
137
+	deleteContainer(cleanedContainerID)
138
+
139
+	logDone("logs - separate stderr (without pseudo-tty)")
140
+}
141
+
142
+func TestLogsStderrInStdout(t *testing.T) {
143
+	msg := "stderr_log"
144
+	runCmd := exec.Command(dockerBinary, "run", "-d", "-t", "busybox", "sh", "-c", fmt.Sprintf("echo %s 1>&2", msg))
145
+
146
+	out, _, _, err := runCommandWithStdoutStderr(runCmd)
147
+	errorOut(err, t, fmt.Sprintf("run failed with errors: %v", err))
148
+
149
+	cleanedContainerID := stripTrailingCharacters(out)
150
+	exec.Command(dockerBinary, "wait", cleanedContainerID).Run()
151
+
152
+	logsCmd := exec.Command(dockerBinary, "logs", cleanedContainerID)
153
+	stdout, stderr, _, err := runCommandWithStdoutStderr(logsCmd)
154
+	errorOut(err, t, fmt.Sprintf("failed to log container: %v %v", out, err))
155
+
156
+	if stderr != "" {
157
+		t.Fatalf("Expected empty stderr stream, got %v", stdout)
158
+	}
159
+
160
+	stdout = strings.TrimSpace(stdout)
161
+	if stdout != msg {
162
+		t.Fatalf("Expected %v in stdout stream, got %v", msg, stdout)
163
+	}
164
+
165
+	deleteContainer(cleanedContainerID)
166
+
167
+	logDone("logs - stderr in stdout (with pseudo-tty)")
168
+}
... ...
@@ -124,6 +124,7 @@ func InitServer(job *engine.Job) engine.Status {
124 124
 		"container_copy":   srv.ContainerCopy,
125 125
 		"insert":           srv.ImageInsert,
126 126
 		"attach":           srv.ContainerAttach,
127
+		"logs":             srv.ContainerLogs,
127 128
 		"search":           srv.ImagesSearch,
128 129
 		"changes":          srv.ContainerChanges,
129 130
 		"top":              srv.ContainerTop,
... ...
@@ -2252,6 +2253,96 @@ func (srv *Server) ContainerResize(job *engine.Job) engine.Status {
2252 2252
 	return job.Errorf("No such container: %s", name)
2253 2253
 }
2254 2254
 
2255
+func (srv *Server) ContainerLogs(job *engine.Job) engine.Status {
2256
+	if len(job.Args) != 1 {
2257
+		return job.Errorf("Usage: %s CONTAINER\n", job.Name)
2258
+	}
2259
+
2260
+	var (
2261
+		name   = job.Args[0]
2262
+		stdout = job.GetenvBool("stdout")
2263
+		stderr = job.GetenvBool("stderr")
2264
+		follow = job.GetenvBool("follow")
2265
+		times  = job.GetenvBool("timestamps")
2266
+		format string
2267
+	)
2268
+	if !(stdout || stderr) {
2269
+		return job.Errorf("You must choose at least one stream")
2270
+	}
2271
+	if times {
2272
+		format = time.StampMilli
2273
+	}
2274
+	container := srv.daemon.Get(name)
2275
+	if container == nil {
2276
+		return job.Errorf("No such container: %s", name)
2277
+	}
2278
+	cLog, err := container.ReadLog("json")
2279
+	if err != nil && os.IsNotExist(err) {
2280
+		// Legacy logs
2281
+		utils.Debugf("Old logs format")
2282
+		if stdout {
2283
+			cLog, err := container.ReadLog("stdout")
2284
+			if err != nil {
2285
+				utils.Errorf("Error reading logs (stdout): %s", err)
2286
+			} else if _, err := io.Copy(job.Stdout, cLog); err != nil {
2287
+				utils.Errorf("Error streaming logs (stdout): %s", err)
2288
+			}
2289
+		}
2290
+		if stderr {
2291
+			cLog, err := container.ReadLog("stderr")
2292
+			if err != nil {
2293
+				utils.Errorf("Error reading logs (stderr): %s", err)
2294
+			} else if _, err := io.Copy(job.Stderr, cLog); err != nil {
2295
+				utils.Errorf("Error streaming logs (stderr): %s", err)
2296
+			}
2297
+		}
2298
+	} else if err != nil {
2299
+		utils.Errorf("Error reading logs (json): %s", err)
2300
+	} else {
2301
+		dec := json.NewDecoder(cLog)
2302
+		for {
2303
+			l := &utils.JSONLog{}
2304
+
2305
+			if err := dec.Decode(l); err == io.EOF {
2306
+				break
2307
+			} else if err != nil {
2308
+				utils.Errorf("Error streaming logs: %s", err)
2309
+				break
2310
+			}
2311
+			logLine := l.Log
2312
+			if times {
2313
+				logLine = fmt.Sprintf("[%s] %s", l.Created.Format(format), logLine)
2314
+			}
2315
+			if l.Stream == "stdout" && stdout {
2316
+				fmt.Fprintf(job.Stdout, "%s", logLine)
2317
+			}
2318
+			if l.Stream == "stderr" && stderr {
2319
+				fmt.Fprintf(job.Stderr, "%s", logLine)
2320
+			}
2321
+		}
2322
+	}
2323
+	if follow {
2324
+		errors := make(chan error, 2)
2325
+		if stdout {
2326
+			stdoutPipe := container.StdoutLogPipe()
2327
+			go func() {
2328
+				errors <- utils.WriteLog(stdoutPipe, job.Stdout, format)
2329
+			}()
2330
+		}
2331
+		if stderr {
2332
+			stderrPipe := container.StderrLogPipe()
2333
+			go func() {
2334
+				errors <- utils.WriteLog(stderrPipe, job.Stderr, format)
2335
+			}()
2336
+		}
2337
+		err := <-errors
2338
+		if err != nil {
2339
+			utils.Errorf("%s", err)
2340
+		}
2341
+	}
2342
+	return engine.StatusOK
2343
+}
2344
+
2255 2345
 func (srv *Server) ContainerAttach(job *engine.Job) engine.Status {
2256 2346
 	if len(job.Args) != 1 {
2257 2347
 		return job.Errorf("Usage: %s CONTAINER\n", job.Name)
... ...
@@ -341,18 +341,15 @@ func (r *bufReader) Close() error {
341 341
 type WriteBroadcaster struct {
342 342
 	sync.Mutex
343 343
 	buf     *bytes.Buffer
344
-	writers map[StreamWriter]bool
345
-}
346
-
347
-type StreamWriter struct {
348
-	wc     io.WriteCloser
349
-	stream string
344
+	streams map[string](map[io.WriteCloser]struct{})
350 345
 }
351 346
 
352 347
 func (w *WriteBroadcaster) AddWriter(writer io.WriteCloser, stream string) {
353 348
 	w.Lock()
354
-	sw := StreamWriter{wc: writer, stream: stream}
355
-	w.writers[sw] = true
349
+	if _, ok := w.streams[stream]; !ok {
350
+		w.streams[stream] = make(map[io.WriteCloser]struct{})
351
+	}
352
+	w.streams[stream][writer] = struct{}{}
356 353
 	w.Unlock()
357 354
 }
358 355
 
... ...
@@ -362,33 +359,83 @@ type JSONLog struct {
362 362
 	Created time.Time `json:"time"`
363 363
 }
364 364
 
365
+func (jl *JSONLog) Format(format string) (string, error) {
366
+	if format == "" {
367
+		return jl.Log, nil
368
+	}
369
+	if format == "json" {
370
+		m, err := json.Marshal(jl)
371
+		return string(m), err
372
+	}
373
+	return fmt.Sprintf("[%s] %s", jl.Created.Format(format), jl.Log), nil
374
+}
375
+
376
+func WriteLog(src io.Reader, dst io.WriteCloser, format string) error {
377
+	dec := json.NewDecoder(src)
378
+	for {
379
+		l := &JSONLog{}
380
+
381
+		if err := dec.Decode(l); err == io.EOF {
382
+			return nil
383
+		} else if err != nil {
384
+			Errorf("Error streaming logs: %s", err)
385
+			return err
386
+		}
387
+		line, err := l.Format(format)
388
+		if err != nil {
389
+			return err
390
+		}
391
+		fmt.Fprintf(dst, "%s", line)
392
+	}
393
+}
394
+
395
+type LogFormatter struct {
396
+	wc         io.WriteCloser
397
+	timeFormat string
398
+}
399
+
365 400
 func (w *WriteBroadcaster) Write(p []byte) (n int, err error) {
401
+	created := time.Now().UTC()
366 402
 	w.Lock()
367 403
 	defer w.Unlock()
404
+	if writers, ok := w.streams[""]; ok {
405
+		for sw := range writers {
406
+			if n, err := sw.Write(p); err != nil || n != len(p) {
407
+				// On error, evict the writer
408
+				delete(writers, sw)
409
+			}
410
+		}
411
+	}
368 412
 	w.buf.Write(p)
369
-	for sw := range w.writers {
370
-		lp := p
371
-		if sw.stream != "" {
372
-			lp = nil
373
-			for {
374
-				line, err := w.buf.ReadString('\n')
375
-				if err != nil {
376
-					w.buf.Write([]byte(line))
377
-					break
378
-				}
379
-				b, err := json.Marshal(&JSONLog{Log: line, Stream: sw.stream, Created: time.Now().UTC()})
413
+	lines := []string{}
414
+	for {
415
+		line, err := w.buf.ReadString('\n')
416
+		if err != nil {
417
+			w.buf.Write([]byte(line))
418
+			break
419
+		}
420
+		lines = append(lines, line)
421
+	}
422
+
423
+	if len(lines) != 0 {
424
+		for stream, writers := range w.streams {
425
+			if stream == "" {
426
+				continue
427
+			}
428
+			var lp []byte
429
+			for _, line := range lines {
430
+				b, err := json.Marshal(&JSONLog{Log: line, Stream: stream, Created: created})
380 431
 				if err != nil {
381
-					// On error, evict the writer
382
-					delete(w.writers, sw)
383
-					continue
432
+					Errorf("Error making JSON log line: %s", err)
384 433
 				}
385 434
 				lp = append(lp, b...)
386 435
 				lp = append(lp, '\n')
387 436
 			}
388
-		}
389
-		if n, err := sw.wc.Write(lp); err != nil || n != len(lp) {
390
-			// On error, evict the writer
391
-			delete(w.writers, sw)
437
+			for sw := range writers {
438
+				if _, err := sw.Write(lp); err != nil {
439
+					delete(writers, sw)
440
+				}
441
+			}
392 442
 		}
393 443
 	}
394 444
 	return len(p), nil
... ...
@@ -397,15 +444,20 @@ func (w *WriteBroadcaster) Write(p []byte) (n int, err error) {
397 397
 func (w *WriteBroadcaster) CloseWriters() error {
398 398
 	w.Lock()
399 399
 	defer w.Unlock()
400
-	for sw := range w.writers {
401
-		sw.wc.Close()
400
+	for _, writers := range w.streams {
401
+		for w := range writers {
402
+			w.Close()
403
+		}
402 404
 	}
403
-	w.writers = make(map[StreamWriter]bool)
405
+	w.streams = make(map[string](map[io.WriteCloser]struct{}))
404 406
 	return nil
405 407
 }
406 408
 
407 409
 func NewWriteBroadcaster() *WriteBroadcaster {
408
-	return &WriteBroadcaster{writers: make(map[StreamWriter]bool), buf: bytes.NewBuffer(nil)}
410
+	return &WriteBroadcaster{
411
+		streams: make(map[string](map[io.WriteCloser]struct{})),
412
+		buf:     bytes.NewBuffer(nil),
413
+	}
409 414
 }
410 415
 
411 416
 func GetTotalUsedFds() int {