1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-21 13:23:07 +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
/* snapshot: Test_handler_streamLogs_error_finding_container */
HTTP/1.1 500 Internal Server Error
HTTP/1.1 404 Not Found
Connection: close
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff

View File

@@ -134,7 +134,7 @@ describe("<LogEventSource />", () => {
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<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>
`);
});
@@ -149,7 +149,7 @@ describe("<LogEventSource />", () => {
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<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>
`);
});
@@ -164,7 +164,7 @@ describe("<LogEventSource />", () => {
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<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>
`);
});
@@ -182,7 +182,7 @@ describe("<LogEventSource />", () => {
await wrapper.vm.$nextTick();
expect(wrapper.find("ul.events")).toMatchInlineSnapshot(`
<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>
`);
});

View File

@@ -35,19 +35,23 @@ export default {
this.es = null;
}
this.es = new EventSource(`${config.base}/api/logs/stream?id=${this.id}`);
const flushBuffer = debounce(
() => {
this.messages.push(...this.buffer);
this.buffer = [];
},
250,
{ maxWait: 1000 }
);
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.buffer = [];
};
this.es.onmessage = (e) => {
this.buffer.push(this.parseMessage(e.data));
flushBuffer();
};
this.es.onerror = (e) => console.log("EventSource failed." + e);
this.$once("hook:beforeDestroy", () => this.es.close());
},
async loadOlderLogs() {

View File

@@ -1,6 +1,6 @@
<template lang="html">
<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="text" v-html="colorize(item.message)"></span>
</li>
@@ -103,6 +103,10 @@ export default {
white-space: pre-wrap;
}
li.event {
color: #f14668;
}
::v-deep mark {
border-radius: 2px;
background-color: #ffdd57;

View File

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

View File

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

View File

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

View File

@@ -59,18 +59,22 @@ const actions = {
},
};
const getters = {
activeContainersById(state) {
return state.activeContainers.reduce((map, obj) => {
activeContainersById({ activeContainers }) {
return activeContainers.reduce((map, obj) => {
map[obj.id] = obj;
return map;
}, {});
},
allContainersById(state) {
return state.containers.reduce((map, obj) => {
allContainersById({ containers }) {
return containers.reduce((map, obj) => {
map[obj.id] = obj;
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`);

View File

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

View File

@@ -33,7 +33,7 @@ type dockerProxy interface {
// Client is a proxy around the docker client
type Client interface {
ListContainers(showAll bool) ([]Container, error)
ListContainers() ([]Container, error)
FindContainer(string) (Container, error)
ContainerLogs(context.Context, string, int) (<-chan string, <-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) {
var container Container
containers, err := d.ListContainers(true)
containers, err := d.ListContainers()
if err != nil {
return container, err
}
@@ -85,10 +85,10 @@ func (d *dockerClient) FindContainer(id string) (Container, error) {
return container, nil
}
func (d *dockerClient) ListContainers(showAll bool) ([]Container, error) {
func (d *dockerClient) ListContainers() ([]Container, error) {
containerListOptions := types.ContainerListOptions{
Filters: d.filters,
All: showAll,
All: true,
}
list, err := d.cli.ContainerList(context.Background(), containerListOptions)
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)
client := &dockerClient{proxy, filters.NewArgs()}
list, err := client.ListContainers(true)
list, err := client.ListContainers()
assert.Empty(t, list, "list should be empty")
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"))
client := &dockerClient{proxy, filters.NewArgs()}
list, err := client.ListContainers(true)
list, err := client.ListContainers()
assert.Nil(t, list, "list should be nil")
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)
client := &dockerClient{proxy, filters.NewArgs()}
list, err := client.ListContainers(true)
list, err := client.ListContainers()
require.NoError(t, err, "error should not return an error.")
assert.Equal(t, list, []Container{

18
main.go
View File

@@ -27,15 +27,13 @@ var (
)
type handler struct {
client docker.Client
showAll bool
box packr.Box
client docker.Client
box packr.Box
}
func init() {
pflag.String("addr", ":8080", "http service address")
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.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")
@@ -49,10 +47,9 @@ func init() {
base = viper.GetString("base")
level = viper.GetString("level")
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.
// filters = viper.GetStringSlice("filter")
// Until https://github.com/spf13/viper/issues/911 is fixed. We have to use this hacky way.
// filters = viper.GetStringMapString("filter")
if value, ok := os.LookupEnv("DOZZLE_FILTER"); ok {
log.Infof("Parsing %s", value)
urlValues, err := url.ParseQuery(strings.ReplaceAll(value, ",", "&"))
@@ -77,7 +74,7 @@ func init() {
func main() {
log.Infof("Dozzle version %s", version)
dockerClient := docker.NewClientWithFilters(filters)
_, err := dockerClient.ListContainers(true)
_, err := dockerClient.ListContainers()
if err != nil {
log.Fatalf("Could not connect to Docker Engine: %v", err)
@@ -85,9 +82,8 @@ func main() {
box := packr.NewBox("./static")
r := createRoutes(base, &handler{
client: dockerClient,
showAll: showAll,
box: box,
client: dockerClient,
box: box,
})
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)
}
func (m *MockedClient) ListContainers(showAll bool) ([]docker.Container, error) {
func (m *MockedClient) ListContainers() ([]docker.Container, error) {
args := m.Called()
containers, ok := args.Get(0).([]docker.Container)
if !ok {
@@ -246,7 +246,7 @@ func Test_createRoutes_index(t *testing.T) {
box := packr.NewBox("./virtual")
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)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
@@ -259,7 +259,7 @@ func Test_createRoutes_redirect(t *testing.T) {
mockedClient := new(MockedClient)
box := packr.NewBox("./virtual")
handler := createRoutes("/foobar", &handler{mockedClient, true, box})
handler := createRoutes("/foobar", &handler{mockedClient, box})
req, err := http.NewRequest("GET", "/foobar", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
@@ -273,7 +273,7 @@ func Test_createRoutes_foobar(t *testing.T) {
box := packr.NewBox("./virtual")
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)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
@@ -287,7 +287,7 @@ func Test_createRoutes_foobar_file(t *testing.T) {
box := packr.NewBox("./virtual")
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)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()
@@ -300,7 +300,7 @@ func Test_createRoutes_version(t *testing.T) {
mockedClient := new(MockedClient)
box := packr.NewBox("./virtual")
handler := createRoutes("/", &handler{mockedClient, true, box})
handler := createRoutes("/", &handler{mockedClient, box})
req, err := http.NewRequest("GET", "/version", nil)
require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder()

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"runtime"
"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) {
containers, err := h.client.ListContainers(h.showAll)
containers, err := h.client.ListContainers()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -109,7 +110,7 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
container, e := h.client.FindContainer(id)
if e != nil {
http.Error(w, e.Error(), http.StatusInternalServerError)
http.Error(w, e.Error(), http.StatusNotFound)
return
}
@@ -135,8 +136,14 @@ Loop:
}
f.Flush()
case e := <-err:
log.Debugf("Error while reading from log stream: %v", e)
break Loop
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)
break Loop
}
}
}