1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-21 21:33:18 +01:00

Makes changes to show all containers and event streams (#448)

This commit is contained in:
Amir Raminfar
2020-05-11 17:05:21 -07:00
committed by GitHub
parent 825491fb06
commit ca20f7c0a3
14 changed files with 79 additions and 58 deletions

View File

@@ -66,7 +66,7 @@ event: containers-changed
data: start data: start
/* snapshot: Test_handler_streamLogs_error_finding_container */ /* snapshot: Test_handler_streamLogs_error_finding_container */
HTTP/1.1 500 Internal Server Error HTTP/1.1 404 Not Found
Connection: close Connection: close
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff X-Content-Type-Options: nosniff

View File

@@ -134,7 +134,7 @@ describe("<LogEventSource />", () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(` expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium"> <ul class="events medium">
<li><span class="date">today at 10:55 AM</span> <span class="text">"This is a message."</span></li> <li class=""><span class="date">today at 10:55 AM</span> <span class="text">"This is a message."</span></li>
</ul> </ul>
`); `);
}); });
@@ -149,7 +149,7 @@ describe("<LogEventSource />", () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(` expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium"> <ul class="events medium">
<li><span class="date">today at 10:55 AM</span> <span class="text"><span style="color:#000">black<span style="color:#AAA">white</span></span></span></li> <li class=""><span class="date">today at 10:55 AM</span> <span class="text"><span style="color:#000">black<span style="color:#AAA">white</span></span></span></li>
</ul> </ul>
`); `);
}); });
@@ -164,7 +164,7 @@ describe("<LogEventSource />", () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(` expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium"> <ul class="events medium">
<li><span class="date">today at 10:55 AM</span> <span class="text">&lt;test&gt;foo bar&lt;/test&gt;</span></li> <li class=""><span class="date">today at 10:55 AM</span> <span class="text">&lt;test&gt;foo bar&lt;/test&gt;</span></li>
</ul> </ul>
`); `);
}); });
@@ -182,7 +182,7 @@ describe("<LogEventSource />", () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(` expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<ul class="events medium"> <ul class="events medium">
<li><span class="date">today at 10:55 AM</span> <span class="text">This is a <mark>test</mark> &lt;hi&gt;&lt;/hi&gt;</span></li> <li class=""><span class="date">today at 10:55 AM</span> <span class="text">This is a <mark>test</mark> &lt;hi&gt;&lt;/hi&gt;</span></li>
</ul> </ul>
`); `);
}); });

View File

