A simple read-only Docker Swarm Logging Api, to export or stream logs to HTTP from containers across different nodes inside a Swarm.
nginx-proxy based on https://github.com/dtan4/nginx-basic-auth-proxy with added docker secrets
Docker Swarm Services list: http://35.189.200.49/service
User: demo
Password: demo
- A Docker Swarm, setup on Ubuntu 16.04 Vms or later, with docker-ce 17.12 or later
- A private or cloud registry where you can push and pull Docker images
- Clone this repository:
git clone https://github.com/dreamPathsProjekt/whalefisher
cd <path-to>/whalefisher
- Make builder scripts executable:
chmod u+x nginx_builder.sh
chmod u+x whale_builder.sh
The nginx_builder and whale_builder scripts can be run with the following syntax:
./whale_builder.sh <version>
You can tag whatever version you like, but you will have to provide the correct image versions (or latest
tag) to the whale-fisher.yml as well.
- Edit the
DOCKER_REGISTRY
variable of both scripts to your own private registry.
#!/bin/bash
# Edit below line to your own private registry
DOCKER_REGISTRY=
- Build the nginx-proxy image. You will be prompted to provide username & password to be stored as Docker secrets, on the first run:
./nginx_builder.sh 0.2
- Build the whalefisher data-provider and manager images:
./whale_builder.sh 0.9.2.3
The deployment .yml for whalefisher stack is the following:
version: "3.3"
secrets:
whale_username:
external: true
whale_password:
external: true
services:
nginx-proxy:
image: registry.dream:5001/nginx-proxy:0.2
deploy:
replicas: 1
secrets:
- source: whale_username
target: whale_username
- source: whale_password
target: whale_password
environment:
- PROXY_PASS=http://whalefisher-manager:5000
- SERVER_NAME=whalefisher.manager
ports:
- 80:80
- 8090:8090
whalefisher-manager:
image: registry.dream:5001/whalefisher-manager:0.9.2.3
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
environment:
DOCKER_NODE: "{{.Node.Hostname}}"
DATA_PROVIDER_PORT: 8080
PUBLISH_PORT: 80
EXT_DOMAIN_NAME: "http://35.189.200.49"
# Above only works for gcloud compute instances, use own ip or domain name to generate uris
volumes:
- /var/run/docker.sock:/var/run/docker.sock
whalefisher-data_provider:
image: registry.dream:5001/whalefisher-data_provider:0.9.2.3
deploy:
mode: global
endpoint_mode: dnsrr
environment:
- DOCKER_NODE={{.Node.Hostname}}
ports:
- target: 5000
published: 8080
protocol: tcp
mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- It is mandatory to mount volumes
/var/run/docker.sock
on both manager and data-provider services, for the docker client to connect. - The data-povider service collects container logs from all nodes (managers and workers), and as such it has to be deployed globally, with published port in
host
mode. This network setup is necessary as themanager
service has to discover worker nodes in the swarm by theirnode ids
andnode internal ips
, to retrieve individual container logs. The above has been a known Docker Swarm Api limitation (or feature). - The manager service has to be deployed on a manager node, as it performs service and node discovery. Those features are restricted on worker nodes.
- The nginx-proxy service proxies requests to the manager service and provides authentication to all of the URIs. It uses Docker secrets
whale_username
andwhale_password
for the authentication and is set withPROXY_PASS
environment value to point to the manager service by Dockerservice dns name
and publishedvip
port. Port8090
is exposed to provide nginx statistics. It is also recommended to setupSERVER_NAME
ofnginx-proxy
to your own domain name.
The following environment variables and configuration options have to be set up according to your environment:
- Setup the published port of the
data_provider
service to whatever port is available in your nodes, but keep the target port at5000
. In order for themanager
service to discover thedata_provider
ports, you also have to setDATA_PROVIDER_PORT
on thewhalefisher-manager
service, to the same value
Example:
whalefisher-data_provider:
# ...
ports:
- target: 5000
published: 8080
whalefisher-manager:
# ...
environment:
# ...
DATA_PROVIDER_PORT: 8080
# ...
- The
manager
service constructs pseudo HATEOAS links to navigate the API. You need to pass your external facing domain or ip address and the port thatnginx-proxy
exposes to thewhalefisher-manager
environment variables:EXT_DOMAIN_NAME
PUBLISH_PORT
Example:
services:
nginx-proxy:
ports:
- 80:80
# ...
whalefisher-manager:
# ...
environment:
DOCKER_NODE: "{{.Node.Hostname}}"
DATA_PROVIDER_PORT: 8080
PUBLISH_PORT: 80
EXT_DOMAIN_NAME: "http://35.189.200.49"
After all of the above configurations, you can deploy the stack with the following command:
docker stack deploy -c whale-fisher.yml <stack_name>
- It is advised to use Chrome, Firefox or Edge with a Json Formatter extension (e.g. Chrome Ext:
JSON Formatter
), for your own links-navigation convenience. - You can also use REST clients like Postman for JSON Endpoints but live streaming log routes do not work.
- Cli tools like curl also work with both JSON and Streaming Routes.
/
: List All Routes/service/
: List All Swarm Services/service/<string:name>
: Search Swarm Services- Example:
http://35.189.200.49/service/whale
returns a list of json service obj where name contains stringwhale
- Output:
- Example:
[
{
"_links": {
"_self": "http://35.189.200.49:80/service/whale_whalefisher-data_provider",
"logs": {
"stream": "http://35.189.200.49:80/service/whale_whalefisher-data_provider/logs/stream"
},
"tasks": "http://35.189.200.49:80/service/whale_whalefisher-data_provider/tasks"
},
"id": "bazngvc41saozxqwnb80p92ip",
"name": "whale_whalefisher-data_provider"
},
{
"_links": {
"_self": "http://35.189.200.49:80/service/whale_whalefisher-manager",
"logs": {
"stream": "http://35.189.200.49:80/service/whale_whalefisher-manager/logs/stream"
},
"tasks": "http://35.189.200.49:80/service/whale_whalefisher-manager/tasks"
},
"id": "tww67edxg3duq644e007vx0jn",
"name": "whale_whalefisher-manager"
}
]
-
/service/<string:exact_name>/tasks
: If exact service name is used,/tasks/
returns a list of docker swarm tasks running for that service. E.g. if a service with nameelastic_elasticsearch
has 3 running replicas,/tasks
should return a list of those replicas. For convenience use the pseudo-HATEOAS link:tasks
under_links
-
/service/<string:exact_name>/tasks/<string:task_id>
: Returns the exact running task with the specified id. You can also use truncated ids, as in the below example:http://35.189.200.49/service/elastic_elasticsearch/tasks/kad1
In the output you can see the full task id used for the request:
{
"_links": {
"_self": "http://35.189.200.49:80/service/elastic_elasticsearch/tasks/kad1nusozo8089egzn4hwudud",
"logs": {
"compact": "http://35.189.200.49:80/service/elastic_elasticsearch/tasks/kad1nusozo8089egzn4hwudud/logs/compact",
"json": "http://35.189.200.49:80/service/elastic_elasticsearch/tasks/kad1nusozo8089egzn4hwudud/logs",
"stream": "http://35.189.200.49:80/service/elastic_elasticsearch/tasks/kad1nusozo8089egzn4hwudud/logs/stream"
},
"service": "http://35.189.200.49:80/service/elastic_elasticsearch"
},
"current_state": "running",
"desired_state": "running",
"id": "kad1nusozo8089egzn4hwudud",
"node_id": "lue9l1amt3x1v0xbputcops7f",
"node_name": "dream-paths",
"service_id": "r8iyt20mqn65gedc2sk6iz78a",
"service_name": "elastic_elasticsearch",
"slot": null
}
_links: {logs: }
field on/tasks/...
redirect to container/task log output_links: {logs: }
field on/services/...
redirect to swarm service logs (contain service level logs from all tasks)- Below you can find details about different log viewing options
/service/<string:exact_name>/tasks/<string:task_id>/logs/
returns a list of container log lines with timestamp and container names.- Example request:
http://35.189.200.49/service/elastic_elasticsearch/tasks/kad1nusozo8089egzn4hwudud/logs
- Example request:
// ...
[{
"Line 398": "2018-05-18T00:44:12.972502668Z \tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)",
"Name": "elastic_elasticsearch.lue9l1amt3x1v0xbputcops7f.kad1nusozo8089egzn4hwudud",
"Timestamp": "2018-05-18T00:44:12.972502668"
}, {
"Line 399": "2018-05-18T00:44:12.972505198Z \tat java.lang.Thread.run(Thread.java:748)",
"Name": "elastic_elasticsearch.lue9l1amt3x1v0xbputcops7f.kad1nusozo8089egzn4hwudud",
"Timestamp": "2018-05-18T00:44:12.972505198"
}, {
"Line 400": "",
"Name": "elastic_elasticsearch.lue9l1amt3x1v0xbputcops7f.kad1nusozo8089egzn4hwudud",
"Timestamp": ""
}]
// ...
/service/<string:exact_name>/tasks/<string:task_id>/logs/compact/
returns a list of JSONified strings, example output:
[
"2018-05-01T17:35:29.786470420Z [2018-05-01 17:35:29,785][WARN ][bootstrap ] unable to install syscall filter: seccomp unavailable: your kernel is buggy and you should upgrade",
"2018-05-01T17:35:30.179505631Z [2018-05-01 17:35:30,179][INFO ][node ] [Huntara] version[2.4.6], pid[1], build[5376dca/2017-07-18T12:17:44Z]",
"2018-05-01T17:35:30.179796393Z [2018-05-01 17:35:30,179][INFO ][node ] [Huntara] initializing ...",
"2018-05-01T17:35:31.201964187Z [2018-05-01 17:35:31,201][INFO ][plugins ] [Huntara] modules [reindex, lang-expression, lang-groovy], plugins [], sites []",
"2018-05-01T17:35:31.310685060Z [2018-05-01 17:35:31,310][INFO ][env ] [Huntara] using [1] data paths, mounts [[/usr/share/elasticsearch/data (/dev/sda1)]], net usable_space [4.5gb], net total_space [9.6gb], spins? [possibly], types [ext4]",
"2018-05-01T17:35:31.310906578Z [2018-05-01 17:35:31,310][INFO ][env ] [Huntara] heap size [1015.6mb], compressed ordinary object pointers [true]",
"2018-05-01T17:35:34.455737006Z [2018-05-01 17:35:34,455][INFO ][node ] [Huntara] initialized",
"2018-05-01T17:35:34.461570654Z [2018-05-01 17:35:34,461][INFO ][node ] [Huntara] starting ...",
"2018-05-01T17:35:34.618512332Z [2018-05-01 17:35:34,618][INFO ][transport ] [Huntara] publish_address {10.0.3.3:9300}, bound_addresses {0.0.0.0:9300}",
"2018-05-01T17:35:34.624375994Z [2018-05-01 17:35:34,624][INFO ][discovery ] [Huntara] elasticsearch/Yx7UjAdZT7q0iOLz82IyTg",
"2018-05-01T17:36:04.627214323Z [2018-05-01 17:36:04,626][WARN ][discovery ] [Huntara] waited for 30s and no initial state was set by the discovery",
"2018-05-01T17:36:04.640998255Z [2018-05-01 17:36:04,640][INFO ][http ] [Huntara] publish_address {10.0.3.3:9200}, bound_addresses {0.0.0.0:9200}",
"2018-05-01T17:36:04.647201506Z [2018-05-01 17:36:04,647][INFO ][node ] [Huntara] started",
"2018-05-02T17:09:45.701716652Z [2018-05-02 17:09:45,695][DEBUG][action.admin.indices.get ] [Huntara] no known master node, scheduling a retry",
"2018-05-02T17:09:56.235000314Z [2018-05-02 17:09:56,216][WARN ][rest.suppressed ] path: /_stats, params: {}",
"2018-05-02T17:09:56.235020569Z ClusterBlockException[blocked by: [SERVICE_UNAVAILABLE/1/state not recovered / initialized];]",
"2018-05-02T17:09:56.235024660Z \tat org.elasticsearch.cluster.block.ClusterBlocks.globalBlockedException(ClusterBlocks.java:158)",
"2018-05-02T17:09:56.235028274Z \tat org.elasticsearch.action.admin.indices.stats.TransportIndicesStatsAction.checkGlobalBlock(TransportIndicesStatsAction.java:70)",
"2018-05-02T17:09:56.235031773Z \tat org.elasticsearch.action.admin.indices.stats.TransportIndicesStatsAction.checkGlobalBlock(TransportIndicesStatsAction.java:47)",
"2018-05-02T17:09:56.235034823Z \tat org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction$AsyncAction.<init>(TransportBroadcastByNodeAction.java:260)",
"2018-05-02T17:09:56.235038068Z \tat org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction.doExecute(TransportBroadcastByNodeAction.java:238)",
"2018-05-02T17:09:56.235040992Z \tat org.elasticsearch.action.support.broadcast.node.TransportBroadcastByNodeAction.doExecute(TransportBroadcastByNodeAction.java:79)",
"2018-05-02T17:09:56.235069404Z \tat org.elasticsearch.action.support.TransportAction.execute(TransportAction.java:137)",
"2018-05-02T17:09:56.235073838Z \tat org.elasticsearch.action.support.TransportAction.execute(TransportAction.java:85)",
"2018-05-02T17:09:56.235076466Z \tat org.elasticsearch.client.node.NodeClient.doExecute(NodeClient.java:58)",
"2018-05-02T17:09:56.235078975Z \tat org.elasticsearch.client.support.AbstractClient.execute(AbstractClient.java:359)",
]
-
Important: Very large log output, when requested as a single response, may grow substantially large (you can view network tab in developer tools as it loads). For those use-cases, it is best advised to use streaming endpoints as described below.
-
/service/<string:exact_name>/tasks/<string:task_id>/logs/stream/
returns a plain-text stream of logs from the creation of a container till now.- CLI tools like curl also retain terminal-color settings from the initial logs.
-
/service/<string:exact_name>/tasks/<string:task_id>/logs/tail/<int:lines>
returns the tail of lastlines
log lines till now, withlines
provided in the url. Additional realtime output is appended.-
Example request:
http://35.189.200.49/service/elastic_elasticsearch/tasks/kad1nusozo8089egzn4hwudud/logs/tail/10
-
Output:
-
2018-05-18T00:44:12.972480658Z at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:88)
2018-05-18T00:44:12.972483231Z at org.jboss.netty.channel.socket.nio.AbstractNioWorker.process(AbstractNioWorker.java:108)
2018-05-18T00:44:12.972485755Z at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:337)
2018-05-18T00:44:12.972488454Z at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:89)
2018-05-18T00:44:12.972492366Z at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178)
2018-05-18T00:44:12.972494958Z at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
2018-05-18T00:44:12.972497471Z at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42)
2018-05-18T00:44:12.972500133Z at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
2018-05-18T00:44:12.972502668Z at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
2018-05-18T00:44:12.972505198Z at java.lang.Thread.run(Thread.java:748)
- Important: Streamed/Tail Responses load faster than JSON Responses but keep the connection alive forever, as they follow log output in real-time. For small log outputs it is usually faster to use the JSON Routes
Service-Level logs can be only viewed as streamed Responses, due to limitations in the Docker Low-Level Api. Keep in mind that individual container logs may be included mixed in service-level logs, as they are asynchronously updated when individual Docker tasks are started and finished.
/service/<string:exact_name>/logs/stream
returns a plain-text stream of logs./service/<string:exact_name>/logs/tail/<int:lines>
returns the tail of lastlines
service-log lines till now, withlines
provided in the url. Additional realtime output is appended.
- For your own convenience you can navigate the Api using the pseudo-HATEOAS
_links
objects. - When a URI resource is not found, the output produced is:
{
"error": "Not found"
}
-
The above response may occur on wrong input, or during a Docker Service is restarting individual containers. At this situation individual task ids are recreated, so it is advised to refresh the
.../tasks/
endpoint and follow the new recreated ids. -
It is not recommended to use values <= 5 on
/tail/lines
routes. As of newer versions of the API (0.9 and later), fewer than 5 lines return a400 error - bad request
{
"error": "Bad Request"
}
It is generally not advisable to view logs using the low-level Api.
If you, however, wish to view individual containers on each host via the data provider global service, you can use the data provider's host ip with it's exported port:
/node/current
: Returns the hostname of the individual node you are sending requests to.- Example Request:
http://35.189.200.49:8080/node/current
- Output:
- Example Request:
{
"current": "dream-paths"
}
/container/<string:id>
: Returns an individual container's info. Container ids can be also submitted truncated.- The Below URIs work according to the High-Level Api Template, as documented earlier:
/container/<string:id>/logs/
/container/<string:id>/logs/compact/
/container/<string:id>/logs/stream/
/container/<string:id>/logs/tail/<int:lines>
- Include Token Authorization (JWT)
- Implement
since
&until
routes to find logs within a given timeframe.