@@ -35,19 +35,23 @@ export default {
this.es = null; this.es = null;
} }
this.es = new EventSource(`${config.base}/api/logs/stream?id=${this.id}`); this.es = new EventSource(`${config.base}/api/logs/stream?id=${this.id}`);
const flushBuffer = debounce(
() => { this.es.addEventListener("container-stopped", (e) => {
this.es.close();
this.buffer.push({ event: "container-stopped", message: "Container stopped", date: new Date() });
flushNow();
});
this.es.addEventListener("error", (e) => console.log("EventSource failed: " + JSON.stringify(e)));
const flushBuffer = debounce(() => flushNow(), 250, { maxWait: 1000 });
const flushNow = () => {
this.messages.push(...this.buffer); this.messages.push(...this.buffer);
this.buffer = []; this.buffer = [];
}, };
250,
{ maxWait: 1000 }
);
this.es.onmessage = (e) => { this.es.onmessage = (e) => {
this.buffer.push(this.parseMessage(e.data)); this.buffer.push(this.parseMessage(e.data));
flushBuffer(); flushBuffer();
}; };
this.es.onerror = (e) => console.log("EventSource failed." + e);
this.$once("hook:beforeDestroy", () => this.es.close()); this.$once("hook:beforeDestroy", () => this.es.close());
}, },
async loadOlderLogs() { async loadOlderLogs() {

View File

@@ -1,6 +1,6 @@
<template lang="html"> <template lang="html">
<ul class="events" :class="settings.size"> <ul class="events" :class="settings.size">
<li v-for="item in filtered" :key="item.key"> <li v-for="item in filtered" :key="item.key" :class="{ event: !!item.event }">
<span class="date" v-if="settings.showTimestamp">{{ item.date | relativeTime }}</span> <span class="date" v-if="settings.showTimestamp">{{ item.date | relativeTime }}</span>
<span class="text" v-html="colorize(item.message)"></span> <span class="text" v-html="colorize(item.message)"></span>
</li> </li>
@@ -103,6 +103,10 @@ export default {
white-space: pre-wrap; white-space: pre-wrap;
} }
li.event {
color: #f14668;
}
::v-deep mark { ::v-deep mark {
border-radius: 2px; border-radius: 2px;
background-color: #ffdd57; background-color: #ffdd57;

View File

@@ -27,7 +27,7 @@
</template> </template>
<script> <script>
import { mapActions, mapGetters, mapState } from "vuex"; import { mapGetters } from "vuex";
export default { export default {
props: [], props: [],
@@ -37,13 +37,8 @@ export default {
showNav: false, showNav: false,
}; };
}, },
computed: { computed: {
...mapState(["containers"]), ...mapGetters(["activeContainersById", "visibleContainers"]),
...mapGetters(["activeContainersById"]),
},
methods: {
...mapActions({}),
}, },
watch: { watch: {
$route(to, from) { $route(to, from) {

View File

@@ -18,7 +18,7 @@
</div> </div>
<p class="menu-label is-hidden-mobile">Containers</p> <p class="menu-label is-hidden-mobile">Containers</p>
<ul class="menu-list is-hidden-mobile"> <ul class="menu-list is-hidden-mobile">
<li v-for="item in containers"> <li v-for="item in visibleContainers" :key="item.id" :class="item.state">
<router-link <router-link
:to="{ name: 'container', params: { id: item.id, name: item.name } }" :to="{ name: 'container', params: { id: item.id, name: item.name } }"
active-class="is-active" active-class="is-active"
@@ -55,8 +55,8 @@ export default {
return {}; return {};
}, },
computed: { computed: {
...mapState(["containers", "activeContainers"]), ...mapState(["activeContainers"]),
...mapGetters(["activeContainersById"]), ...mapGetters(["activeContainersById", "visibleContainers"]),
}, },
methods: { methods: {
...mapActions({ ...mapActions({
@@ -88,6 +88,10 @@ aside {
} }
} }
li.exited a {
color: #777;
}
.will-append-container.icon { .will-append-container.icon {
transition: transform 0.2s ease-out; transition: transform 0.2s ease-out;
&.is-active { &.is-active {

View File

@@ -37,6 +37,12 @@
</b-switch> </b-switch>
</div> </div>
<div class="item">
<b-switch v-model="showAllContainers">
Show stopped containers
</b-switch>
</div>
<div class="item"> <div class="item">
<h2 class="title is-6 is-marginless">Font size</h2> <h2 class="title is-6 is-marginless">Font size</h2>
Modify the font size when viewing logs. Modify the font size when viewing logs.
@@ -104,7 +110,7 @@ export default {
}, },
computed: { computed: {
...mapState(["settings"]), ...mapState(["settings"]),
...["search", "size", "smallerScrollbars", "showTimestamp"].reduce((map, name) => { ...["search", "size", "smallerScrollbars", "showTimestamp", "showAllContainers"].reduce((map, name) => {
map[name] = { map[name] = {
get() { get() {
return this.settings[name]; return this.settings[name];

View File

@@ -59,18 +59,22 @@ const actions = {
}, },
}; };
const getters = { const getters = {
activeContainersById(state) { activeContainersById({ activeContainers }) {
return state.activeContainers.reduce((map, obj) => { return activeContainers.reduce((map, obj) => {
map[obj.id] = obj; map[obj.id] = obj;
return map; return map;
}, {}); }, {});
}, },
allContainersById(state) { allContainersById({ containers }) {
return state.containers.reduce((map, obj) => { return containers.reduce((map, obj) => {
map[obj.id] = obj; map[obj.id] = obj;
return map; return map;
}, {}); }, {});
}, },
visibleContainers({ containers, settings: { showAllContainers } }) {
const filter = showAllContainers ? () => true : (c) => c.state === "running";
return containers.filter(filter);
},
}; };
const es = new EventSource(`${config.base}/api/events/stream`); const es = new EventSource(`${config.base}/api/events/stream`);

View File

@@ -5,4 +5,5 @@ export const DEFAULT_SETTINGS = {
menuWidth: 15, menuWidth: 15,
smallerScrollbars: false, smallerScrollbars: false,
showTimestamp: true, showTimestamp: true,
showAllContainers: false,
}; };

View File

@@ -33,7 +33,7 @@ type dockerProxy interface {
// Client is a proxy around the docker client // Client is a proxy around the docker client
type Client interface { type Client interface {
ListContainers(showAll bool) ([]Container, error) ListContainers() ([]Container, error)
FindContainer(string) (Container, error) FindContainer(string) (Container, error)
ContainerLogs(context.Context, string, int) (<-chan string, <-chan error) ContainerLogs(context.Context, string, int) (<-chan string, <-chan error)
Events(context.Context) (<-chan events.Message, <-chan error) Events(context.Context) (<-chan events.Message, <-chan error)
@@ -65,7 +65,7 @@ func NewClientWithFilters(f map[string]string) Client {
func (d *dockerClient) FindContainer(id string) (Container, error) { func (d *dockerClient) FindContainer(id string) (Container, error) {
var container Container var container Container
containers, err := d.ListContainers(true) containers, err := d.ListContainers()
if err != nil { if err != nil {
return container, err return container, err
} }
@@ -85,10 +85,10 @@ func (d *dockerClient) FindContainer(id string) (Container, error) {
return container, nil return container, nil
} }
func (d *dockerClient) ListContainers(showAll bool) ([]Container, error) { func (d *dockerClient) ListContainers() ([]Container, error) {
containerListOptions := types.ContainerListOptions{ containerListOptions := types.ContainerListOptions{
Filters: d.filters, Filters: d.filters,
All: showAll, All: true,
} }
list, err := d.cli.ContainerList(context.Background(), containerListOptions) list, err := d.cli.ContainerList(context.Background(), containerListOptions)
if err != nil { if err != nil {

View File

@@ -55,7 +55,7 @@ func Test_dockerClient_ListContainers_null(t *testing.T) {
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil)
client := &dockerClient{proxy, filters.NewArgs()} client := &dockerClient{proxy, filters.NewArgs()}
list, err := client.ListContainers(true) list, err := client.ListContainers()
assert.Empty(t, list, "list should be empty") assert.Empty(t, list, "list should be empty")
require.NoError(t, err, "error should not return an error.") require.NoError(t, err, "error should not return an error.")
@@ -67,7 +67,7 @@ func Test_dockerClient_ListContainers_error(t *testing.T) {
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test")) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test"))
client := &dockerClient{proxy, filters.NewArgs()} client := &dockerClient{proxy, filters.NewArgs()}
list, err := client.ListContainers(true) list, err := client.ListContainers()
assert.Nil(t, list, "list should be nil") assert.Nil(t, list, "list should be nil")
require.Error(t, err, "test.") require.Error(t, err, "test.")
@@ -90,7 +90,7 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) {
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
client := &dockerClient{proxy, filters.NewArgs()} client := &dockerClient{proxy, filters.NewArgs()}
list, err := client.ListContainers(true) list, err := client.ListContainers()
require.NoError(t, err, "error should not return an error.") require.NoError(t, err, "error should not return an error.")
assert.Equal(t, list, []Container{ assert.Equal(t, list, []Container{

10
main.go
View File

@@ -28,14 +28,12 @@ var (
type handler struct { type handler struct {
client docker.Client client docker.Client
showAll bool
box packr.Box box packr.Box
} }
func init() { func init() {
pflag.String("addr", ":8080", "http service address") pflag.String("addr", ":8080", "http service address")
pflag.String("base", "/", "base address of the application to mount") pflag.String("base", "/", "base address of the application to mount")
pflag.Bool("showAll", false, "show all containers, even stopped")
pflag.String("level", "info", "logging level") pflag.String("level", "info", "logging level")
pflag.Int("tailSize", 300, "Tail size to use for initial container logs") pflag.Int("tailSize", 300, "Tail size to use for initial container logs")
pflag.StringToStringVar(&filters, "filter", map[string]string{}, "Container filters to use for showing logs") pflag.StringToStringVar(&filters, "filter", map[string]string{}, "Container filters to use for showing logs")
@@ -49,10 +47,9 @@ func init() {
base = viper.GetString("base") base = viper.GetString("base")
level = viper.GetString("level") level = viper.GetString("level")
tailSize = viper.GetInt("tailSize") tailSize = viper.GetInt("tailSize")
showAll = viper.GetBool("showAll")
// Until https://github.com/spf13/viper/issues/608 is fixed. We have to use this hacky way. // Until https://github.com/spf13/viper/issues/911 is fixed. We have to use this hacky way.
// filters = viper.GetStringSlice("filter") // filters = viper.GetStringMapString("filter")
if value, ok := os.LookupEnv("DOZZLE_FILTER"); ok { if value, ok := os.LookupEnv("DOZZLE_FILTER"); ok {
log.Infof("Parsing %s", value) log.Infof("Parsing %s", value)
urlValues, err := url.ParseQuery(strings.ReplaceAll(value, ",", "&")) urlValues, err := url.ParseQuery(strings.ReplaceAll(value, ",", "&"))
@@ -77,7 +74,7 @@ func init() {
func main() { func main() {
log.Infof("Dozzle version %s", version) log.Infof("Dozzle version %s", version)
dockerClient := docker.NewClientWithFilters(filters) dockerClient := docker.NewClientWithFilters(filters)
_, err := dockerClient.ListContainers(true) _, err := dockerClient.ListContainers()
if err != nil { if err != nil {
log.Fatalf("Could not connect to Docker Engine: %v", err) log.Fatalf("Could not connect to Docker Engine: %v", err)
@@ -86,7 +83,6 @@ func main() {
box := packr.NewBox("./static") box := packr.NewBox("./static")
r := createRoutes(base, &handler{ r := createRoutes(base, &handler{
client: dockerClient, client: dockerClient,
showAll: showAll,
box: box, box: box,
}) })
srv := &http.Server{Addr: addr, Handler: r} srv := &http.Server{Addr: addr, Handler: r}

View File

@@ -32,7 +32,7 @@ func (m *MockedClient) FindContainer(id string) (docker.Container, error) {
return container, args.Error(1) return container, args.Error(1)
} }
func (m *MockedClient) ListContainers(showAll bool) ([]docker.Container, error) { func (m *MockedClient) ListContainers() ([]docker.Container, error) {
args := m.Called() args := m.Called()
containers, ok := args.Get(0).([]docker.Container) containers, ok := args.Get(0).([]docker.Container)
if !ok { if !ok {
@@ -246,7 +246,7 @@ func Test_createRoutes_index(t *testing.T) {
box := packr.NewBox("./virtual") box := packr.NewBox("./virtual")
require.NoError(t, box.AddString("index.html", "index page"), "AddString should have no error.") require.NoError(t, box.AddString("index.html", "index page"), "AddString should have no error.")
handler := createRoutes("/", &handler{mockedClient, true, box}) handler := createRoutes("/", &handler{mockedClient, box})
req, err := http.NewRequest("GET", "/", nil) req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@@ -259,7 +259,7 @@ func Test_createRoutes_redirect(t *testing.T) {
mockedClient := new(MockedClient) mockedClient := new(MockedClient)
box := packr.NewBox("./virtual") box := packr.NewBox("./virtual")
handler := createRoutes("/foobar", &handler{mockedClient, true, box}) handler := createRoutes("/foobar", &handler{mockedClient, box})
req, err := http.NewRequest("GET", "/foobar", nil) req, err := http.NewRequest("GET", "/foobar", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@@ -273,7 +273,7 @@ func Test_createRoutes_foobar(t *testing.T) {
box := packr.NewBox("./virtual") box := packr.NewBox("./virtual")
require.NoError(t, box.AddString("index.html", "foo page"), "AddString should have no error.") require.NoError(t, box.AddString("index.html", "foo page"), "AddString should have no error.")
handler := createRoutes("/foobar", &handler{mockedClient, true, box}) handler := createRoutes("/foobar", &handler{mockedClient, box})
req, err := http.NewRequest("GET", "/foobar/", nil) req, err := http.NewRequest("GET", "/foobar/", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@@ -287,7 +287,7 @@ func Test_createRoutes_foobar_file(t *testing.T) {
box := packr.NewBox("./virtual") box := packr.NewBox("./virtual")
require.NoError(t, box.AddString("/test", "test page"), "AddString should have no error.") require.NoError(t, box.AddString("/test", "test page"), "AddString should have no error.")
handler := createRoutes("/foobar", &handler{mockedClient, true, box}) handler := createRoutes("/foobar", &handler{mockedClient, box})
req, err := http.NewRequest("GET", "/foobar/test", nil) req, err := http.NewRequest("GET", "/foobar/test", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@@ -300,7 +300,7 @@ func Test_createRoutes_version(t *testing.T) {
mockedClient := new(MockedClient) mockedClient := new(MockedClient)
box := packr.NewBox("./virtual") box := packr.NewBox("./virtual")
handler := createRoutes("/", &handler{mockedClient, true, box}) handler := createRoutes("/", &handler{mockedClient, box})
req, err := http.NewRequest("GET", "/version", nil) req, err := http.NewRequest("GET", "/version", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"io"
"net/http" "net/http"
"runtime" "runtime"
"time" "time"
@@ -68,7 +69,7 @@ func (h *handler) index(w http.ResponseWriter, req *http.Request) {
} }
func (h *handler) listContainers(w http.ResponseWriter, r *http.Request) { func (h *handler) listContainers(w http.ResponseWriter, r *http.Request) {
containers, err := h.client.ListContainers(h.showAll) containers, err := h.client.ListContainers()
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@@ -109,7 +110,7 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
container, e := h.client.FindContainer(id) container, e := h.client.FindContainer(id)
if e != nil { if e != nil {
http.Error(w, e.Error(), http.StatusInternalServerError) http.Error(w, e.Error(), http.StatusNotFound)
return return
} }
@@ -135,10 +136,16 @@ Loop:
} }
f.Flush() f.Flush()
case e := <-err: case e := <-err:
if e == io.EOF {
log.Debugf("Container stopped: %v", container.ID)
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")
f.Flush()
} else {
log.Debugf("Error while reading from log stream: %v", e) log.Debugf("Error while reading from log stream: %v", e)
break Loop break Loop
} }
} }
}
log.WithField("NumGoroutine", runtime.NumGoroutine()).Debug("runtime stats") log.WithField("NumGoroutine", runtime.NumGoroutine()).Debug("runtime stats")
} }