Compare commits

...

133 Commits

Author SHA1 Message Date
Amir Raminfar 293bf9e021 chore: release v8.4.2 2024-09-08 09:05:10 -07:00
Amir Raminfar fa87939676 chore: updates readme 2024-09-08 09:02:31 -07:00
Amir Raminfar d482dcd667 chore: updates modules 2024-09-08 09:00:09 -07:00
Amir Raminfar 65cdb306f8 chore: adds fatal error message when event.message is nil (#3259) 2024-09-08 08:57:24 -07:00
Amir Raminfar 7179b630a5 chore: improves animation 2024-09-08 08:54:16 -07:00
Amir Raminfar 8ee18ce9f4 more fixes 2024-09-07 16:24:42 -07:00
Amir Raminfar a5ffd88884 fix: improves search by backfilling on first search (#3261) 2024-09-07 22:11:09 +00:00
renovate[bot] 328a277cff fix(deps): update all non-major dependencies (#3260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 14:12:21 +00:00
renovate[bot] 0d58f2bfca fix(deps): update all non-major dependencies (#3241)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-06 14:51:11 +00:00
Amir Raminfar 73414c6a8d chore: updates go 1.23.1 (#3258) 2024-09-06 07:41:02 -07:00
Amir Raminfar 95fafd12c5 chore: updates go 1.23.1 2024-09-06 06:31:57 -07:00
Amir Raminfar cdf6334cef chore: skip vue errors 2024-09-05 11:42:38 -07:00
Amir Raminfar 6791c53d43 fix: fixes search while looking back history (#3256) 2024-09-05 09:48:13 -07:00
Amir Raminfar 0cbc08fa1a chore: updates vue (#3255) 2024-09-05 08:30:18 -07:00
Amir Raminfar 45c7224d13 fix: improves search on initital load (#3253) 2024-09-05 00:22:30 +00:00
Amir Raminfar 51a3f1f03b chore: updates vue (#3252) 2024-09-04 17:00:27 -07:00
Amir Raminfar 9ddbe6fbb0 feat: shows raw json in complex logs with option to copy (#3251) 2024-09-04 08:53:40 -07:00
Amir Raminfar a62331a488 chore: release v8.4.1 2024-09-03 13:56:59 -07:00
Amir Raminfar 2a3af94467 fix: fixes merged view and incorrect base. fixes #3248 (#3249) 2024-09-03 20:54:10 +00:00
Amir Raminfar 42e423fd2e chore: release v8.4.0 2024-09-03 09:02:48 -07:00
Amir Raminfar 9a146ae62a chore: fixes docs 2024-09-03 09:01:21 -07:00
Amir Raminfar 1518d80aad chore: updates to vue 3.5 2024-09-03 08:55:19 -07:00
Amir Raminfar a1efb8e340 fix: supports searching for int, float and boolean (#3247) 2024-09-03 15:53:00 +00:00
Amir Raminfar 7d42bd2b02 chore: adds comparison chart for agents and removes warning (#3246) 2024-09-03 15:13:42 +00:00
Amir Raminfar 9f2f8b8245 feat!: moves search to backend for better experience. It does remove show search results in context. (#3245) 2024-09-03 08:02:09 -07:00
Amir Raminfar 057d7ad712 chore: updates modules 2024-09-01 15:41:16 -07:00
Amir Raminfar 712e1d389a chore: updates vue component compiler 2024-08-30 09:44:22 -07:00
Amir Raminfar c1fcafba43 chore: updates vue 2024-08-30 09:43:58 -07:00
Amir Raminfar 74063a0c72 feat: makes improvements to mobile view (#3244) 2024-08-30 09:36:27 -07:00
Amir Raminfar 12e7ee33b7 chore: release v8.3.0 2024-08-29 13:23:06 -07:00
Amir Raminfar db33e28117 chore: fixes wrap 2024-08-29 13:11:22 -07:00
Amir Raminfar df1a650980 chore: adds pinned headers 2024-08-29 13:07:00 -07:00
Amir Raminfar a937615747 chore: adds all toggle 2024-08-29 13:04:31 -07:00
Amir Raminfar d13163b0d9 chore: removes tag if not there for level (#3242) 2024-08-29 15:59:50 +00:00
Amir Raminfar 9ea7024c2a feat: show version number on top right. see #3228 (#3239) 2024-08-29 00:39:24 +00:00
Amir Raminfar 85271a595a feat: adds a side panel for complex logs that enables reordering and enabling fields (#3237) 2024-08-29 00:37:04 +00:00
renovate[bot] 62a2ef9eb4 fix(deps): update all non-major dependencies (#3238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 15:52:03 +00:00
Amir Raminfar 64790daea7 chore: const 2024-08-26 14:23:04 -07:00
Amir Raminfar 0798de3b3a fix: fixes broken skip logs 2024-08-26 13:42:19 -07:00
Amir Raminfar 0a03923587 feat: changes scroll progress to reflect total available logs instead of just the logs in the browser (#3236) 2024-08-26 20:12:24 +00:00
Amir Raminfar 3a0eaf1609 fix: fixes infinite loop when there are no logs (#3235) 2024-08-26 18:22:43 +00:00
Amir Raminfar a6620ff315 chore: fixes broken settings page 2024-08-26 08:13:13 -07:00
Amir Raminfar aec8139a19 feat: adds animations to bottom when in live mode (#3231) 2024-08-26 14:58:39 +00:00
Amir Raminfar afcb80938e feat: retries to fetch more logs if no logs are fetched when scrolling up (#3230) 2024-08-26 06:23:11 -07:00
Amir Raminfar 36f3308e1d perf: tries to improve burst of logs (#3227) 2024-08-26 06:22:50 -07:00
renovate[bot] b6091ff944 chore(deps): update dependency jsdom to v25 (#3233) 2024-08-26 00:51:35 +00:00
renovate[bot] 4bb007fdfd fix(deps): update all non-major dependencies (#3232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-26 00:22:44 +00:00
Amir Raminfar a56fea1728 chore: updates makefile 2024-08-25 08:39:10 -07:00
renovate[bot] 6a1a412f47 chore(deps): update all non-major dependencies (#3224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-23 16:25:45 +00:00
Qhilm eaf20059ba fix: host port nb typo in readme (#3220) 2024-08-21 09:11:56 -07:00
renovate[bot] a0db92401f chore(deps): update mcr.microsoft.com/playwright docker tag to v1.46.1 (#3218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 15:19:17 +00:00
Amir Raminfar 3f8728c821 chore: fixes typecheck by reverting hoisting in npmrc (#3219) 2024-08-19 15:08:09 +00:00
Amir Raminfar 4319415e1d fix: fixes settings page when changing language (#3216) 2024-08-16 19:34:14 +00:00
renovate[bot] b54611fb65 fix(deps): update dependency unplugin-vue-macros to ^2.11.6 (#3215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-16 15:45:03 +00:00
Cololi b0012633aa chore: Complete Simplified and Traditional Chinese translations (#3214) 2024-08-16 08:39:05 -07:00
Amir Raminfar 8cbd04812a bug: fixes broken alt enter on fuzzy search 2024-08-15 15:46:34 -07:00
renovate[bot] 1dea4f1059 fix(deps): update dependency @vueuse/router to v11 (#3213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 15:40:27 -07:00
renovate[bot] 3d9e025b39 fix(deps): update dependency @vueuse/components to v11 (#3210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 22:20:40 +00:00
renovate[bot] acd9946bc8 fix(deps): update dependency @vueuse/core to v11 (#3211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 22:02:00 +00:00
renovate[bot] 9f743132dd fix(deps): update all non-major dependencies (#3209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 21:58:25 +00:00
Amir Raminfar 621a3d98bd chore: release v8.2.1 2024-08-15 11:01:40 -07:00
renovate[bot] bf574c6b83 chore(deps): update all non-major dependencies (#3208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-15 17:31:36 +00:00
Florian 9ace6eccdf feat: Translate the example log entries (#3206) 2024-08-15 08:20:45 -07:00
Florian 3d152b13dc bug: Translated incorrect into German correctly and added missing (#3205) 2024-08-14 19:21:40 +00:00
renovate[bot] 10f868d2c4 chore(deps): update all non-major dependencies (#3201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-14 17:12:56 +00:00
Amir Raminfar 077eeb7917 chore: uses zerolog instead of logrus (#3203) 2024-08-14 17:04:18 +00:00
Amir Raminfar 37f0249065 chore: removes healthcheck analytics 2024-08-13 13:06:52 -07:00
Amir Raminfar aacf0474e4 chore: fixes broken anallytics for generate 2024-08-13 13:00:29 -07:00
Amir Raminfar c1bf6b15c9 chore: release v8.2.0 2024-08-13 12:43:08 -07:00
Amir Raminfar fa7c7ca62d docs: updates agents custom certs 2024-08-13 12:42:32 -07:00
Amir Raminfar 0d3ed03e04 chore: updates makefile 2024-08-13 11:49:24 -07:00
Amir Raminfar 7f735d26db feat: reads certs locally if available (#3196) 2024-08-13 11:48:56 -07:00
Amir Raminfar 9467e3d7d5 chore: update readme (#3199) 2024-08-13 15:47:59 +00:00
Amir Raminfar 3072709f7e chore: updates readme 2024-08-13 08:30:46 -07:00
renovate[bot] d1a308b3b6 chore(deps): update all non-major dependencies (#3198)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 15:22:58 +00:00
Amir Raminfar 9dfdfbf8b4 chore: removes version from yml files and docs 2024-08-13 08:15:03 -07:00
Jérémy Touati acbb2a8d24 feat: implements support for custom container names (#3197) 2024-08-13 08:04:51 -07:00
Amir Raminfar 6c5dcb7a62 chore: release v8.1.5 2024-08-12 09:43:02 -07:00
Amir Raminfar aa824453a5 feat: supports any service name in swarm mode by looking internally the hostname (#3193) 2024-08-12 09:34:53 -07:00
renovate[bot] bdf19587f2 chore(deps): update all non-major dependencies (#3195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-12 15:35:40 +00:00
Amir Raminfar 1637b37cc1 chore: hides drop down menu on mobile 2024-08-11 08:54:08 -07:00
renovate[bot] 4eb73e96c5 chore(deps): update all non-major dependencies (#3191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-10 20:41:25 +00:00
Amir Raminfar 69fb227519 feat: adds action toolbar for groups, services and stacks (#3190) 2024-08-10 13:35:56 -07:00
renovate[bot] ffc4559047 fix(deps): update all non-major dependencies (#3188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-08 17:44:48 +00:00
renovate[bot] 05fc3f04ec chore(deps): update all non-major dependencies (#3186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-07 17:06:13 +00:00
renovate[bot] 84be3b1648 chore(deps): update all non-major dependencies (#3185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-07 07:20:47 +00:00
renovate[bot] cabe2c803a chore(deps): update all non-major dependencies (#3184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-06 14:14:18 +00:00
Amir Raminfar f5a9669249 chore: release v8.1.4 2024-08-05 14:47:50 -07:00
renovate[bot] c382b25b97 fix(deps): update dependency vitepress to v1.3.2 (#3171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 21:08:00 +00:00
Amir Raminfar 9db280e7b4 chore: updates vue-router by adding hoisting (#3181) 2024-08-05 21:00:52 +00:00
Amir Raminfar 22c1473c4b fix: fixes search skipping every other log (#3180) 2024-08-05 12:50:40 -07:00
Amir Raminfar f53c4d7d3d chore: updates modules (#3179) 2024-08-04 16:31:24 -07:00
Amir Raminfar 2514c666c4 chore: updates node modules (#3177) 2024-08-03 15:48:09 +00:00
Amir Raminfar b38a02f5ba fix: fixes broken image tag in title bar (#3176) 2024-08-03 00:24:18 +00:00
Amir Raminfar db1cccc61f chore: updates vue 2024-08-01 09:05:12 -07:00
Amir Raminfar db75e0f3e9 chore: updates vue/vite (#3174) 2024-08-01 09:02:01 -07:00
Amir Raminfar fe149dc8b2 chore: updates vitest (#3175) 2024-08-01 15:59:57 +00:00
Amir Raminfar 3a77e2b187 chore: updates node (#3173) 2024-08-01 08:43:37 -07:00
Amir Raminfar 6d4be52006 fix: fixes missing info (#3172) 2024-08-01 15:36:33 +00:00
Amir Raminfar ddcd625a70 chore: release v8.1.3 2024-07-31 10:48:36 -07:00
Amir Raminfar 1b9f25e64f chore: fixes analytics to have better data (#3170) 2024-07-31 17:39:24 +00:00
Steven Horner 73fa622721 docs: Fix syntax to enable actions (#3169) 2024-07-31 05:58:57 -07:00
renovate[bot] 0e652cb936 chore(deps): update all non-major dependencies (#3168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-31 02:37:07 +00:00
Amir Raminfar 657643e6ce fix: fixes panic in agent with stopped containers (#3167) 2024-07-30 19:31:14 -07:00
Amir Raminfar b4277603cf chore: fixes title truncate (#3165) 2024-07-30 12:10:17 -07:00
Amir Raminfar d72edefac2 chore: updates swarm file 2024-07-30 07:48:41 -07:00
Amir Raminfar df4b449bc4 chore: release v8.1.2 2024-07-29 13:10:02 -07:00
Amir Raminfar 74fda04ebb chore: adds swarm mode as analytics 2024-07-29 12:18:58 -07:00
Amir Raminfar 638889ed58 feat: supports container actions for agents (#3164) 2024-07-29 19:16:38 +00:00
renovate[bot] f09e294f38 fix(deps): update all non-major dependencies (#3162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 01:54:46 +00:00
Amir Raminfar de2e757665 fix: refreshes versions for agent properly after restart (#3160) 2024-07-28 14:55:15 -07:00
Amir Raminfar ed84b9a181 perf: uses a semaphore to fetch containers faster in parallel (#3161) 2024-07-28 14:54:45 -07:00
Amir Raminfar c906b80f52 fix: fixes improper version handeling (#3157) 2024-07-28 00:32:38 +00:00
Amir Raminfar e2a951ca3d chore: release v8.1.1 2024-07-27 16:54:01 -07:00
Amir Raminfar c91adb221a fix: fixes default fallback not working. fixes #3154 (#3156) 2024-07-27 16:46:11 -07:00
Amir Raminfar 989f0f03ee feat: uses desktop menu instead for mobile menu (#3153) 2024-07-26 11:18:05 -07:00
Amir Raminfar ba10c03a90 chore: release v8.1.0 2024-07-26 09:32:46 -07:00
Amir Raminfar 41859e64a5 chore: wraps icons 2024-07-26 08:10:58 -07:00
Amir Raminfar 5ef18bf508 chore: fixes sizing on icon 2024-07-26 08:04:31 -07:00
Amir Raminfar 19d0355d7c perf: only loads selected locales (#3152) 2024-07-25 17:29:16 -07:00
Amir Raminfar cbed761a18 perf: limits maximum items to be fetched when scrolling up. see #3122 (#3151) 2024-07-25 15:26:28 +00:00
renovate[bot] abd9cebdbe chore(deps): update all non-major dependencies (#3150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 15:08:22 +00:00
Amir Raminfar 3a988b8fdc feat: updates agents to be more resilient by reconnecting. also adds big performance issues in swarm mode with little updates to the UI. (#3145) 2024-07-25 08:01:33 -07:00
Amir Raminfar c87a7b1272 docs: fixes agent page 2024-07-24 19:42:02 -07:00
Amir Raminfar ed351f9d07 chore: release v8.0.7 2024-07-24 08:25:15 -07:00
renovate[bot] c4ff571eb5 chore(deps): update all non-major dependencies (#3148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 15:15:56 +00:00
Amir Raminfar 2bd0031feb chore: fixes async bug 2024-07-24 08:03:25 -07:00
Amir Raminfar 082bed2382 perf: fixes an issue with too many containers (#3144) 2024-07-24 06:58:21 -07:00
Amir Raminfar 4aa315a471 fix: fixes first few lines not showing up. fixes #3139 (#3142) 2024-07-23 21:52:46 -07:00
Amir Raminfar 79118368b1 chore: adds more logs for authentication (#3143) 2024-07-23 13:05:17 -07:00
renovate[bot] daf8cf815b chore(deps): update all non-major dependencies (#3137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-22 21:02:26 +00:00
Amir Raminfar d2015fdebd chore: cleans up logging context (#3136) 2024-07-22 20:57:27 +00:00
renovate[bot] 989a650f12 chore(deps): update pnpm to v9.6.0 (#3132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-22 00:26:28 +00:00
159 changed files with 5329 additions and 3250 deletions
+3 -3
View File
@@ -89,9 +89,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
- name: Login to DockerHub
uses: docker/login-action@v3.2.0
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -107,7 +107,7 @@ jobs:
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.4.1
uses: docker/build-push-action@v6.7.0
with:
push: true
context: .
+3 -3
View File
@@ -13,9 +13,9 @@ jobs:
if: ${{ !github.event.repository.fork && !github.event.pull_request.head.repo.fork && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == 'amir20/dozzle') }}
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
- name: Login to DockerHub
uses: docker/login-action@v3.2.0
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -31,7 +31,7 @@ jobs:
echo "${{ secrets.TTL_KEY }}" > shared_key.pem
echo "${{ secrets.TTL_CERT }}" > shared_cert.pem
- name: Build and push
uses: docker/build-push-action@v6.4.1
uses: docker/build-push-action@v6.7.0
with:
context: .
push: true
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: 1.23.1
check-latest: true
- name: Checkout code
uses: actions/checkout@v4
+1
View File
@@ -1,3 +1,4 @@
shared-workspace-lockfile=false
include-workspace-root=true
ignore-workspace-root-check=true
+1 -1
View File
@@ -1,7 +1,7 @@
{
"i18n-ally.localesPaths": ["locales"],
"i18n-ally.keystyle": "nested",
"cSpell.words": ["healthcheck", "orderedmap", "stdcopy", "Warnf"],
"cSpell.words": ["healthcheck", "orderedmap", "Retriable", "stdcopy", "Warnf"],
"editor.formatOnSave": true,
"i18n-ally.extract.autoDetect": true
}
+2 -2
View File
@@ -1,5 +1,5 @@
# Build assets
FROM --platform=$BUILDPLATFORM node:22.5.1-alpine AS node
FROM --platform=$BUILDPLATFORM node:22.8.0-alpine AS node
RUN corepack enable
@@ -22,7 +22,7 @@ COPY public ./public
# Build assets
RUN pnpm build
FROM --platform=$BUILDPLATFORM golang:1.22.5-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.23.1-alpine AS builder
# install gRPC dependencies
RUN apk add --no-cache ca-certificates protoc protobuf-dev\
+16 -5
View File
@@ -22,7 +22,7 @@ fake_assets:
.PHONY: test
test: fake_assets generate
go test -cover -race -count 1 -timeout 20s ./...
go test -cover -race -count 1 -timeout 40s ./...
.PHONY: build
build: dist generate
@@ -35,7 +35,7 @@ docker: shared_key.pem shared_cert.pem
generate: shared_key.pem shared_cert.pem $(GEN_FILES)
.PHONY: dev
dev: shared_key.pem shared_cert.pem
dev: generate fake_assets
pnpm dev
.PHONY: int
@@ -45,7 +45,7 @@ int:
shared_key.pem:
@openssl genpkey -algorithm RSA -out shared_key.pem -pkeyopt rsa_keygen_bits:2048
shared_cert.pem:
shared_cert.pem: shared_key.pem
@openssl req -new -key shared_key.pem -out shared_request.csr -subj "/C=US/ST=California/L=San Francisco/O=Dozzle"
@openssl x509 -req -in shared_request.csr -signkey shared_key.pem -out shared_cert.pem -days 365
@rm shared_request.csr
@@ -55,5 +55,16 @@ $(GEN_DIR)/%.pb.go: $(PROTO_DIR)/%.proto
.PHONY: push
push: docker
@docker tag amir20/dozzle:latest amir20/dozzle:agent
@docker push amir20/dozzle:agent
@docker tag amir20/dozzle:latest amir20/dozzle:local-test
@docker push amir20/dozzle:local-test
tools:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/cespare/reflex@latest
run: docker
docker run -it --rm -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock amir20/dozzle:latest
preview: build
pnpm preview
+45 -27
View File
@@ -2,9 +2,9 @@
Dozzle is a small lightweight application with a web based interface to monitor Docker logs. It doesnt store any log files. It is for live monitoring of your container logs only.
https://user-images.githubusercontent.com/260667/227634771-9ebbe381-16a8-465a-b28a-450c5cd20c94.mp4
https://github.com/user-attachments/assets/66a7b4b2-d6c9-4fca-ab04-aef6cd7c0c31
[![Go Report Card](https://goreportcard.com/badge/github.com/amir20/dozzle)](https://goreportcard.com/report/github.com/amir20/dozzle)
[![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/amir20/dozzle)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Pulls](https://img.shields.io/docker/pulls/amir20/dozzle.svg)](https://hub.docker.com/r/amir20/dozzle/)
[![Docker Version](https://img.shields.io/docker/v/amir20/dozzle?sort=semver)](https://hub.docker.com/r/amir20/dozzle/)
![Test](https://github.com/amir20/dozzle/workflows/Test/badge.svg)
@@ -15,15 +15,17 @@ https://user-images.githubusercontent.com/260667/227634771-9ebbe381-16a8-465a-b2
- Search logs using regex 🔦
- Small memory footprint 🏎
- Split screen for viewing multiple logs
- Download logs easily
- Live stats with memory and CPU usage
- Multi-user authentication with support for proxy forward authorization 🚨
- Swarm mode support 🐳
- Agent mode for monitoring multiple Docker hosts 🕵️‍♂️
- Dark mode 🌙
Dozzle has been tested with hundreds of containers. However, it doesn't support offline searching. Products like [Loggly](https://www.loggly.com), [Papertrail](https://papertrailapp.com) or [Kibana](https://www.elastic.co/products/kibana) are more suited for full search capabilities.
## Getting Started
Dozzle is a small container (4 MB compressed). Pull the latest release with:
Dozzle is a small container (7 MB compressed). Pull the latest release with:
$ docker pull amir20/dozzle:latest
@@ -31,13 +33,12 @@ Dozzle is a small container (4 MB compressed). Pull the latest release with:
The simplest way to use dozzle is to run the docker container. Also, mount the Docker Unix socket with `--volume` to `/var/run/docker.sock`:
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8888:8080 amir20/dozzle:latest
$ docker run --name dozzle -d --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest
Dozzle will be available at [http://localhost:8888/](http://localhost:8888/).
Dozzle will be available at [http://localhost:8080/](http://localhost:8080/).
Here is the Docker Compose file:
version: "3"
services:
dozzle:
container_name: dozzle
@@ -45,10 +46,26 @@ Here is the Docker Compose file:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8888:8080
- 8080:8080
For advanced options like [authentication](https://dozzle.dev/guide/authentication), [remote hosts](https://dozzle.dev/guide/remote-hosts) or common [questions](https://dozzle.dev/guide/faq) see documentation at [dozzle.dev](https://dozzle.dev/guide/getting-started).
## Swarm Mode
Dozzle works with Docker Swarm mode. You can run Dozzle as a global service with:
$ docker service create --name dozzle --env DOZZLE_MODE=swarm --mode global --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock -p 8080:8080 amir20/dozzle:latest
See the [Swarm Mode](https://dozzle.dev/guide/swarm-mode) documentation for more details.
## Agent Mode
Dozzle can be used to monitor multiple Docker hosts. You can run Dozzle in agent mode with:
$ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 7007:7007 amir20/dozzle:latest agent
See the [Agent Mode](https://dozzle.dev/guide/agent-mode) documentation for more details.
## Technical Details
Dozzle users automatic API negotiation which works with most Docker configurations. Dozzle also works with [Colima](https://github.com/abiosoft/colima) and [Podman](https://podman.io/).
@@ -76,7 +93,7 @@ If it's not enabled please follow [this tutorial](https://github.com/containers/
Once you have the podman remote socket you can run Dozzle on podman.
```
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8888:8080 amir20/dozzle:latest
podman run --volume=/run/user/1000/podman/podman.sock:/var/run/docker.sock -d -p 8080:8080 amir20/dozzle:latest
```
## Security
@@ -93,21 +110,22 @@ If you do not want to be tracked at all, see the `--no-analytics` flag below.
Dozzle follows the [12-factor](https://12factor.net/) model. Configurations can use the CLI flags or environment variables. The table below outlines all supported options and their respective env vars.
| Flag | Env Variable | Default |
| --------------------------- | -------------------------------- | -------------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| `--auth-provider` | `DOZZLE_AUTH_PROVIDER` | `none` |
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | false |
| `--wait-for-docker-seconds` | `DOZZLE_WAIT_FOR_DOCKER_SECONDS` | 0 |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
| Flag | Env Variable | Default |
| --------------------- | -------------------------- | -------------- |
| `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` |
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
| `--level` | `DOZZLE_LEVEL` | `info` |
| `--auth-provider` | `DOZZLE_AUTH_PROVIDER` | `none` |
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | false |
| `--filter` | `DOZZLE_FILTER` | `""` |
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | false |
| `--mode` | `DOZZLE_MODE` | `server` |
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
## Support
@@ -125,10 +143,10 @@ There are many ways you can support Dozzle:
## Building
To Build and test locally:
To build and test locally:
1. Install [NodeJs](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io/installation).
2. Install [Go](https://go.dev/doc/install).
3. Install [reflex](https://github.com/cespare/reflex) with `go install github.com/cespare/reflex@latest`.
3. Install tools with `make tools`.
4. Install node modules `pnpm install`.
5. Run `make dev` to start a development server.
5. Run `make dev` to start a development server with hot reload.
+23 -2
View File
@@ -50,6 +50,7 @@ declare global {
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const flattenJSON: typeof import('./utils/index')['flattenJSON']
const flattenJSONToMap: typeof import('./utils/index')['flattenJSONToMap']
const formatBytes: typeof import('./utils/index')['formatBytes']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
@@ -104,8 +105,11 @@ declare global {
const persistentVisibleKeysForContainer: typeof import('./composable/storage')['persistentVisibleKeysForContainer']
const pinnedContainers: typeof import('./composable/storage')['pinnedContainers']
const provide: typeof import('vue')['provide']
const provideDetails: typeof import('./composable/showDetails')['provideDetails']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const provideLogDetails: typeof import('./composable/showLogDetails')['provideLogDetails']
const provideLoggingContext: typeof import('./composable/logContext')['provideLoggingContext']
const provideScrollContext: typeof import('./composable/scrollContext')['provideScrollContext']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
@@ -122,6 +126,7 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const scrollContextKey: typeof import('./composable/scrollContext')['scrollContextKey']
const search: typeof import('./stores/settings')['search']
const sessionHost: typeof import('./composable/storage')['sessionHost']
const setActivePinia: typeof import('pinia')['setActivePinia']
@@ -132,6 +137,7 @@ declare global {
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showAllContainers: typeof import('./stores/settings')['showAllContainers']
const showLogDetails: typeof import('./composable/showLogDetails')['showLogDetails']
const showStd: typeof import('./stores/settings')['showStd']
const showTimestamp: typeof import('./stores/settings')['showTimestamp']
const size: typeof import('./stores/settings')['size']
@@ -201,6 +207,7 @@ declare global {
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDetails: typeof import('./composable/showDetails')['useDetails']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
@@ -242,6 +249,7 @@ declare global {
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useLogDetails: typeof import('./composable/showLogDetails')['useLogDetails']
const useLogSearchContext: typeof import('./composable/logSearchContext')['useLogSearchContext']
const useLoggingContext: typeof import('./composable/logContext')['useLoggingContext']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
@@ -288,6 +296,7 @@ declare global {
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollContext: typeof import('./composable/scrollContext')['useScrollContext']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSearchFilter: typeof import('./composable/search')['useSearchFilter']
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
@@ -407,6 +416,7 @@ declare module 'vue' {
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly flattenJSONToMap: UnwrapRef<typeof import('./utils/index')['flattenJSONToMap']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
@@ -462,7 +472,9 @@ declare module 'vue' {
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly provideLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['provideLogDetails']>
readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']>
readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -479,6 +491,7 @@ declare module 'vue' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly scrollContextKey: UnwrapRef<typeof import('./composable/scrollContext')['scrollContextKey']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
@@ -489,6 +502,7 @@ declare module 'vue' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['showLogDetails']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
@@ -599,7 +613,7 @@ declare module 'vue' {
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']>
readonly useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
@@ -645,6 +659,7 @@ declare module 'vue' {
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['useScrollContext']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
@@ -757,6 +772,7 @@ declare module '@vue/runtime-core' {
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly flattenJSON: UnwrapRef<typeof import('./utils/index')['flattenJSON']>
readonly flattenJSONToMap: UnwrapRef<typeof import('./utils/index')['flattenJSONToMap']>
readonly formatBytes: UnwrapRef<typeof import('./utils/index')['formatBytes']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
@@ -812,7 +828,9 @@ declare module '@vue/runtime-core' {
readonly pinnedContainers: UnwrapRef<typeof import('./composable/storage')['pinnedContainers']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly provideLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['provideLogDetails']>
readonly provideLoggingContext: UnwrapRef<typeof import('./composable/logContext')['provideLoggingContext']>
readonly provideScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['provideScrollContext']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@@ -829,6 +847,7 @@ declare module '@vue/runtime-core' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly scrollContextKey: UnwrapRef<typeof import('./composable/scrollContext')['scrollContextKey']>
readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composable/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
@@ -839,6 +858,7 @@ declare module '@vue/runtime-core' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['showLogDetails']>
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
@@ -949,7 +969,7 @@ declare module '@vue/runtime-core' {
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useLogSearchContext: UnwrapRef<typeof import('./composable/logSearchContext')['useLogSearchContext']>
readonly useLogDetails: UnwrapRef<typeof import('./composable/showLogDetails')['useLogDetails']>
readonly useLoggingContext: UnwrapRef<typeof import('./composable/logContext')['useLoggingContext']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
@@ -995,6 +1015,7 @@ declare module '@vue/runtime-core' {
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollContext: UnwrapRef<typeof import('./composable/scrollContext')['useScrollContext']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSearchFilter: UnwrapRef<typeof import('./composable/search')['useSearchFilter']>
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
+9 -4
View File
@@ -14,7 +14,6 @@ declare module 'vue' {
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
'Carbon:play': typeof import('~icons/carbon/play')['default']
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
'Carbon:searchLocate': typeof import('~icons/carbon/search-locate')['default']
'Carbon:star': typeof import('~icons/carbon/star')['default']
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
'Carbon:stopFilledAlt': typeof import('~icons/carbon/stop-filled-alt')['default']
@@ -37,9 +36,9 @@ declare module 'vue' {
Dropdown: typeof import('./components/common/Dropdown.vue')['default']
DropdownMenu: typeof import('./components/common/DropdownMenu.vue')['default']
EventSource: typeof import('./components/LogViewer/EventSource.vue')['default']
FieldList: typeof import('./components/LogViewer/FieldList.vue')['default']
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
HostList: typeof import('./components/HostList.vue')['default']
HostMenu: typeof import('./components/HostMenu.vue')['default']
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
@@ -48,13 +47,12 @@ declare module 'vue' {
LabeledInput: typeof import('./components/common/LabeledInput.vue')['default']
Links: typeof import('./components/Links.vue')['default']
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogDetails: typeof import('./components/LogViewer/LogDetails.vue')['default']
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
LogList: typeof import('./components/LogViewer/LogList.vue')['default']
LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default']
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
'MaterialSymbols:collapseAllRounded': typeof import('~icons/material-symbols/collapse-all-rounded')['default']
'MaterialSymbols:expandAllRounded': typeof import('~icons/material-symbols/expand-all-rounded')['default']
'Mdi:announcement': typeof import('~icons/mdi/announcement')['default']
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
'Mdi:check': typeof import('~icons/mdi/check')['default']
@@ -63,10 +61,15 @@ declare module 'vue' {
'Mdi:chevronRight': typeof import('~icons/mdi/chevron-right')['default']
'Mdi:close': typeof import('~icons/mdi/close')['default']
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
'Mdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
'Mdi:docker': typeof import('~icons/mdi/docker')['default']
'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default']
'Mdi:hexagonMultiple': typeof import('~icons/mdi/hexagon-multiple')['default']
'Mdi:keyboardEsc': typeof import('~icons/mdi/keyboard-esc')['default']
'Mdi:magnify': typeof import('~icons/mdi/magnify')['default']
'Mdi:satelliteVariant': typeof import('~icons/mdi/satellite-variant')['default']
MobileMenu: typeof import('./components/common/MobileMenu.vue')['default']
MultiContainerActionToolbar: typeof import('./components/LogViewer/MultiContainerActionToolbar.vue')['default']
MultiContainerLog: typeof import('./components/MultiContainerViewer/MultiContainerLog.vue')['default']
MultiContainerStat: typeof import('./components/LogViewer/MultiContainerStat.vue')['default']
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
@@ -80,6 +83,7 @@ declare module 'vue' {
'Ph:computerTower': typeof import('~icons/ph/computer-tower')['default']
'Ph:controlBold': typeof import('~icons/ph/control-bold')['default']
'Ph:cpu': typeof import('~icons/ph/cpu')['default']
'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default']
'Ph:memory': typeof import('~icons/ph/memory')['default']
'Ph:stack': typeof import('~icons/ph/stack')['default']
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']
@@ -91,6 +95,7 @@ declare module 'vue' {
ScrollProgress: typeof import('./components/ScrollProgress.vue')['default']
Search: typeof import('./components/Search.vue')['default']
ServiceLog: typeof import('./components/ServiceViewer/ServiceLog.vue')['default']
SideDrawer: typeof import('./components/common/SideDrawer.vue')['default']
SideMenu: typeof import('./components/SideMenu.vue')['default']
SidePanel: typeof import('./components/SidePanel.vue')['default']
SimpleLogItem: typeof import('./components/LogViewer/SimpleLogItem.vue')['default']
+2
View File
@@ -24,6 +24,7 @@
v-model="selectedHost"
/>
</div>
<!-- @vue-ignore -->
<dropdown-menu
class="btn-sm"
v-model="selectedHost"
@@ -36,6 +37,7 @@
</div>
<div class="flex-1 text-right" v-show="containers.length > pageSizes[0]">
{{ $t("label.per-page") }}
<!-- @vue-ignore -->
<dropdown-menu
class="dropdown-left btn-xs md:btn-sm"
v-model="perPage"
@@ -115,7 +115,7 @@ const { container } = defineProps<{ container: Container }>();
const { actionStates, start, stop, restart } = useContainerActions(toRef(() => container));
const downloadParams = computed(() =>
Object.entries(streamConfig)
Object.entries(toValue(streamConfig))
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {}),
);
@@ -11,10 +11,9 @@
</a>
</div>
</template>
<template #default="{ setLoading }">
<template #default>
<ViewerWithSource
ref="viewer"
@loading-more="setLoading($event)"
:stream-source="useContainerStream"
:entity="container"
:visible-keys="visibleKeys"
@@ -27,6 +26,7 @@
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
import { useTemplateRef } from "vue";
const {
id,
@@ -43,10 +43,9 @@ const {
const close = defineEmit();
const store = useContainerStore();
const container = store.currentContainer($$(id));
const container = store.currentContainer(toRef(() => id));
const visibleKeys = persistentVisibleKeysForContainer(container);
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const viewer = useTemplateRef<ComponentExposed<typeof ViewerWithSource>>("viewer");
provideLoggingContext(toRef(() => [container.value]));
</script>
+3 -3
View File
@@ -31,9 +31,9 @@ function createFuzzySearchModal() {
initialState: {
container: {
containers: [
new Container("123", new Date(), "image", "test", "command", "host", {}, "status", "running", []),
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "status", "running", []),
new Container("567", new Date(), "image", "baz", "command", "host", {}, "status", "running", []),
new Container("123", new Date(), "image", "test", "command", "host", {}, "running", []),
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "running", []),
new Container("567", new Date(), "image", "baz", "command", "host", {}, "running", []),
],
},
},
+10 -5
View File
@@ -9,11 +9,16 @@
@keydown.down="selectedIndex = Math.min(selectedIndex + 1, data.length - 1)"
@keydown.up="selectedIndex = Math.max(selectedIndex - 1, 0)"
@keydown.enter.exact="selected(data[selectedIndex].item)"
@keydown.alt.enter="addColumn(data[selectedIndex])"
@keydown.alt.enter="addColumn(data[selectedIndex].item)"
v-model="query"
:placeholder="$t('placeholder.search-containers')"
/>
<mdi:keyboard-esc class="flex" />
<form method="dialog">
<button class="swap swap-rotate hover:swap-active">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
</div>
<div
class="dropdown-content !relative mt-2 max-h-[calc(100dvh-20rem)] w-full overflow-y-scroll rounded-md border-y-8 border-transparent bg-base-lighter px-2"
@@ -45,14 +50,14 @@
</div>
<DistanceTime :date="result.item.created" class="text-xs font-light" />
<a
<span
@click.stop.prevent="addColumn(result.item)"
:title="$t('tooltip.pin-column')"
class="hover:text-secondary"
>
<ic:sharp-keyboard-return v-if="index === selectedIndex" />
<cil:columns v-else />
</a>
<cil:columns v-else-if="result.item.type === 'container'" />
</span>
</a>
</li>
</ul>
@@ -8,15 +8,15 @@
</div>
</div>
<MultiContainerStat class="ml-auto" :containers="group.containers" />
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default="{ setLoading }">
<template #default>
<ViewerWithSource
ref="viewer"
@loading-more="setLoading($event)"
:stream-source="useGroupedStream"
:entity="group"
:visible-keys="visibleKeys"
:visible-keys="new Map<string[], boolean>()"
:show-container-name="true"
/>
</template>
@@ -26,6 +26,7 @@
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { GroupedContainers } from "@/models/Container";
import { ComponentExposed } from "vue-component-type-helpers";
const { name, scrollable = false } = defineProps<{
name: string;
@@ -33,6 +34,7 @@ const { name, scrollable = false } = defineProps<{
}>();
const containerStore = useContainerStore();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const { ready } = storeToRefs(containerStore);
@@ -41,6 +43,5 @@ const { customGroups } = storeToRefs(swarmStore);
const group = computed(() => customGroups.value.find((g) => g.name === name) ?? new GroupedContainers("", []));
const visibleKeys = ref<string[][]>([]);
provideLoggingContext(toRef(() => group.value.containers));
</script>
+29 -10
View File
@@ -2,20 +2,39 @@
<ul class="grid gap-4 md:grid-cols-[repeat(auto-fill,minmax(480px,1fr))]">
<li v-for="host in hosts" class="card bg-base-lighter">
<div class="card-body grid auto-cols-auto grid-flow-col justify-between gap-4">
<div class="overflow-hidden">
<div class="truncate text-xl font-semibold">
{{ host.name }} <span class="badge badge-error badge-xs p-1.5" v-if="!host.available">offline</span>
<div class="flex flex-col gap-2 overflow-hidden">
<div class="flex items-center gap-1 truncate text-xl font-semibold">
<HostIcon :type="host.type" class="flex-none" />
<div class="truncate">
{{ host.name }}
</div>
<span class="badge badge-error badge-xs gap-2 p-2" v-if="!host.available">
<carbon:warning />
offline
</span>
<span
class="badge badge-success badge-xs gap-2 p-2"
:class="{ 'badge-warning': config.version != host.agentVersion }"
v-else-if="host.type == 'agent'"
title="Dozzle Agent"
>
{{ host.agentVersion }}
</span>
</div>
<ul class="flex flex-row gap-2 text-sm md:gap-4">
<li><ph:cpu class="inline-block" /> {{ host.nCPU }} <span class="mobile-hidden">CPUs</span></li>
<li>
<ph:memory class="inline-block" /> {{ formatBytes(host.memTotal) }}
<ul class="flex flex-row gap-x-2 text-sm md:gap-3">
<li class="flex items-center gap-1"><ph:cpu /> {{ host.nCPU }} <span class="mobile-hidden">CPUs</span></li>
<li class="flex items-center gap-1">
<ph:memory /> {{ formatBytes(host.memTotal) }}
<span class="mobile-hidden">total</span>
</li>
</ul>
<div class="text-sm">
<octicon:container-24 class="inline-block" /> {{ $t("label.container", hostContainers[host.id]?.length) }}
</div>
<ul class="flex flex-row flex-wrap gap-x-2 text-sm md:gap-3">
<li class="flex items-center gap-1">
<octicon:container-24 class="inline-block" /> {{ $t("label.container", hostContainers[host.id]?.length) }}
</li>
<li class="flex items-center gap-1"><mdi:docker class="inline-block" /> {{ host.dockerVersion }}</li>
</ul>
</div>
<div class="flex flex-row gap-4 md:gap-8" v-if="weightedStats[host.id]">
+3 -5
View File
@@ -15,7 +15,7 @@
<ul class="menu p-0">
<li v-for="host in hosts" :key="host.id">
<a @click.prevent="setHost(host.id)" :class="{ 'pointer-events-none text-base-content/50': !host.available }">
<ph:computer-tower />
<HostIcon :type="host.type" />
{{ host.name }}
<span class="badge badge-error badge-xs p-1.5" v-if="!host.available">offline</span>
</a>
@@ -25,7 +25,6 @@
<template #right>
<ul class="containers menu p-0 [&_li.menu-title]:px-0">
<li v-for="{ label, containers, icon } in menuItems" :key="label">
<!-- @vue-ignore -->
<details :open="!collapsedGroups.has(label)" @toggle="updateCollapsedGroups($event, label)">
<summary class="font-light text-base-content/80">
<component :is="icon" />
@@ -33,9 +32,8 @@
<router-link
:to="{
name: '/merged/[name]',
query: { id: containers.map(({ id }) => id) },
params: { name: label.replace('label.', '') },
name: '/merged/[ids]',
params: { ids: containers.map(({ id }) => id).join(',') },
}"
class="btn btn-square btn-outline btn-primary btn-xs"
active-class="btn-active"
+1
View File
@@ -1,5 +1,6 @@
<template>
<div class="flex items-center justify-end gap-4">
<slot name="more-items"></slot>
<Dropdown class="dropdown-end" @closed="latestTag = latest?.tag ?? config.version">
<template #trigger>
<mdi:announcement class="size-6 -rotate-12" />
+9 -20
View File
@@ -1,10 +1,8 @@
<template>
<div class="group/item relative flex w-full gap-x-2" @click="expandToggle()">
<label class="swap swap-rotate invisible absolute -left-4 top-0.5 size-4 group-hover/item:visible">
<input type="checkbox" v-model="expanded" @click="expandToggle()" />
<material-symbols:expand-all-rounded class="swap-off text-secondary" />
<material-symbols:collapse-all-rounded class="swap-on text-secondary" />
</label>
<div
class="group/item relative flex w-full cursor-pointer gap-x-2 hover:bg-secondary/10"
@click="showLogDetails(logEntry)"
>
<div v-if="showContainerName">
<ContainerName :id="logEntry.containerID" />
</div>
@@ -18,41 +16,32 @@
<LogLevel :level="logEntry.level" />
</div>
<div>
<ul class="fields cursor-pointer space-x-4" :class="{ expanded }">
<ul class="fields space-x-4">
<li v-for="(value, name) in validValues" :key="name">
<span class="text-light">{{ name }}=</span><span class="font-bold" v-if="value === null">&lt;null&gt;</span>
<template v-else-if="Array.isArray(value)">
<span class="font-bold" v-html="markSearch(JSON.stringify(value))"> </span>
<span class="font-bold" v-html="JSON.stringify(value)"> </span>
</template>
<span class="font-bold" v-html="markSearch(value)" v-else></span>
<span class="font-bold" v-html="value" v-else></span>
</li>
<li class="text-light" v-if="Object.keys(validValues).length === 0">all values are hidden</li>
</ul>
<FieldList :fields="logEntry.unfilteredMessage" :expanded="expanded" :visible-keys="visibleKeys" />
</div>
<LogMessageActions
class="duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100"
:message="() => JSON.stringify(logEntry.message)"
:log-entry="logEntry"
/>
</div>
</template>
<script lang="ts" setup>
import { type ComplexLogEntry } from "@/models/LogEntry";
const { markSearch } = useSearchFilter();
const { logEntry, showContainerName = false } = defineProps<{
logEntry: ComplexLogEntry;
visibleKeys: string[][];
showContainerName?: boolean;
}>();
const [expanded, expandToggle] = useToggle();
const validValues = computed(() => {
return Object.fromEntries(Object.entries(logEntry.message).filter(([_, value]) => value !== undefined));
});
const showLogDetails = useLogDetails();
</script>
<style lang="postcss" scoped>
@@ -50,7 +50,7 @@ describe("<ContainerEventSource />", () => {
},
) {
settings.value.hourStyle = hourStyle;
search.searchFilter.value = searchFilter;
search.searchQueryFilter.value = searchFilter;
if (searchFilter) {
search.showSearch.value = true;
}
@@ -74,7 +74,10 @@ describe("<ContainerEventSource />", () => {
LogViewer,
},
provide: {
scrollingPaused: computed(() => false),
[scrollContextKey as symbol]: {
paused: computed(() => false),
loading: computed(() => false),
},
[loggingContextKey as symbol]: {
containers: computed(() => [{ id: "abc", image: "test:v123", host: "localhost" }]),
streamConfig: reactive({ stdout: true, stderr: true }),
@@ -88,7 +91,7 @@ describe("<ContainerEventSource />", () => {
},
props: {
streamSource: useContainerStream,
entity: new Container("abc", new Date(), "image", "name", "command", "localhost", {}, "status", "created", []),
entity: new Container("abc", new Date(), "image", "name", "command", "localhost", {}, "created", []),
},
});
}
@@ -143,24 +146,11 @@ describe("<ContainerEventSource />", () => {
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render messages with html entities", async () => {
const wrapper = createLogEventSource();
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render dates with 12 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "12" });
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
});
vi.runAllTimers();
@@ -172,25 +162,9 @@ describe("<ContainerEventSource />", () => {
test("should render dates with 24 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "24" });
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
});
vi.runAllTimers();
await nextTick();
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
});
test("should render messages with filter", async () => {
const wrapper = createLogEventSource({ searchFilter: "test" });
sources[sourceUrl].emitOpen();
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
});
sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"test bar", "id":2}`,
});
vi.runAllTimers();
await nextTick();
+4 -7
View File
@@ -1,22 +1,19 @@
<template>
<InfiniteLoader :onLoadMore="fetchMore" :enabled="enabled && messages.length > 50" />
<InfiniteLoader :onLoadMore="fetchMore" :enabled="!loadingMore && messages.length > 10" />
<slot :messages="messages"></slot>
</template>
<script lang="ts" setup generic="T">
import { LogStreamSource } from "@/composable/eventStreams";
const loadingMore = defineEmit<[value: boolean]>();
const { entity, streamSource } = $defineProps<{
streamSource: (t: Ref<T>) => LogStreamSource;
entity: T;
}>();
const { messages, loadOlderLogs, isLoadingMore } = streamSource($$(entity));
const { loadingMore } = useLoggingContext();
const beforeLoading = () => loadingMore(true);
const afterLoading = () => loadingMore(false);
const enabled = ref(true);
defineExpose({
@@ -25,10 +22,10 @@ defineExpose({
const fetchMore = async () => {
if (!isLoadingMore()) {
beforeLoading();
loadingMore.value = true;
enabled.value = false;
await loadOlderLogs();
afterLoading();
loadingMore.value = false;
enabled.value = true;
}
};
-74
View File
@@ -1,74 +0,0 @@
<template>
<ul v-if="expanded" ref="root" class="ml-8">
<li v-for="(value, name) in fields" :key="name">
<template v-if="isObject(value)">
<span class="text-light">{{ name }}=</span>
<FieldList :fields="value" :parent-key="parentKey.concat(name)" :visible-keys="visibleKeys" expanded />
</template>
<template v-else-if="Array.isArray(value)">
<a @click.stop="toggleField(name)" class="link-primary mr-2 cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}
</a>
<span class="text-light">{{ name }}=</span>[
<span class="font-bold" v-for="(item, index) in value">
<span v-html="JSON.stringify(item)"></span><span v-if="index !== value.length - 1">,</span>
</span>
]
</template>
<template v-else>
<a @click.stop="toggleField(name)" class="link-primary mr-2 cursor-pointer">
{{ hasField(name) ? "remove" : "add" }}
</a>
<span class="text-light">{{ name }}=</span><span class="font-bold" v-html="value"></span>
</template>
</li>
</ul>
</template>
<script lang="ts" setup>
import { arrayEquals, isObject } from "@/utils";
const {
fields,
expanded = false,
parentKey = [],
visibleKeys = [],
} = defineProps<{
fields: Record<string, any>;
expanded?: boolean;
parentKey?: string[];
visibleKeys?: string[][];
}>();
const root = ref<HTMLElement>();
async function toggleField(field: string) {
const index = fieldIndex(field);
if (index > -1) {
visibleKeys.splice(index, 1);
} else {
visibleKeys.push(parentKey.concat(field));
}
await nextTick();
root.value?.scrollIntoView({
block: "center",
});
}
function hasField(field: string) {
return fieldIndex(field) > -1;
}
function fieldIndex(field: string) {
const path = parentKey.concat(field);
return visibleKeys.findIndex((keys) => arrayEquals(toRaw(keys), toRaw(path)));
}
</script>
<style scoped lang="postcss">
.text-light {
@apply text-base-content/70;
}
</style>
+189
View File
@@ -0,0 +1,189 @@
<template>
<header class="flex items-center gap-4">
<Tag :data-level="entry.level" class="uppercase text-white" v-if="entry.level">{{ entry.level }}</Tag>
<h1 class="mobile-hidden text-lg">
<DateTime :date="entry.date" />
</h1>
<h2 class="text-sm"><DistanceTime :date="entry.date" /> on {{ entry.std }}</h2>
</header>
<div class="mt-8 flex flex-col gap-10">
<section class="grid grid-cols-3 gap-2">
<div>
<div class="font-thin">Container Name</div>
<div class="truncate text-lg font-bold">{{ container.name }}</div>
</div>
<div>
<div class="font-thin">Host</div>
<div class="truncate text-lg font-bold">
{{ hosts[container.host].name }}
</div>
</div>
<div>
<div class="font-thin">Image</div>
<div class="truncate text-lg font-bold">{{ container.image }}</div>
</div>
</section>
<section class="flex flex-col gap-2">
<div class="flex gap-2 font-thin">
Raw JSON
<UseClipboard v-slot="{ copy, copied }" :source="JSON.stringify(entry.unfilteredMessage)">
<button class="swap swap-flip outline-none" @click="copy()" :class="{ 'hover:swap-active': copied }">
<mdi:check class="swap-on" />
<mdi:content-copy class="swap-off" />
</button>
</UseClipboard>
</div>
<div class="max-h-48 overflow-scroll rounded border border-base-lighter bg-base-darker p-2">
<pre v-html="syntaxHighlight(entry.unfilteredMessage)"></pre>
</div>
</section>
<table class="table table-pin-rows table-fixed" v-if="entry instanceof ComplexLogEntry">
<caption class="caption-bottom">
Fields are sortable by dragging and dropping.
</caption>
<thead class="text-lg">
<tr>
<th class="w-60">Field</th>
<th class="mobile-hidden">Value</th>
<th class="w-20"><input type="checkbox" class="toggle toggle-primary" @change="toggleAll($event)" /></th>
</tr>
</thead>
<tbody ref="list">
<tr v-for="{ key, value, enabled } in fields" :key="key.join('.')" class="hover">
<td class="cursor-move break-all font-mono">
{{ key.join(".") }}
</td>
<td class="mobile-hidden truncate">
<code v-html="JSON.stringify(value)"></code>
</td>
<td>
<input type="checkbox" class="toggle toggle-primary" :checked="enabled" @change="toggleField(key)" />
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { ComplexLogEntry } from "@/models/LogEntry";
import { useSortable } from "@vueuse/integrations/useSortable";
import { UseClipboard } from "@vueuse/components";
const { entry } = defineProps<{ entry: ComplexLogEntry }>();
const { currentContainer } = useContainerStore();
const list = ref<HTMLElement>();
const container = currentContainer(toRef(() => entry.containerID));
const visibleKeys = persistentVisibleKeysForContainer(container);
const { hosts } = useHosts();
function toggleField(key: string[]) {
if (visibleKeys.value.size === 0) {
visibleKeys.value = new Map<string[], boolean>(fields.value.map(({ key }) => [key, true]));
}
const enabled = visibleKeys.value.get(key);
visibleKeys.value.set(key, !enabled);
}
function toggleAll(e: Event) {
if (visibleKeys.value.size === 0) {
visibleKeys.value = new Map<string[], boolean>(fields.value.map(({ key }) => [key, true]));
}
const enabled = e.target instanceof HTMLInputElement && e.target.checked;
for (const key of visibleKeys.value.keys()) {
visibleKeys.value.set(key, enabled);
}
}
const fields = computed({
get() {
const fieldsWithValue: { key: string[]; value: any; enabled: boolean }[] = [];
const allFields = flattenJSONToMap(entry.unfilteredMessage);
if (visibleKeys.value.size === 0) {
for (const [key, value] of allFields) {
fieldsWithValue.push({ key, value, enabled: true });
}
} else {
for (const [key, enabled] of visibleKeys.value) {
const value = getDeep(entry.unfilteredMessage, key);
fieldsWithValue.push({ key, value, enabled });
}
for (const [key, value] of allFields) {
if ([...visibleKeys.value.keys()].findIndex((k) => arrayEquals(k, key)) === -1) {
fieldsWithValue.push({ key, value, enabled: false });
}
}
}
return fieldsWithValue;
},
set(value) {
const map = new Map<string[], boolean>();
for (const { key, enabled } of value) {
map.set(key, enabled);
}
visibleKeys.value = map;
},
});
function syntaxHighlight(json: any) {
json = JSON.stringify(json, null, 2);
return json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g,
function (match: string) {
var cls = "json-number";
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = "json-key";
} else {
cls = "json-string";
}
} else if (/true|false/.test(match)) {
cls = "json-boolean";
} else if (/null/.test(match)) {
cls = "json-null";
}
return '<span class="' + cls + '">' + match + "</span>";
},
);
}
useSortable(list, fields);
</script>
<style lang="postcss" scoped>
.font-mono {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Consolas,
Liberation Mono,
monaco,
Menlo,
monospace;
}
pre {
:deep(.json-key) {
@apply text-blue;
}
:deep(.json-string) {
@apply text-green;
}
:deep(.json-number) {
@apply text-orange;
}
:deep(.json-boolean) {
@apply text-purple;
}
:deep(.json-null) {
@apply text-red;
}
}
</style>
+6 -5
View File
@@ -32,25 +32,26 @@ defineProps<{
margin-top: -0.4em;
align-self: flex-start;
}
</style>
<style lang="postcss">
[data-level="debug"],
[data-level="trace"] {
@apply bg-purple;
@apply !bg-purple;
}
[data-level="info"] {
@apply bg-green;
@apply !bg-green;
}
[data-level="error"],
[data-level="severe"],
[data-level="critical"],
[data-level="fatal"] {
@apply bg-red;
@apply !bg-red;
}
[data-level="warn"],
[data-level="warning"] {
@apply bg-orange;
@apply !bg-orange;
}
</style>
+38 -16
View File
@@ -1,40 +1,62 @@
<template>
<ul
class="events group py-4"
class="events group pt-4"
:class="{ 'disable-wrap': !softWrap, [size]: true, compact }"
v-if="messages.length > 0"
>
<li
v-for="item in messages"
ref="list"
:key="item.id"
:data-key="item.id"
:class="{ 'border border-secondary': toRaw(item) === toRaw(lastSelectedItem) }"
:data-time="item.date.getTime()"
class="group/entry"
>
<component
:is="item.getComponent()"
:log-entry="item"
:visible-keys="visibleKeys"
:show-container-name="showContainerName"
/>
<component :is="item.getComponent()" :log-entry="item" :show-container-name="showContainerName" />
</li>
</ul>
<div v-else class="m-4 text-center">
<span class="loading loading-ring loading-md text-primary"></span>
</div>
</template>
<script lang="ts" setup>
import { toRaw } from "vue";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
defineProps<{
const { loading, progress, currentDate } = useScrollContext();
const { messages } = defineProps<{
messages: LogEntry<string | JSONObject>[];
visibleKeys: string[][];
lastSelectedItem: LogEntry<string | JSONObject> | undefined;
showContainerName: boolean;
}>();
watchEffect(() => {
loading.value = messages.length === 0;
});
const { containers } = useLoggingContext();
const list = ref<HTMLElement[]>([]);
useIntersectionObserver(
list,
(entries) => {
if (containers.value.length != 1) return;
const container = containers.value[0];
for (const entry of entries) {
if (entry.isIntersecting) {
const time = entry.target.getAttribute("data-time");
if (time) {
const date = new Date(parseInt(time));
const diff = new Date().getTime() - container.created.getTime();
progress.value = (date.getTime() - container.created.getTime()) / diff;
currentDate.value = date;
}
}
}
},
{
rootMargin: "-10% 0px -10% 0px",
threshold: 1,
},
);
</script>
<style scoped lang="postcss">
.events {
@@ -7,31 +7,18 @@
>
<span
class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700"
@click="copyLogMessageToClipBoard()"
@click.prevent="copyLogMessageToClipBoard()"
>
<carbon:copy-file />
</span>
</div>
<div
class="flex min-w-[0.98rem] items-start justify-end align-bottom hover:cursor-pointer"
:title="t('log_actions.jump_to_context')"
v-if="isSearching()"
>
<a
class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700"
@click="handleJumpLineSelected($event, logEntry)"
:href="`#${logEntry.id}`"
>
<carbon:search-locate />
</a>
</div>
</div>
</template>
<script lang="ts" setup>
import { LogEntry, JSONObject } from "@/models/LogEntry";
const { message, logEntry } = defineProps<{
const { message } = defineProps<{
message: () => string;
logEntry: LogEntry<string | JSONObject>;
}>();
@@ -40,9 +27,6 @@ const { showToast } = useToast();
const { copy, isSupported, copied } = useClipboard();
const { t } = useI18n();
const { isSearching } = useSearchFilter();
const { handleJumpLineSelected } = useLogSearchContext();
async function copyLogMessageToClipBoard() {
await copy(message());
+35 -26
View File
@@ -1,43 +1,52 @@
<template>
<LogList
:messages="filtered"
:last-selected-item="lastSelectedItem"
:visible-keys="visibleKeys"
:show-container-name="showContainerName"
/>
<SideDrawer ref="drawer">
<LogDetails :entry="entry" v-if="entry && entry instanceof ComplexLogEntry" />
</SideDrawer>
<LogList :messages="visibleMessages" :show-container-name="showContainerName" />
</template>
<script lang="ts" setup>
import { useRouteHash } from "@vueuse/router";
import { type JSONObject, LogEntry } from "@/models/LogEntry";
import SideDrawer from "@/components/common/SideDrawer.vue";
import { ComplexLogEntry, type JSONObject, LogEntry } from "@/models/LogEntry";
const props = defineProps<{
messages: LogEntry<string | JSONObject>[];
visibleKeys: string[][];
visibleKeys: Map<string[], boolean>;
showContainerName: boolean;
}>();
const { messages, visibleKeys } = toRefs(props);
const { filteredPayload } = useVisibleFilter(visibleKeys);
const { filteredMessages } = useSearchFilter();
const { debouncedSearchFilter } = useSearchFilter();
const { streamConfig } = useLoggingContext();
const visible = filteredPayload(messages);
const filtered = filteredMessages(visible);
const drawer = ref<InstanceType<typeof SideDrawer>>() as Ref<InstanceType<typeof SideDrawer>>;
const { lastSelectedItem } = useLogSearchContext() as {
lastSelectedItem: Ref<LogEntry<string | JSONObject> | undefined>;
};
const routeHash = useRouteHash();
watch(
routeHash,
(hash) => {
if (hash) {
document.querySelector(`[data-key="${hash.substring(1)}"]`)?.scrollIntoView({ block: "center" });
}
},
{ immediate: true, flush: "post" },
);
const { entry } = provideLogDetails(drawer);
const visibleMessages = filteredPayload(messages);
const router = useRouter();
watchEffect(() => {
const query = {} as Record<string, string>;
if (debouncedSearchFilter.value !== "") {
query.search = debouncedSearchFilter.value;
}
if (!streamConfig.value.stderr) {
query.stderr = streamConfig.value.stderr.toString();
}
if (!streamConfig.value.stdout) {
query.stdout = streamConfig.value.stdout.toString();
}
router.push({
query,
replace: true,
});
});
</script>
<style scoped lang="postcss"></style>
@@ -0,0 +1,83 @@
<template>
<div class="dropdown dropdown-end dropdown-hover">
<label tabindex="0" class="btn btn-ghost btn-sm gap-0.5 px-2">
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
</label>
<ul tabindex="0" class="menu dropdown-content z-50 w-52 rounded-box bg-base p-1 shadow">
<li>
<a @click.prevent="clear()">
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a>
</li>
<li>
<a @click.prevent="showSearch = true">
<mdi:magnify /> {{ $t("toolbar.search") }}
<KeyShortcut char="f" />
</a>
</li>
<li class="line"></li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = true;
"
>
<div class="flex size-4 gap-0.5">
<template v-if="streamConfig.stderr && streamConfig.stdout">
<carbon:circle-solid class="w-2 text-red" />
<carbon:circle-solid class="w-2 text-blue" />
</template>
</div>
{{ $t("toolbar.show-all") }}
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = true;
streamConfig.stderr = false;
"
>
<div class="flex size-4 flex-col gap-1">
<carbon:circle-solid class="w-2 text-blue" v-if="!streamConfig.stderr && streamConfig.stdout" />
</div>
{{ $t("toolbar.show", { std: "STDOUT" }) }}
</a>
</li>
<li>
<a
@click="
streamConfig.stdout = false;
streamConfig.stderr = true;
"
>
<div class="flex size-4 flex-col gap-1">
<carbon:circle-solid class="w-2 text-red" v-if="streamConfig.stderr && !streamConfig.stdout" />
</div>
{{ $t("toolbar.show", { std: "STDERR" }) }}
</a>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
const { showSearch } = useSearchFilter();
const clear = defineEmit();
const { streamConfig } = useLoggingContext();
</script>
<style scoped lang="postcss">
li.line {
@apply h-px bg-base-content/20;
}
a {
@apply whitespace-nowrap;
}
</style>
@@ -32,8 +32,7 @@ const { showContainerName = false } = defineProps<{
showContainerName?: boolean;
}>();
const { markSearch } = useSearchFilter();
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
const colorize = (value: string) => ansiConvertor.toHtml(value);
const urlPattern = /(https?:\/\/[^\s]+)/g;
const linkify = (text: string) =>
text.replace(urlPattern, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
@@ -1,11 +1,5 @@
<template>
<EventSource
ref="source"
#default="{ messages }"
@loading-more="loadingMore($event)"
:stream-source="streamSource"
:entity="entity"
>
<EventSource ref="source" #default="{ messages }" :stream-source="streamSource" :entity="entity">
<LogViewer :messages="messages" :visible-keys="visibleKeys" :show-container-name="showContainerName" />
</EventSource>
</template>
@@ -16,13 +10,11 @@ import { LogStreamSource } from "@/composable/eventStreams";
const { streamSource, visibleKeys, showContainerName, entity } = defineProps<{
streamSource: (t: Ref<T>) => LogStreamSource;
visibleKeys: string[][];
visibleKeys: Map<string[], boolean>;
showContainerName: boolean;
entity: T;
}>();
const loadingMore = defineEmit<[value: boolean]>();
const source = $ref<InstanceType<typeof LogEventSource>>();
defineExpose({
@@ -1,19 +1,18 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ContainerEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
"<ul data-v-cf9ff940="" class="events group py-4 medium">
<li data-v-cf9ff940="" data-key="1" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2" visible-keys="">
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if-->
<!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
<!--v-if-->
</div>
</div>
</li>
@@ -21,19 +20,18 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
`;
exports[`<ContainerEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
"<ul data-v-cf9ff940="" class="events group py-4 medium">
<li data-v-cf9ff940="" data-key="1" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2" visible-keys="">
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if-->
<!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42</time></div>
</div>
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">foo bar</div>
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
<!--v-if-->
</div>
</div>
</li>
@@ -41,9 +39,9 @@ exports[`<ContainerEventSource /> > render html correctly > should render dates
`;
exports[`<ContainerEventSource /> > render html correctly > should render messages 1`] = `
"<ul data-v-cf9ff940="" class="events group py-4 medium">
<li data-v-cf9ff940="" data-key="1" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2" visible-keys="">
"<ul data-v-cf9ff940="" class="events group pt-4 medium">
<li data-v-cf9ff940="" data-key="1" data-time="1560336942459" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2">
<!--v-if-->
<!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
@@ -53,49 +51,6 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">This is a message.</div>
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
<!--v-if-->
</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > render html correctly > should render messages with filter 1`] = `
"<ul data-v-cf9ff940="" class="events group py-4 medium">
<li data-v-cf9ff940="" data-key="2" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2" visible-keys="">
<!--v-if-->
<!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap"><mark>test</mark> bar</div>
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
<div class="flex min-w-[0.98rem] items-start justify-end align-bottom hover:cursor-pointer" title="log_actions.jump_to_context"><a class="rounded bg-slate-800/60 px-1.5 py-1 text-primary hover:bg-slate-700" href="#2"><svg viewBox="0 0 32 32" width="1.2em" height="1.2em">
<path fill="currentColor" d="m30 28.586l-4.688-4.688a8.028 8.028 0 1 0-1.415 1.414L28.586 30zM19 25a6 6 0 1 1 6-6a6.007 6.007 0 0 1-6 6M2 12h8v2H2zM2 2h16v2H2zm0 5h16v2H2z"></path>
</svg></a></div>
</div>
</div>
</li>
</ul>"
`;
exports[`<ContainerEventSource /> > render html correctly > should render messages with html entities 1`] = `
"<ul data-v-cf9ff940="" class="events group py-4 medium">
<li data-v-cf9ff940="" data-key="1" class="group/entry">
<div data-v-a49e52d4="" data-v-cf9ff940="" class="relative flex w-full items-start gap-x-2" visible-keys="">
<!--v-if-->
<!--v-if-->
<div data-v-961504e7="" data-v-a49e52d4="" class="inline-flex items-center justify-center rounded bg-base-lighter px-2 py-[0.2em] select-none" size="small">
<div class="inline-flex gap-2 whitespace-nowrap text-blue"><time datetime="2019-06-12T10:55:42.459Z" class="mobile-hidden">06/12/2019</time><time datetime="2019-06-12T10:55:42.459Z">10:55:42 AM</time></div>
</div>
<div data-v-e625cddd="" data-v-a49e52d4="" class="mt-1.5 size-2.5 flex-none rounded-lg flex"></div>
<div data-v-a49e52d4="" class="log-wrapper whitespace-pre-wrap [word-break:break-word] group-[.disable-wrap]:whitespace-nowrap">&lt;test&gt;foo bar&lt;/test&gt;</div>
<div data-v-a49e52d4="" class="flex gap-2 duration-250 absolute -right-1 opacity-0 transition-opacity delay-150 group-hover/entry:opacity-100">
<!--v-if-->
<!--v-if-->
</div>
</div>
</li>
@@ -104,7 +59,17 @@ exports[`<ContainerEventSource /> > render html correctly > should render messag
exports[`<ContainerEventSource /> > renders loading correctly 1`] = `
"<div class="flex min-h-[1px] justify-center"><span class="loading loading-bars loading-md mt-4 text-primary" style="display: none;"></span></div>
<div data-v-cf9ff940="" class="m-4 text-center"><span data-v-cf9ff940="" class="loading loading-ring loading-md text-primary"></span></div>"
<dialog data-v-43493931="" class="modal-right modal items-start outline-none backdrop:bg-none">
<div data-v-43493931="" class="modal-box">
<form data-v-43493931="" method="dialog"><button data-v-43493931="" class="swap swap-rotate absolute right-4 top-4 outline-none hover:swap-active"><svg data-v-43493931="" viewBox="0 0 24 24" width="1.2em" height="1.2em" class="swap-off">
<path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"></path>
</svg><svg data-v-43493931="" viewBox="0 0 24 24" width="1.2em" height="1.2em" class="swap-on">
<path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"></path>
</svg></button></form>
</div>
<form data-v-43493931="" method="dialog" class="modal-backdrop"><button data-v-43493931="">close</button></form>
</dialog>
<!--v-if-->"
`;
exports[`<ContainerEventSource /> > should parse messages 1`] = `
@@ -8,15 +8,15 @@
</div>
</div>
<MultiContainerStat class="ml-auto" :containers="containers" />
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default="{ setLoading }">
<template #default>
<ViewerWithSource
ref="viewer"
@loading-more="setLoading($event)"
:stream-source="useMergedStream"
:entity="containers"
:visible-keys="visibleKeys"
:visible-keys="new Map<string[], boolean>()"
:show-container-name="true"
/>
</template>
@@ -25,6 +25,7 @@
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { ids = [], scrollable = false } = defineProps<{
ids?: string[];
@@ -32,12 +33,9 @@ const { ids = [], scrollable = false } = defineProps<{
}>();
const containerStore = useContainerStore();
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const { allContainersById, ready } = storeToRefs(containerStore);
const containers = computed(() => ids.map((id) => allContainersById.value[id]));
const visibleKeys = ref<string[][]>([]);
provideLoggingContext(containers);
</script>
+5 -5
View File
@@ -1,12 +1,12 @@
<template>
<div class="flex flex-col gap-8 px-4 py-4 md:px-8">
<section>
<Links />
<Links>
<template #more-items>
<Tag>{{ config.version }}</Tag>
</template>
</Links>
</section>
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
+26 -41
View File
@@ -1,60 +1,45 @@
<template>
<transition name="fadeout">
<div class="pointer-events-none relative inline-block" ref="root" v-show="!autoHide || show">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" class="fill-base-darker stroke-primary" />
</svg>
<div class="absolute inset-0 flex items-center justify-center font-light">
<template v-if="indeterminate">
<div class="text-4xl">&#8734;</div>
</template>
<template v-else-if="!isNaN(scrollProgress)">
<div class="inline-flex flex-col items-end gap-2" ref="root" v-show="!autoHide || show">
<div class="relative inline-block">
<svg width="100" height="100" viewBox="0 0 100 100" :class="{ indeterminate }">
<circle r="44" cx="50" cy="50" class="fill-base-darker stroke-primary" />
</svg>
<div class="absolute inset-0 flex items-center justify-center font-light">
<span class="text-4xl">
{{ Math.ceil(scrollProgress * 100) }}
{{ Math.ceil(progress * 100) }}
</span>
<span> % </span>
</template>
</div>
</div>
<DistanceTime :date="date" class="whitespace-nowrap text-sm" />
</div>
</transition>
</template>
<script lang="ts" setup>
const { indeterminate = false, autoHide = false } = defineProps<{
const {
indeterminate = false,
autoHide = false,
progress,
date = new Date(),
} = defineProps<{
indeterminate?: boolean;
autoHide?: boolean;
progress: number;
date?: Date;
}>();
const scrollProgress = ref(0);
const root = ref<HTMLElement>();
const pinnedLogsStore = usePinnedLogsStore();
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
const scrollElement = ref<HTMLElement | Document>((root.value?.closest("[data-scrolling]") as HTMLElement) ?? document);
const { y: scrollY } = useScroll(scrollElement as Ref<HTMLElement | Document>, { throttle: 100 });
const show = autoResetRef(false, 2000);
onMounted(() => {
watch(
pinnedLogs,
() => {
scrollElement.value = (root.value?.closest("[data-scrolling]") as HTMLElement) ?? document;
},
{ immediate: true, flush: "post" },
);
});
watchPostEffect(() => {
const parent =
scrollElement.value === document
? (scrollElement.value as Document).documentElement
: (scrollElement.value as HTMLElement);
scrollProgress.value = Math.max(0, Math.min(1, scrollY.value / (parent.scrollHeight - parent.clientHeight)));
if (autoHide) {
show.value = true;
}
});
watch(
() => progress,
() => {
if (autoHide) {
show.value = true;
}
},
);
</script>
<style scoped lang="postcss">
svg {
@@ -72,7 +57,7 @@ svg {
transition: stroke-dashoffset 250ms ease-out;
transform: rotate(-90deg);
transform-origin: 50% 50%;
stroke-dashoffset: calc(276.32px - v-bind(scrollProgress) * 276.32px);
stroke-dashoffset: calc(276.32px - v-bind(progress) * 276.32px);
stroke-dasharray: 276.32px 276.32px;
stroke-linecap: round;
stroke-width: 3;
+56 -32
View File
@@ -7,23 +7,37 @@
<slot name="header"></slot>
</header>
<main :data-scrolling="scrollable ? true : undefined" class="snap-y overflow-auto">
<div class="invisible mr-28 text-right md:visible" v-show="paused">
<ScrollProgress :indeterminate="loading" :auto-hide="!loading" class="!fixed top-16 z-10" />
<div class="invisible relative md:visible" v-show="scrollContext.paused">
<div class="absolute right-44 top-4">
<ScrollProgress
:indeterminate="loadingMore"
:auto-hide="!loadingMore"
:progress="scrollContext.progress"
:date="scrollContext.currentDate"
class="!fixed z-10 min-w-40"
/>
</div>
</div>
<div ref="scrollableContent">
<slot :setLoading="setLoading"></slot>
<slot></slot>
<div v-if="scrollContext.loading" class="m-4 text-center">
<span class="loading loading-ring loading-md text-primary"></span>
</div>
</div>
<div
class="animate-background h-1 bg-gradient-radial from-primary to-transparent to-70%"
v-show="!scrollContext.paused && !scrollContext.loading"
></div>
<div ref="scrollObserver" class="h-px"></div>
</main>
<div class="mr-16 text-right">
<transition name="fade">
<button
class="btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow transition-colors"
class="transition-colorsblur-xs dark btn btn-primary fixed bottom-8 rounded p-3 text-primary-content shadow"
:class="hasMore ? 'btn-secondary animate-bounce-fast text-secondary-content' : ''"
@click="scrollToBottom()"
v-show="paused"
v-show="scrollContext.paused"
>
<mdi:chevron-double-down />
</button>
@@ -35,43 +49,38 @@
<script lang="ts" setup>
const { scrollable = false } = defineProps<{ scrollable?: boolean }>();
let paused = $ref(false);
let hasMore = $ref(false);
let loading = $ref(false);
const hasMore = ref(false);
const scrollObserver = ref<HTMLElement>();
const scrollableContent = ref<HTMLElement>();
provide("scrollingPaused", $$(paused));
const scrollContext = provideScrollContext();
const mutationObserver = new MutationObserver((e) => {
if (!paused) {
scrollToBottom();
} else {
const record = e[e.length - 1];
const children = (record.target as HTMLElement).children;
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore = true;
}
}
});
const { loadingMore } = useLoggingContext();
const intersectionObserver = new IntersectionObserver((entries) => (paused = entries[0].intersectionRatio == 0), {
useIntersectionObserver(scrollObserver, ([entry]) => (scrollContext.paused = entry.intersectionRatio == 0), {
threshold: [0, 1],
rootMargin: "80px 0px",
rootMargin: "40px 0px",
});
onMounted(() => {
mutationObserver.observe(scrollableContent.value!, { childList: true, subtree: true });
intersectionObserver.observe(scrollObserver.value!);
});
useMutationObserver(
scrollableContent,
(records) => {
if (!scrollContext.paused) {
scrollToBottom();
} else {
const record = records[records.length - 1];
const children = (record.target as HTMLElement).children;
if (children[children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
hasMore.value = true;
}
}
},
{ childList: true, subtree: true },
);
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
scrollObserver.value?.scrollIntoView({ behavior });
hasMore = false;
}
function setLoading(value: boolean) {
loading = value;
hasMore.value = false;
}
</script>
<style scoped lang="postcss">
@@ -84,6 +93,21 @@ function setLoading(value: boolean) {
.fade-leave-to {
@apply opacity-0;
}
.animate-background {
animation: gradient-animation 2s ease infinite;
background-position: center;
}
@keyframes gradient-animation {
0%,
100% {
background-size: 100% 100%;
}
50% {
background-size: 140% 120%;
}
}
</style>
<style>
+6 -3
View File
@@ -7,14 +7,17 @@
ref="container"
:style="style"
>
<div class="input input-primary flex h-auto items-center !shadow-lg">
<div
class="input input-primary flex h-auto items-center !shadow-lg"
:class="!isValidQuery ? 'input-warning' : ''"
>
<mdi:magnify />
<input
class="input input-ghost w-72 flex-1"
type="text"
placeholder="Find / RegEx"
ref="input"
v-model="searchFilter"
v-model="searchQueryFilter"
@keyup.esc="resetSearch()"
/>
<a class="btn btn-circle btn-xs" @click="resetSearch()"> <mdi:close /></a>
@@ -26,7 +29,7 @@
<script lang="ts" setup>
const input = ref<HTMLInputElement>();
const container = ref<HTMLDivElement>();
const { searchFilter, showSearch, resetSearch } = useSearchFilter();
const { searchQueryFilter, showSearch, resetSearch, isValidQuery } = useSearchFilter();
const { style } = useDraggable(container);
@@ -11,15 +11,15 @@
</Tag>
</div>
<MultiContainerStat class="ml-auto" :containers="service.containers" />
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default="{ setLoading }">
<template #default>
<ViewerWithSource
ref="viewer"
@loading-more="setLoading($event)"
:stream-source="useServiceStream"
:entity="service"
:visible-keys="visibleKeys"
:visible-keys="new Map<string[], boolean>()"
:show-container-name="true"
/>
</template>
@@ -29,14 +29,14 @@
<script lang="ts" setup>
import { Service } from "@/models/Stack";
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { name, scrollable = false } = defineProps<{
scrollable?: boolean;
name: string;
}>();
const visibleKeys = ref<string[][]>([]);
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const store = useSwarmStore();
const { services } = storeToRefs(store) as unknown as { services: Ref<Service[]> };
const service = computed(() => services.value.find((s) => s.name === name) ?? new Service("", []));
+5 -6
View File
@@ -14,15 +14,15 @@
</Tag>
</div>
<MultiContainerStat class="ml-auto" :containers="stack.containers" />
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default="{ setLoading }">
<template #default>
<ViewerWithSource
ref="viewer"
@loading-more="setLoading($event)"
:stream-source="useStackStream"
:entity="stack"
:visible-keys="visibleKeys"
:visible-keys="new Map<string[], boolean>()"
:show-container-name="true"
/>
</template>
@@ -32,14 +32,13 @@
<script lang="ts" setup>
import { Stack } from "@/models/Stack";
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { name, scrollable = false } = defineProps<{
scrollable?: boolean;
name: string;
}>();
const visibleKeys = ref<string[][]>([]);
const viewer = ref<ComponentExposed<typeof ViewerWithSource>>();
const store = useSwarmStore();
const { stacks } = storeToRefs(store) as unknown as { stacks: Ref<Stack[]> };
const stack = computed(() => stacks.value.find((s) => s.name === name) ?? new Stack("", [], []));
+10
View File
@@ -0,0 +1,10 @@
<template>
<mdi:satellite-variant v-if="type == 'agent'" />
<ph:globe-simple v-else-if="type == 'remote'" />
<mdi:hexagon-multiple v-else-if="type == 'swarm'" />
<ph:computer-tower v-else />
</template>
<script setup lang="ts">
import { Host } from "@/stores/hosts";
const { type } = defineProps<{ type: Host["type"] }>();
</script>
+1 -43
View File
@@ -21,61 +21,19 @@
<transition name="fade">
<div v-show="show" class="h-[calc(100vh-60px)] overflow-auto">
<div class="mt-4 flex items-center justify-center gap-2">
<dropdown-menu
v-model="sessionHost"
:options="hosts"
defaultLabel="Hosts"
class="btn-sm"
v-if="config.hosts.length > 1"
/>
</div>
<ul class="menu">
<li class="menu-title">{{ $t("label.containers") }}</li>
<li v-for="item in sortedContainers" :key="item.id" :class="item.state">
<router-link
:to="{ name: '/container/[id]', params: { id: item.id } }"
active-class="active-primary"
class="truncate"
:title="item.name"
>
{{ item.name }}
</router-link>
</li>
</ul>
<SideMenu />
</div>
</transition>
</nav>
</template>
<script lang="ts" setup>
import { sessionHost } from "@/composable/storage";
const store = useContainerStore();
const route = useRoute();
const { visibleContainers } = storeToRefs(store);
const show = ref(false);
watch(route, () => {
show.value = false;
});
const sortedContainers = computed(() =>
visibleContainers.value
.filter((c) => c.host === sessionHost.value)
.sort((a, b) => {
if (a.state === "running" && b.state !== "running") {
return -1;
} else if (a.state !== "running" && b.state === "running") {
return 1;
} else {
return a.name.localeCompare(b.name);
}
}),
);
const hosts = computed(() => config.hosts.map(({ id, name }) => ({ value: id, label: name })));
</script>
<style scoped lang="postcss">
li.exited {
+34
View File
@@ -0,0 +1,34 @@
<template>
<dialog ref="panel" class="modal-right modal items-start outline-none backdrop:bg-none">
<div class="modal-box">
<form method="dialog">
<button class="swap swap-rotate absolute right-4 top-4 outline-none hover:swap-active">
<mdi:keyboard-esc class="swap-off" />
<mdi:close class="swap-on" />
</button>
</form>
<slot></slot>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</template>
<script setup lang="ts">
const panel = ref<HTMLDialogElement>();
defineExpose({
open: () => {
panel.value?.showModal();
},
});
</script>
<style scoped lang="postcss">
.modal-right :where(.modal-box) {
@apply fixed right-0 h-lvh max-h-screen max-w-3xl translate-x-24 scale-100 rounded-none bg-base-lighter shadow-none;
}
.modal-right[open] .modal-box {
@apply translate-x-0;
}
</style>
+80 -113
View File
@@ -1,5 +1,5 @@
import { type Ref } from "vue";
import { encodeXML } from "entities";
import { ShallowRef, type Ref } from "vue";
import debounce from "lodash.debounce";
import {
type LogEvent,
@@ -12,129 +12,79 @@ import {
import { Service, Stack } from "@/models/Stack";
import { Container, GroupedContainers } from "@/models/Container";
const { isSearching, debouncedSearchFilter } = useSearchFilter();
function parseMessage(data: string): LogEntry<string | JSONObject> {
const e = JSON.parse(data, (key, value) => {
if (typeof value === "string") {
return encodeXML(value);
}
return value;
}) as LogEvent;
const e = JSON.parse(data) as LogEvent;
return asLogEntry(e);
}
export function useContainerStream(container: Ref<Container>): LogStreamSource {
const { streamConfig } = useLoggingContext();
const url = computed(() => {
const params = Object.entries(streamConfig)
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
return withBase(
`/api/hosts/${container.value.host}/containers/${container.value.id}/logs/stream?${new URLSearchParams(params).toString()}`,
);
});
const loadMoreUrl = computed(() => {
const params = Object.entries(streamConfig)
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
return withBase(
`/api/hosts/${container.value.host}/containers/${container.value.id}/logs?${new URLSearchParams(params).toString()}`,
);
});
const url = computed(() => `/api/hosts/${container.value.host}/containers/${container.value.id}/logs/stream`);
const loadMoreUrl = computed(() => `/api/hosts/${container.value.host}/containers/${container.value.id}/logs`);
return useLogStream(url, loadMoreUrl);
}
export function useStackStream(stack: Ref<Stack>): LogStreamSource {
const { streamConfig } = useLoggingContext();
const url = computed(() => {
const params = Object.entries(streamConfig)
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
return withBase(`/api/stacks/${stack.value.name}/logs/stream?${new URLSearchParams(params).toString()}`);
});
return useLogStream(url);
return useLogStream(computed(() => `/api/stacks/${stack.value.name}/logs/stream`));
}
export function useGroupedStream(group: Ref<GroupedContainers>): LogStreamSource {
const { streamConfig } = useLoggingContext();
const url = computed(() => {
const params = Object.entries(streamConfig)
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
return withBase(`/api/groups/${group.value.name}/logs/stream?${new URLSearchParams(params).toString()}`);
});
return useLogStream(url);
return useLogStream(computed(() => `/api/groups/${group.value.name}/logs/stream`));
}
export function useMergedStream(containers: Ref<Container[]>): LogStreamSource {
const { streamConfig } = useLoggingContext();
const url = computed(() => {
const params = [
...Object.entries(streamConfig).map(([key, value]) => [key, value ? "1" : "0"]),
...containers.value.map((c) => ["id", c.id]),
];
return withBase(
`/api/hosts/${containers.value[0].host}/logs/mergedStream?${new URLSearchParams(params).toString()}`,
);
const ids = containers.value.map((c) => c.id).join(",");
return `/api/hosts/${containers.value[0].host}/logs/mergedStream/${ids}`;
});
return useLogStream(url);
}
export function useServiceStream(service: Ref<Service>): LogStreamSource {
const { streamConfig } = useLoggingContext();
const url = computed(() => {
const params = Object.entries(streamConfig)
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {});
return withBase(`/api/services/${service.value.name}/logs/stream?${new URLSearchParams(params).toString()}`);
});
return useLogStream(url);
return useLogStream(computed(() => `/api/services/${service.value.name}/logs/stream`));
}
export type LogStreamSource = ReturnType<typeof useLogStream>;
function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
let messages: LogEntry<string | JSONObject>[] = $ref([]);
let buffer: LogEntry<string | JSONObject>[] = $ref([]);
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
const messages: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]);
const buffer: ShallowRef<LogEntry<string | JSONObject>[]> = shallowRef([]);
const { paused: scrollingPaused } = useScrollContext();
function flushNow() {
if (messages.length > config.maxLogs) {
if (scrollingPaused) {
console.log("Skipping ", buffer.length, " log items");
if (messages.at(-1) instanceof SkippedLogsEntry) {
const lastEvent = messages.at(-1) as SkippedLogsEntry;
const lastItem = buffer.at(-1) as LogEntry<string | JSONObject>;
lastEvent.addSkippedEntries(buffer.length, lastItem);
if (messages.value.length + buffer.value.length > config.maxLogs) {
if (scrollingPaused.value === true) {
if (messages.value.at(-1) instanceof SkippedLogsEntry) {
const lastEvent = messages.value.at(-1) as SkippedLogsEntry;
const lastItem = buffer.value.at(-1) as LogEntry<string | JSONObject>;
lastEvent.addSkippedEntries(buffer.value.length, lastItem);
} else {
const firstItem = buffer.at(0) as LogEntry<string | JSONObject>;
const lastItem = buffer.at(-1) as LogEntry<string | JSONObject>;
messages.push(new SkippedLogsEntry(new Date(), buffer.length, firstItem, lastItem));
const firstItem = buffer.value.at(0) as LogEntry<string | JSONObject>;
const lastItem = buffer.value.at(-1) as LogEntry<string | JSONObject>;
messages.value = [
...messages.value,
new SkippedLogsEntry(new Date(), buffer.value.length, firstItem, lastItem),
];
}
buffer = [];
buffer.value = [];
} else {
messages.push(...buffer);
buffer = [];
messages = messages.slice(-config.maxLogs);
if (buffer.value.length > config.maxLogs / 2) {
messages.value = buffer.value.slice(-config.maxLogs / 2);
} else {
messages.value = [...messages.value, ...buffer.value].slice(-config.maxLogs);
}
buffer.value = [];
}
} else {
if (messages.length == 0) {
if (messages.value.length == 0) {
// sort the buffer the very first time because of multiple logs in parallel
buffer.sort((a, b) => a.date.getTime() - b.date.getTime());
buffer.value.sort((a, b) => a.date.getTime() - b.date.getTime());
}
messages.push(...buffer);
buffer = [];
messages.value = [...messages.value, ...buffer.value];
buffer.value = [];
}
}
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
@@ -149,19 +99,30 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
function clearMessages() {
flushBuffer.cancel();
messages = [];
buffer = [];
messages.value = [];
buffer.value = [];
}
const { streamConfig } = useLoggingContext();
const params = computed(() => {
const params = Object.entries(toValue(streamConfig))
.filter(([, value]) => value)
.reduce((acc, [key]) => ({ ...acc, [key]: "1" }), {} as Record<string, string>);
if (isSearching.value) {
params["filter"] = debouncedSearchFilter.value;
}
return params;
});
const urlWithParams = computed(() => withBase(`${url.value}?${new URLSearchParams(params.value).toString()}`));
function connect({ clear } = { clear: true }) {
close();
if (clear) {
clearMessages();
}
es = new EventSource(url.value);
if (clear) clearMessages();
es = new EventSource(urlWithParams.value);
es.addEventListener("container-event", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string };
const containerEvent = new ContainerEventLogEntry(
@@ -170,21 +131,28 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
new Date(),
event.name as "container-stopped" | "container-started",
);
buffer.push(containerEvent);
flushBuffer();
buffer.value = [...buffer.value, containerEvent];
flushBuffer.flush();
});
es.addEventListener("logs-backfill", (e) => {
const data = JSON.parse((e as MessageEvent).data) as LogEvent[];
const logs = data.map((e) => asLogEntry(e));
messages.value = [...logs, ...messages.value];
});
es.onmessage = (e) => {
if (e.data) {
buffer.push(parseMessage(e.data));
buffer.value = [...buffer.value, parseMessage(e.data)];
flushBuffer();
}
};
es.onerror = () => clearMessages();
}
watch(url, () => connect(), { immediate: true });
watch(urlWithParams, () => connect(), { immediate: true });
let fetchingInProgress = false;
@@ -192,8 +160,8 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
if (!loadMoreUrl) return;
if (fetchingInProgress) return;
const to = messages[0].date;
const last = messages[Math.min(messages.length - 1, 300)].date;
const to = messages.value[0].date;
const last = messages.value[Math.min(messages.value.length - 1, 300)].date;
const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta);
@@ -201,13 +169,12 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
const signal = abortController.signal;
fetchingInProgress = true;
try {
const stopWatcher = watchOnce(url, () => abortController.abort("stream changed"));
const logs = await (
await fetch(
`${loadMoreUrl.value}&${new URLSearchParams({ from: from.toISOString(), to: to.toISOString() }).toString()}`,
{ signal },
)
).text();
const moreParams = { ...params.value, from: from.toISOString(), to: to.toISOString(), minimum: "100" };
const urlWithMoreParams = computed(() =>
withBase(`${loadMoreUrl.value}?${new URLSearchParams(moreParams).toString()}`),
);
const stopWatcher = watchOnce(urlWithMoreParams, () => abortController.abort("stream changed"));
const logs = await (await fetch(urlWithMoreParams.value, { signal })).text();
stopWatcher();
if (logs && signal.aborted === false) {
@@ -215,7 +182,7 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
.trim()
.split("\n")
.map((line) => parseMessage(line));
messages.unshift(...newMessages);
messages.value = [...newMessages, ...messages.value];
}
} catch (e) {
console.error("Error loading older logs", e);
@@ -228,5 +195,5 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
const isLoadingMore = () => fetchingInProgress;
return { ...$$({ messages }), loadOlderLogs, isLoadingMore };
return { messages, loadOlderLogs, isLoadingMore };
}
+24 -10
View File
@@ -2,22 +2,36 @@ import { Container } from "@/models/Container";
type LogContext = {
streamConfig: { stdout: boolean; stderr: boolean };
containers: Ref<Container[]>;
containers: Container[];
loadingMore: boolean;
};
// export for testing
export const loggingContextKey = Symbol("loggingContext") as InjectionKey<LogContext>;
const searchParams = new URLSearchParams(window.location.search);
const stdout = searchParams.has("stdout") ? searchParams.get("stdout") === "true" : true;
const stderr = searchParams.has("stderr") ? searchParams.get("stderr") === "true" : true;
export const provideLoggingContext = (containers: Ref<Container[]>) => {
provide(loggingContextKey, {
streamConfig: reactive({ stdout: true, stderr: true }),
containers,
});
provide(
loggingContextKey,
reactive({
streamConfig: { stdout, stderr },
containers,
loadingMore: false,
}),
);
};
export const useLoggingContext = () => {
const context = inject(loggingContextKey);
if (!context) {
throw new Error("No logging context provided");
}
return context;
const context = inject(
loggingContextKey,
reactive({
streamConfig: { stdout: true, stderr: true },
containers: [],
loadingMore: false,
}),
);
return toRefs(context);
};
-16
View File
@@ -1,16 +0,0 @@
import { JSONObject, LogEntry } from "@/models/LogEntry";
const lastSelectedItem = ref<LogEntry<string | JSONObject> | undefined>(undefined);
export const useLogSearchContext = () => {
const { resetSearch } = useSearchFilter();
function handleJumpLineSelected(e: Event, item: LogEntry<string | JSONObject>) {
lastSelectedItem.value = item;
resetSearch();
}
return {
lastSelectedItem,
handleJumpLineSelected,
};
};
+33 -3
View File
@@ -1,14 +1,34 @@
import { Profile } from "@/stores/config";
export function useProfileStorage<K extends keyof Profile>(key: K, defaultValue: NonNullable<Profile[K]>) {
interface SerializerTransformer<T, U> {
from: (raw: U) => T;
to: (value: T) => U;
}
export function useProfileStorage<K extends keyof Profile>(
key: K,
defaultValue: NonNullable<Profile[K]>,
transformer?: SerializerTransformer<NonNullable<Profile[K]>, any>,
) {
const storageKey = "DOZZLE_" + key.toUpperCase();
const storage = useStorage<NonNullable<Profile[K]>>(storageKey, defaultValue, undefined, {
writeDefaults: false,
mergeDefaults: true,
serializer: transformer
? {
read: (raw) => transformer.from(JSON.parse(raw)),
write: (value) => JSON.stringify(transformer.to(value)),
}
: undefined,
onError: (e) => {
console.error(`Failed to read ${storageKey} from storage`, e);
},
});
if (config.profile?.[key]) {
if (storage.value instanceof Set && config.profile[key] instanceof Array) {
if (transformer) {
storage.value = transformer.from(config.profile[key]);
} else if (storage.value instanceof Set && config.profile[key] instanceof Array) {
storage.value = new Set([...(config.profile[key] as Iterable<any>)]) as unknown as NonNullable<Profile[K]>;
} else if (config.profile[key] instanceof Array) {
storage.value = config.profile[key] as NonNullable<Profile[K]>;
@@ -23,9 +43,19 @@ export function useProfileStorage<K extends keyof Profile>(key: K, defaultValue:
watch(
storage,
(value) => {
const transformedValue = transformer ? transformer.to(value) : value;
fetch(withBase("/api/profile"), {
method: "PATCH",
body: JSON.stringify({ [key]: value }, (_, value) => (value instanceof Set ? [...value] : value)),
body: JSON.stringify({ [key]: transformedValue }, (_, value) => {
if (value instanceof Set) {
return Array.from(value);
} else if (value instanceof Map) {
return Array.from(value.entries());
} else {
return value;
}
}),
});
},
{ deep: true },
+30
View File
@@ -0,0 +1,30 @@
type ScrollContext = {
loading: boolean;
paused: boolean;
progress: number;
currentDate: Date;
};
// export for testing
export const scrollContextKey = Symbol("scrollContext") as InjectionKey<ScrollContext>;
export const provideScrollContext = () => {
const context = defauleValue();
provide(scrollContextKey, context);
return context;
};
export const useScrollContext = () => {
const defaultValue = defauleValue();
const context = inject(scrollContextKey, defaultValue);
return toRefs(context);
};
function defauleValue() {
return reactive({
loading: false,
paused: false,
progress: 1,
currentDate: new Date(),
});
}
+24 -75
View File
@@ -1,85 +1,34 @@
import { type Ref } from "vue";
import { type LogEntry, type JSONObject, SimpleLogEntry, ComplexLogEntry } from "@/models/LogEntry";
import { encodeXML } from "entities";
const searchFilter = ref<string>("");
const debouncedSearchFilter = useDebounce(searchFilter);
const searchQueryFilter = ref<string>("");
const debouncedSearchFilter = refDebounced(searchQueryFilter);
const showSearch = ref(false);
function matchRecord(record: Record<string, any>, regex: RegExp): boolean {
for (const key in record) {
const value = record[key];
if (typeof value === "string" && regex.test(value)) {
return true;
}
if (isObject(value) && regex.test(JSON.stringify(value))) {
return true;
}
if (Array.isArray(value) && matchRecord(value, regex)) {
return true;
}
}
return false;
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.get("search") !== null && searchParams.get("search") !== "") {
searchQueryFilter.value = searchParams.get("search") || "";
showSearch.value = true;
}
function resetSearch() {
searchQueryFilter.value = "";
showSearch.value = false;
}
const isSearching = computed(() => showSearch.value && debouncedSearchFilter.value !== "");
const isValidQuery = computed(() => {
try {
new RegExp(searchQueryFilter.value);
return true;
} catch (e) {
return false;
}
});
export function useSearchFilter() {
const regex = $computed(() => {
const isSmartCase = debouncedSearchFilter.value === debouncedSearchFilter.value.toLowerCase();
return new RegExp(encodeXML(debouncedSearchFilter.value), isSmartCase ? "ig" : "g");
});
function filteredMessages(messages: Ref<LogEntry<string | JSONObject>[]>) {
return computed(() => {
if (debouncedSearchFilter.value && showSearch.value) {
try {
return messages.value.filter((d) => {
if (d instanceof SimpleLogEntry) {
return regex.test(d.message);
} else if (d instanceof ComplexLogEntry) {
return matchRecord(d.message, regex);
}
return false;
});
} catch (e) {
if (e instanceof SyntaxError) {
console.info(`Ignoring SyntaxError from search.`, e);
return messages.value;
}
throw e;
}
}
return messages.value;
});
}
function markSearch(log: { toString(): string }): string;
function markSearch(log: string[]): string[];
function markSearch(log: { toString(): string } | string[]) {
if (!debouncedSearchFilter.value) {
return log;
}
if (Array.isArray(log)) {
return log.map((d) => markSearch(d));
}
return log.toString().replace(regex, (match) => `<mark>${match}</mark>`);
}
function resetSearch() {
searchFilter.value = "";
showSearch.value = false;
}
function isSearching() {
return showSearch.value && searchFilter.value;
}
return {
filteredMessages,
searchFilter,
searchQueryFilter,
isValidQuery,
debouncedSearchFilter,
showSearch,
markSearch,
resetSearch,
isSearching,
};
+22
View File
@@ -0,0 +1,22 @@
import { JSONObject, LogEntry } from "@/models/LogEntry";
import SideDrawer from "@/components/common/SideDrawer.vue";
export const showLogDetails = Symbol("showLogDetails") as InjectionKey<
(logEntry: LogEntry<string | JSONObject>) => void
>;
export const provideLogDetails = (drawer: Ref<InstanceType<typeof SideDrawer>>) => {
const entry = ref<LogEntry<string | JSONObject>>();
provide(showLogDetails, (logEntry: LogEntry<string | JSONObject>) => {
entry.value = logEntry;
drawer.value?.open();
});
return { entry };
};
export const useLogDetails = () => {
const showDetails = inject(showLogDetails, () => {});
return showDetails;
};
+15 -11
View File
@@ -7,17 +7,21 @@ if (config.hosts.length === 1 && !sessionHost.value) {
sessionHost.value = config.hosts[0].id;
}
export function persistentVisibleKeysForContainer(container: Ref<Container>): Ref<string[][]> {
const storage = useProfileStorage("visibleKeys", {});
return computed(() => {
if (!(container.value.storageKey in storage.value)) {
// Returning a temporary ref here to avoid writing an empty array to storage
const visibleKeys = ref<string[][]>([]);
watchOnce(visibleKeys, () => (storage.value[container.value.storageKey] = visibleKeys.value), { deep: true });
return visibleKeys.value;
}
return storage.value[container.value.storageKey];
const storage = useProfileStorage("visibleKeys", new Map<string, Map<string[], boolean>>(), {
from(transformed: [string, [string[], boolean][]][]) {
return new Map(transformed.map(([key, value]) => [key, new Map(value)]));
},
to(value: Map<string, Map<string[], boolean>>) {
const outer = Array.from(value.entries());
const inner = outer.map(([key, value]) => [key, Array.from(value.entries())]);
return inner;
},
});
export function persistentVisibleKeysForContainer(container: Ref<Container>): Ref<Map<string[], boolean>> {
// Computed property to only store to storage when the value changes
return computed({
get: () => storage.value.get(container.value.storageKey) || new Map<string[], boolean>(),
set: (value: Map<string[], boolean>) => storage.value.set(container.value.storageKey, value),
});
}
+17 -9
View File
@@ -1,16 +1,24 @@
import { ComplexLogEntry, type JSONObject, type LogEntry } from "@/models/LogEntry";
import type { Ref } from "vue";
export function useVisibleFilter(visibleKeys: Ref<string[][]>) {
export function useVisibleFilter(visibleKeys: Ref<Map<string[], boolean>>) {
const { isSearching } = useSearchFilter();
function filteredPayload(messages: Ref<LogEntry<string | JSONObject>[]>) {
return computed(() => {
return messages.value.map((d) => {
if (d instanceof ComplexLogEntry) {
return ComplexLogEntry.fromLogEvent(d, visibleKeys);
} else {
return d;
}
});
return messages.value
.map((d) => {
if (d instanceof ComplexLogEntry) {
return ComplexLogEntry.fromLogEvent(d, visibleKeys);
} else {
return d;
}
})
.filter((d) => {
if (isSearching.value && d instanceof ComplexLogEntry) {
return Object.values(d.message).some((v) => JSON.stringify(v)?.includes("<mark>"));
} else {
return true;
}
});
});
}
+1
View File
@@ -17,6 +17,7 @@
--in: 65% 0.171 249.5;
--inc: 100% 0 0;
--er: 64% 0.218 28.85;
--su: 56% 0.119722 164.12;
--erc: 100% 0 0;
--wa: 70% 0.186 48.13;
--wac: 100% 0 0;
+2 -2
View File
@@ -3,8 +3,8 @@ import { createApp, App as VueApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
Object.values(import.meta.glob<{ install: (app: VueApp) => void }>("./modules/*.ts", { eager: true })).forEach(
(i) => i.install?.(app),
Object.values(import.meta.glob<{ install: (app: VueApp) => void }>("./modules/*.ts", { eager: true })).forEach((i) =>
i.install?.(app),
);
app.mount("#app");
-1
View File
@@ -35,7 +35,6 @@ export class Container {
public readonly command: string,
public readonly host: string,
public readonly labels = {} as Record<string, string>,
public status: string,
public state: ContainerState,
stats: Stat[],
public readonly group?: string,
+12 -9
View File
@@ -58,7 +58,7 @@ export class SimpleLogEntry extends LogEntry<string> {
}
export class ComplexLogEntry extends LogEntry<JSONObject> {
private readonly filteredMessage: ComputedRef<JSONObject>;
private readonly filteredMessage: ComputedRef<Record<string, any>>;
constructor(
message: JSONObject,
@@ -67,15 +67,18 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
date: Date,
public readonly level: Level,
public readonly std: Std,
visibleKeys?: Ref<string[][]>,
visibleKeys?: Ref<Map<string[], boolean>>,
) {
super(message, containerID, id, date, std, level);
if (visibleKeys) {
this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) {
if (visibleKeys.value.size === 0) {
return flattenJSON(message);
} else {
return visibleKeys.value.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
const keys = Array.from(visibleKeys.value.entries())
.filter(([, value]) => value)
.map(([key]) => key);
return keys.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
}
});
} else {
@@ -86,15 +89,15 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
return ComplexLogItem;
}
public get message(): JSONObject {
return this.filteredMessage.value;
public get message(): Record<string, any> {
return unref(this.filteredMessage);
}
public get unfilteredMessage(): JSONObject {
return this._message;
}
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry {
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<Map<string[], boolean>>): ComplexLogEntry {
return new ComplexLogEntry(
event._message,
event.containerID,
@@ -165,7 +168,7 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
event.id,
new Date(event.ts),
event.l,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
);
} else {
return new SimpleLogEntry(
@@ -175,7 +178,7 @@ export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
new Date(event.ts),
event.l,
event.p,
event.s === "unknown" ? "stderr" : event.s ?? "stderr",
event.s === "unknown" ? "stderr" : (event.s ?? "stderr"),
);
}
}
+43 -10
View File
@@ -1,23 +1,56 @@
import { type App } from "vue";
import { createI18n } from "vue-i18n";
import { locale } from "@/stores/settings";
import type { Locale } from "vue-i18n";
import messages from "@intlify/unplugin-vue-i18n/messages";
const localesMap = Object.fromEntries(
Object.entries(import.meta.glob("../../locales/*.yml")).map(([path, loadLocale]) => [
path.match(/([\w-]*)\.yml$/)?.[1],
loadLocale,
]),
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>;
const defaultLocale = messages?.hasOwnProperty(navigator.language)
? navigator.language
: navigator.language.slice(0, 2);
export const availableLocales = Object.keys(localesMap);
const i18n = createI18n({
function setI18nLanguage(lang: Locale) {
i18n.global.locale.value = lang;
return lang;
}
export const i18n = createI18n({
legacy: false,
locale: locale.value || defaultLocale,
locale: "",
fallbackLocale: "en",
messages,
messages: {},
});
watch(locale, (value) => {
i18n.global.locale.value = value || defaultLocale;
});
const loadedLanguages: string[] = [];
async function loadLanguage(lang: string, setLang = true): Promise<Locale> {
if (setLang) {
if (i18n.global.locale.value === lang) return setI18nLanguage(lang);
if (loadedLanguages.includes(lang)) return setI18nLanguage(lang);
}
const messages = await localesMap[lang]();
i18n.global.setLocaleMessage(lang, messages.default);
loadedLanguages.push(lang);
return setI18nLanguage(lang);
}
await loadLanguage("en", false); // load default language
const userLocale = computed(
() =>
locale.value ||
[navigator.language, navigator.language.slice(0, 2)].find((l) => availableLocales.includes(l)) ||
"en",
);
if (userLocale.value !== "en") {
await loadLanguage(userLocale.value);
}
watchEffect(() => loadLanguage(userLocale.value));
export const install = (app: App) => app.use(i18n);
export default i18n;
@@ -7,12 +7,12 @@
const containerStore = useContainerStore();
const { ready } = storeToRefs(containerStore);
const route = useRoute("/merged/[name]");
const route = useRoute("/merged/[ids]");
const pinnedLogsStore = usePinnedLogsStore();
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
const ids = toRef(() => route.query.id as string[]);
const ids = toRef(() => route.params.ids.split(","));
watchEffect(() => {
if (ready.value) {
+50 -34
View File
@@ -36,6 +36,7 @@
{{ $t("settings.locale") }}
</template>
<template #input>
<!-- @vue-ignore -->
<dropdown-menu
v-model="locale"
:options="[
@@ -52,6 +53,7 @@
</template>
<template #input>
<div class="flex gap-2">
<!-- @vue-ignore -->
<dropdown-menu
v-model="dateLocale"
:options="[
@@ -62,6 +64,7 @@
{ label: 'YYYY-MM-DD', value: 'en-CA' },
]"
/>
<!-- @vue-ignore -->
<dropdown-menu
v-model="hourStyle"
:options="[
@@ -79,6 +82,7 @@
{{ $t("settings.font-size") }}
</template>
<template #input>
<!-- @vue-ignore -->
<dropdown-menu
v-model="size"
:options="[
@@ -95,6 +99,7 @@
{{ $t("settings.color-scheme") }}
</template>
<template #input>
<!-- @vue-ignore -->
<dropdown-menu
v-model="lightTheme"
:options="[
@@ -108,7 +113,6 @@
</div>
<LogList
:messages="fakeMessages"
:visible-keys="keys"
:last-selected-item="undefined"
:show-container-name="false"
class="hidden overflow-hidden rounded-lg border border-base-content/50 shadow @3xl:block"
@@ -156,49 +160,61 @@ import {
locale,
} from "@/stores/settings";
const { t, availableLocales } = useI18n();
import { availableLocales, i18n } from "@/modules/i18n";
const { t } = useI18n();
setTitle(t("title.settings"));
const { latest, hasUpdate } = useReleases();
const keys = ref<string[][]>([]);
const now = new Date();
const hoursAgo = (hours: number) => {
const date = new Date();
const date = new Date(now);
date.setHours(date.getHours() - hours);
return date;
};
const fakeMessages = [
new SimpleLogEntry("This is a preview of the logs", "123", 1, hoursAgo(16), "info", undefined, "stdout"),
new SimpleLogEntry("A warning log looks like this", "123", 2, hoursAgo(12), "warn", undefined, "stdout"),
new SimpleLogEntry("This is a multi line error message", "123", 3, hoursAgo(7), "error", "start", "stderr"),
new SimpleLogEntry("with a second line", "123", 4, hoursAgo(2), "error", "middle", "stderr"),
new SimpleLogEntry("and finally third line.", "123", 5, new Date(), "error", "end", "stderr"),
new ComplexLogEntry(
{
message: "This is a complex log entry as json",
context: {
key: "value",
key2: "value2",
const fakeMessages = computedWithControl(
() => i18n.global.locale.value,
() => [
new SimpleLogEntry(t("settings.log.preview"), "123", 1, hoursAgo(16), "info", undefined, "stdout"),
new SimpleLogEntry(t("settings.log.warning"), "123", 2, hoursAgo(12), "warn", undefined, "stdout"),
new SimpleLogEntry(
t("settings.log.multi-line-error.start-line"),
"123",
3,
hoursAgo(7),
"error",
"start",
"stderr",
),
new SimpleLogEntry(
t("settings.log.multi-line-error.middle-line"),
"123",
4,
hoursAgo(2),
"error",
"middle",
"stderr",
),
new SimpleLogEntry(t("settings.log.multi-line-error.end-line"), "123", 5, new Date(), "error", "end", "stderr"),
new ComplexLogEntry(
{
message: t("settings.log.complex"),
context: {
key: "value",
key2: "value2",
},
},
},
"123",
6,
new Date(),
"info",
"stdout",
keys,
),
new SimpleLogEntry(
"This is a very very long message which would wrap by default. Disabling soft wraps would disable this. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
"123",
7,
new Date(),
"debug",
undefined,
"stderr",
),
];
"123",
6,
new Date(),
"info",
"stdout",
),
new SimpleLogEntry(t("settings.log.simple"), "123", 7, new Date(), "debug", undefined, "stderr"),
],
);
</script>
<style lang="postcss" scoped>
.has-underline {
+3 -7
View File
@@ -3,14 +3,12 @@ import { Host } from "@/stores/hosts";
const text = document.querySelector("script#config__json")?.textContent || "{}";
type HostWithoutAvailable = Omit<Host, "available">;
export interface Config {
version: string;
base: string;
maxLogs: number;
hostname: string;
hosts: HostWithoutAvailable[];
hosts: Host[];
authProvider: "simple" | "none" | "forward-proxy";
enableActions: boolean;
user?: {
@@ -24,7 +22,7 @@ export interface Config {
export interface Profile {
settings?: Settings;
pinned?: Set<string>;
visibleKeys?: { [key: string]: string[][] };
visibleKeys?: Map<string, Map<string[], boolean>>;
releaseSeen?: string;
collapsedGroups?: Set<string>;
}
@@ -32,14 +30,12 @@ export interface Profile {
const pageConfig = JSON.parse(text);
const config: Config = {
maxLogs: 600,
maxLogs: 400,
version: "v0.0.0",
hosts: [],
...pageConfig,
};
config.version = config.version.replace(/^v/, "");
export default Object.freeze(config);
export const withBase = (path: string) => `${config.base}${path}`;
+5 -6
View File
@@ -3,9 +3,10 @@ import { Ref, UnwrapNestedRefs } from "vue";
import type { ContainerHealth, ContainerJson, ContainerStat } from "@/types/Container";
import { Container } from "@/models/Container";
import i18n from "@/modules/i18n";
import { Host } from "./hosts";
const { showToast, removeToast } = useToast();
const { markHostAvailable } = useHosts();
const { updateHost } = useHosts();
// @ts-ignore
const { t } = i18n.global;
@@ -74,9 +75,9 @@ export const useContainerStore = defineStore("container", () => {
}
});
es.addEventListener("host-unavailable", (e) => {
const hostId = (e as MessageEvent).data;
markHostAvailable(hostId, false);
es.addEventListener("update-host", (e) => {
const host = JSON.parse((e as MessageEvent).data) as Host;
updateHost(host);
});
es.addEventListener("container-health", (e) => {
@@ -121,7 +122,6 @@ export const useContainerStore = defineStore("container", () => {
existingContainers.forEach((c) => {
const existing = allContainersById.value[c.id];
existing.status = c.status;
existing.state = c.state;
existing.health = c.health;
});
@@ -137,7 +137,6 @@ export const useContainerStore = defineStore("container", () => {
c.command,
c.host,
c.labels,
c.status,
c.state,
c.stats,
c.group,
+21 -13
View File
@@ -1,27 +1,35 @@
export type Host = {
name: string;
id: string;
name: string;
nCPU: number;
memTotal: number;
type: "agent" | "local" | "remote" | "swarm";
endpoint: string;
available: boolean;
dockerVersion: string;
agentVersion: string;
};
const hosts = computed(() =>
config.hosts.reduce(
(acc, item) => {
acc[item.id] = { ...item, available: true };
return acc;
},
{} as Record<string, Host>,
),
);
const markHostAvailable = (id: string, available: boolean) => {
hosts.value[id].available = available;
const hosts = ref(
config.hosts
.sort((a, b) => a.name.localeCompare(b.name))
.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, Host>,
),
);
const updateHost = (host: Host) => {
delete hosts.value[host.endpoint];
hosts.value[host.id] = host;
return host;
};
export function useHosts() {
return {
hosts,
markHostAvailable,
updateHost,
};
}
+1 -1
View File
@@ -23,7 +23,7 @@ declare module 'vue-router/auto-routes' {
'/container/[id]': RouteRecordInfo<'/container/[id]', '/container/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
'/group/[name]': RouteRecordInfo<'/group/[name]', '/group/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>,
'/login': RouteRecordInfo<'/login', '/login', Record<never, never>, Record<never, never>>,
'/merged/[name]': RouteRecordInfo<'/merged/[name]', '/merged/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>,
'/merged/[ids]': RouteRecordInfo<'/merged/[ids]', '/merged/:ids', { ids: ParamValue<true> }, { ids: ParamValue<false> }>,
'/service/[name]': RouteRecordInfo<'/service/[name]', '/service/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>,
'/settings': RouteRecordInfo<'/settings', '/settings', Record<never, never>, Record<never, never>>,
'/show': RouteRecordInfo<'/show', '/show', Record<never, never>, Record<never, never>>,
+17 -5
View File
@@ -16,16 +16,28 @@ export function isObject(value: any): value is Record<string, any> {
}
export function flattenJSON(obj: Record<string, any>, path: string[] = []) {
const result: Record<string, any> = {};
Object.keys(obj).forEach((key) => {
const map = flattenJSONToMap(obj);
const result = {} as Record<string, any>;
for (const [key, value] of map) {
result[key.join(".")] = value;
}
return result;
}
export function flattenJSONToMap(obj: Record<string, any>, path: string[] = []): Map<string[], any> {
const result = new Map<string[], any>();
for (const key of Object.keys(obj)) {
const value = obj[key];
const newPath = path.concat(key);
if (isObject(value)) {
Object.assign(result, flattenJSON(value, newPath));
for (const [k, v] of flattenJSONToMap(value, newPath)) {
result.set(k, v);
}
} else {
result[newPath.join(".")] = value;
result.set(newPath, value);
}
});
}
return result;
}
+1 -1
View File
@@ -94,7 +94,7 @@ services:
playwright:
container_name: playwright
image: mcr.microsoft.com/playwright:v1.45.2-jammy
image: mcr.microsoft.com/playwright:v1.47.0-jammy
working_dir: /app
volumes:
- .:/app
+1
View File
@@ -65,6 +65,7 @@ export default defineConfig({
{ text: "Actions", link: "/guide/actions" },
{ text: "Agent Mode", link: "/guide/agent" },
{ text: "Changing Base", link: "/guide/changing-base" },
{ text: "Container Names", link: "/guide/container-names" },
{ text: "Container Groups", link: "/guide/container-groups" },
{ text: "Data Analytics", link: "/guide/analytics" },
{ text: "Display Name", link: "/guide/hostname" },
+1 -5
View File
@@ -1,15 +1,11 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
const defaultFormatter = (value: number) => {
return value.toLocaleString();
};
const {
start,
end,
duration,
formatter = defaultFormatter,
formatter = (value: number) => value.toLocaleString(),
} = defineProps<{
start: number;
end: number;
+1 -3
View File
@@ -17,7 +17,6 @@ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir2
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
@@ -25,8 +24,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 8080:8080
environment:
DOZZLE_ENABLE_ACTIONS: true
environment: DOZZLE_ENABLE_ACTIONS=true
```
:::
+57 -2
View File
@@ -2,7 +2,7 @@
title: Agent Mode
---
# Agent Mode <Badge type="tip" text="Beta" />
# Agent Mode
Dozzle can run in agent mode which can expose Docker hosts to other Dozzle instances. All communication is done over a secured connection using TLS. This means that you can deploy Dozzle on a remote host and connect to it from your local machine.
@@ -97,7 +97,7 @@ Similar to Dozzle instance, you can change the agent's name by providing the `DO
::: code-group
```sh
docker run -v /var/run/docker.sock:/var/run/docker.sock -p 7007:7007 amir20/dozzle:agent agent --hostname my-special-name
docker run -v /var/run/docker.sock:/var/run/docker.sock -p 7007:7007 amir20/dozzle:latest agent --hostname my-special-name
```
```yaml [docker-compose.yml]
@@ -116,3 +116,58 @@ services:
:::
This will change the agent's name to `my-special-name` and reflected on the UI when connecting to the agent.
## Custom certificates
By default, Dozzle uses self-signed certificates for communication between agents. This is a private certificate which is only valid to other Dozzle instances. This is secure and recommended for most use cases. However, if Dozzle is exposed extenrally and an attacker knows exactly which port the agent is running on, then they can setup their own Dozzle instance and connect to the agent. To prevent this, you can provide your own certificates.
To provide custom certificates, you need to mount or use secrets to provide the certificates. Here is an example:
```yml
services:
agent:
image: amir20/dozzle:latest
command: agent
volumes:
- /var/run/docker.sock:/var/run/docker.sock
secrets:
- source: cert
target: /dozzle_cert.pem
- source: key
target: /dozzle_key.pem
ports:
- 7070:7070
secrets:
cert:
file: ./cert.pem
key:
file: ./key.pem
```
> [!TIP]
> Docker secrets are preferred for providing certificates. The can be created using `docker secret create` command or as the example above using `docker-compose.yml`. The same certificates should be provided to the Dozzle instance connecting to the agent.
This will mount the `cert.pem` and `key.pem` files to the agent. The agent will use these certificates for communication. The same certificates should be provided to the Dozzle instance connecting to the agent.
To generate certificates, you can use the following command:
```sh
$ openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048
$ openssl req -new -key key.pem -out request.csr -subj "/C=US/ST=California/L=San Francisco/O=My Company"
$ openssl x509 -req -in request.csr -signkey key.pem -out cert.pem -days 365
```
## Comparing agents with remote connection
Agents are similar to remote connections, but they have some advantages. Generally, agents are preferred over remote connections due to performance and security reasons. Here is a comparison:
| Feature | Agent | Remote Connection |
| ----------- | -------------------------- | ------------------------------- |
| Performance | Better with disturbed load | Worse on the UI |
| Security | Private SSL | Insecure or Docker TLS |
| Ease of use | Easy out of the box | Requires exposing Docker socket |
| Permissions | Full access to Docker | Can be controlled with a proxy |
| Reconnect | Automatically reconnects | Requires UI restart |
| Healthcheck | Built-in healthcheck | No healthcheck |
If you do plan to use remote connections, make sure to secure the connection using Docker TLS or a reverse proxy.
-6
View File
@@ -38,7 +38,6 @@ $ docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/dozzle/dat
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
@@ -86,7 +85,6 @@ $ docker run -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir20/do
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
@@ -113,8 +111,6 @@ In this mode, Dozzle expects the following headers:
::: code-group
```yaml [docker-compose.yml]
version: "3.3"
networks:
net:
driver: bridge
@@ -252,8 +248,6 @@ software. This section defines how Dozzle can be setup to use Cloudflare Zero
Trust for authentication.
```yaml [docker-compose.yml]
version: "3.3"
services:
dozzle:
image: amir20/dozzle:latest
-1
View File
@@ -13,7 +13,6 @@ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir2
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
-1
View File
@@ -23,7 +23,6 @@ docker run --label dev.dozzle.group=myapp hello-world
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: hello-world
+29
View File
@@ -0,0 +1,29 @@
---
title: Container Names
---
# Container Names
By default, Dozzle retrieves container names directly from Docker. This is usually sufficient, as these names can be customized using the `--name` flag in `docker run` commands or through the `container_name` field in Docker Compose services.
## Custom Names
In cases where modifying the container name itself isn't possible, you can override it by adding a `dev.dozzle.name` label to your container.
Here is an example using Docker Compose or Docker CLI:
::: code-group
```sh
docker run --label dev.dozzle.name=hello hello-world
```
```yaml [docker-compose.yml]
services:
dozzle:
image: hello-world
labels:
- dev.dozzle.name=hello
```
:::
-1
View File
@@ -7,7 +7,6 @@ title: Debugging
By default, Dozzle does not output a lot of logs. However this can be changed with `--level` flag. The default value is `info` which only prints limited logs. You can use `debug` or `trace` which will show details about memory, configuration and other stats. `DOZZLE_LEVEL` can be used in compose configurations. Below is an example of using `docker-compose.yml` file to enable `debug` level.
```yaml
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
-1
View File
@@ -13,7 +13,6 @@ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir2
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
+1 -2
View File
@@ -4,10 +4,9 @@ title: Healthcheck
# Adding healthcheck
Dozzle has internal support for healthcheck using `dozzle healthcheck` command. It is not enabled by default as it adds extra CPU usage. To use `healthcheck` you need to configure it. Below is an example that checks the health of Dozzle every 3 seconds.
Dozzle has internal support for healthcheck using `dozzle healthcheck` command. It is not enabled by default as it adds extra CPU usage. To use `healthcheck` you need to configure it. Below is an example that checks the health of Dozzle every 3 seconds.
```yaml
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
-1
View File
@@ -15,7 +15,6 @@ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir2
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
+1 -2
View File
@@ -21,7 +21,6 @@ docker run -v /var/log/system.log:/var/log/test.log alpine tail -f /var/log/test
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle-from-file:
container_name: dozzle-from-file
@@ -31,7 +30,7 @@ services:
command:
- tail
- -f
- /var/log/stream.log
- /var/log/stream.log
network_mode: none
restart: unless-stopped
```
+2 -4
View File
@@ -2,14 +2,14 @@
title: Remote Host Setup
---
# Remote Host Setup <Badge type="warning" text="Deprecated" />
# Remote Host Setup
Dozzle supports connecting to remote Docker hosts. This is useful when running Dozzle in a container and you want to monitor a different Docker host.
However, with Dozzle agents, you can connect to remote hosts without exposing the Docker socket. See the [agent](/guide/agent) page for more information.
> [!WARNING]
> Remote hosts will soon be deprecated in favor of agents. Agents provide a more secure way to connect to remote hosts. See the [agent](/guide/agent) page for more information. If you want keep using remote hosts then follow this discussion on [GitHub](https://github.com/amir20/dozzle/issues/3066).
> Remote hosts have been replaced with agents. Agents provide a more secure way to connect to remote hosts. Although remote hosts are still supported, it is recommended to use agents. See the [agent](/guide/agent) page for more information and examples. For comparison, see the [comparing agents with remote connections](/guide/agent#comparing-agents-with-remote-connection) section.
## Connecting to remote hosts with TLS
@@ -26,7 +26,6 @@ $ docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/certs:/cer
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
@@ -72,7 +71,6 @@ docker run --volume=/var/run/docker.sock:/var/run/docker.sock -p 8080:8080 amir2
```
```yaml [docker-compose.yml]
version: "3"
services:
dozzle:
image: amir20/dozzle:latest
+2 -2
View File
@@ -2,7 +2,7 @@
title: Swarm Mode
---
# Swarm Mode <Badge text="New" type="tip" />
# Swarm Mode
Dozzle supports Docker Swarm Mode starting from version 8. When using Swarm Mode, Dozzle will automatically discover services and custom groups. Dozzle does not use Swarm API internally as it is [limited](https://github.com/moby/moby/issues/33183). Dozzle implements its own grouping using swarm labels. Additionally, Dozzle merges stats for containers in a group. This means that you can see logs and stats for all containers in a group in one view. But it does mean each host needs to be setup with Dozzle.
@@ -38,7 +38,7 @@ networks:
Note that the `DOZZLE_MODE` environment variable is set to `swarm`. This tells Dozzle to automatically discover other Dozzle instances in the swarm. The `overlay` network is used to create the mesh network between the different Dozzle instances.
> [!NOTE]
> Due to implementation details, the name for the service must be exactly `dozzle`. This is because Dozzle uses the service name to discover other Dozzle instances in the swarm. In the future, it would make sense to query the Docker API for all services with the current container ID.
> Due to implementation details, <strike>the name for the service must be exactly `dozzle`</strike>. This is no longer required starting with version `v8.2`. You can name the service anything you want. The service name is automatically detected by Dozzle using `com.docker.swarm.service.name` label.
## Setting up simple authentication in Swarm Mode
+1 -1
View File
@@ -6,6 +6,6 @@
"dozzle": "workspace:*"
},
"dependencies": {
"@fontsource-variable/playfair-display": "^5.0.25"
"@fontsource-variable/playfair-display": "^5.0.27"
}
}
+5 -5
View File
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@fontsource-variable/playfair-display':
specifier: ^5.0.25
version: 5.0.25
specifier: ^5.0.27
version: 5.0.27
devDependencies:
dozzle:
specifier: workspace:*
@@ -18,9 +18,9 @@ importers:
packages:
'@fontsource-variable/playfair-display@5.0.25':
resolution: {integrity: sha512-q8tzLiURtXycJQ27YoppTNCEe0W2ZRXIeZ25nH9KoM6fsbWYZAm9ItY0aRoL/jn/GllsEcTSm8H2UKa7l4AxDw==}
'@fontsource-variable/playfair-display@5.0.27':
resolution: {integrity: sha512-VQ7YOmWSvE+ZPg6tP7KtPUIM5in25/fnH+0Y74Z126hGQoan4gIhr557grh5RyIeakIptYizzcj36RJIksrQ9w==}
snapshots:
'@fontsource-variable/playfair-display@5.0.25': {}
'@fontsource-variable/playfair-display@5.0.27': {}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+18
View File
@@ -0,0 +1,18 @@
services:
agent:
image: amir20/dozzle:pr-3196
command: agent
volumes:
- /var/run/docker.sock:/var/run/docker.sock
secrets:
- source: cert
target: /dozzle_cert.pem
- source: key
target: /dozzle_key.pem
ports:
- 7070:7070
secrets:
cert:
file: ./cert.pem
key:
file: ./key.pem
+2 -2
View File
@@ -1,6 +1,6 @@
services:
dozzle:
image: amir20/dozzle:pr-3097
dozzle-service:
image: amir20/dozzle:latest
environment:
- DOZZLE_LEVEL=debug
- DOZZLE_MODE=swarm
+21 -18
View File
@@ -4,7 +4,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alexflint/go-arg v1.5.1
github.com/beme/abide v0.0.0-20190723115211-635a09831760
github.com/docker/docker v27.0.3+incompatible
github.com/docker/docker v27.2.0+incompatible
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1
@@ -15,26 +15,26 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/sirupsen/logrus v1.9.3
github.com/spf13/afero v1.11.0
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/PuerkitoBio/goquery v1.9.2
github.com/cenkalti/backoff/v4 v4.3.0
github.com/PuerkitoBio/goquery v1.10.0
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/jwtauth/v5 v5.3.1
github.com/goccy/go-json v0.10.3
github.com/puzpuzpuz/xsync/v3 v3.4.0
github.com/samber/lo v1.46.0
github.com/rs/zerolog v1.33.0
github.com/samber/lo v1.47.0
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/yuin/goldmark v1.7.4
google.golang.org/grpc v1.65.0
golang.org/x/sync v0.8.0
google.golang.org/grpc v1.66.0
google.golang.org/protobuf v1.34.2
)
@@ -43,6 +43,7 @@ require (
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
@@ -53,25 +54,27 @@ require (
github.com/kr/pretty v0.2.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.5 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.1.0 // indirect
github.com/lestrrat-go/jwx/v2 v2.1.1 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
go.opentelemetry.io/otel v1.27.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect
gotest.tools/v3 v3.0.3 // indirect
)
go 1.22.5
go 1.23.1
+53 -42
View File
@@ -4,6 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
@@ -20,6 +22,7 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -28,8 +31,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnN
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -49,6 +52,7 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -71,18 +75,23 @@ github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.1.0 h1:0zs7Ya6+39qoit7gwAf+cYm1zzgS3fceIdo7RmQ5lkw=
github.com/lestrrat-go/jwx/v2 v2.1.0/go.mod h1:Xpw9QIaUGiIUD1Wx0NcY1sIHwFf8lDuZn/cmxtXYRys=
github.com/lestrrat-go/jwx/v2 v2.1.1 h1:Y2ltVl8J6izLYFs54BVcpXLv5msSW4o8eXwnzZLI32E=
github.com/lestrrat-go/jwx/v2 v2.1.1/go.mod h1:4LvZg7oxu6Q5VJwn7Mk/UwooNRnTHUpXBj2C4j3HNx0=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
@@ -98,22 +107,13 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.2.0 h1:9AzuUeF88YC5bK8u2vEG1Fpvu4wgpM1wfPIExfaaDxQ=
github.com/puzpuzpuz/xsync/v3 v3.2.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.3.0 h1:vX9b3zg+gIivLYwoHav6CoI9PylvXqdfhr/nFyu8O5o=
github.com/puzpuzpuz/xsync/v3 v3.3.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.3.1 h1:vZPJk3OOfoaSjy3cdTX3BZxhDCUVp9SqdHnd+ilGlbQ=
github.com/puzpuzpuz/xsync/v3 v3.3.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/samber/lo v1.43.0 h1:ts0VhPi8+ZQZFVLv/2Vkgt2Cds05FM2v3Enmv+YMBtg=
github.com/samber/lo v1.43.0/go.mod h1:w7R6fO7h2lrnx/s0bWcZ55vXJI89p5UPM6+kyDL373E=
github.com/samber/lo v1.44.0 h1:5il56KxRE+GHsm1IR+sZ/6J42NODigFiqCWpSc2dybA=
github.com/samber/lo v1.44.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/samber/lo v1.45.0 h1:TPK85Y30Lv9Jh8s3TrJeA94u1hwcbFA9JObx/vT6lYU=
github.com/samber/lo v1.45.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ=
github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
@@ -129,7 +129,6 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@@ -140,28 +139,30 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -175,13 +176,17 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -189,12 +194,16 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -206,6 +215,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -220,12 +231,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+58 -50
View File
@@ -5,15 +5,15 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"github.com/goccy/go-json"
"io"
"time"
"github.com/amir20/dozzle/internal/agent/pb"
"github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/utils"
log "github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
orderedmap "github.com/wk8/go-ordered-map/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@@ -23,16 +23,16 @@ import (
)
type Client struct {
client pb.AgentServiceClient
host docker.Host
conn *grpc.ClientConn
client pb.AgentServiceClient
conn *grpc.ClientConn
endpoint string
}
func NewClient(endpoint string, certificates tls.Certificate, opts ...grpc.DialOption) (*Client, error) {
caCertPool := x509.NewCertPool()
c, err := x509.ParseCertificate(certificates.Certificate[0])
if err != nil {
log.Fatalf("failed to parse certificate: %v", err)
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
caCertPool.AddCert(c)
tlsConfig := &tls.Config{
@@ -51,23 +51,11 @@ func NewClient(endpoint string, certificates tls.Certificate, opts ...grpc.DialO
}
client := pb.NewAgentServiceClient(conn)
info, err := client.HostInfo(context.Background(), &pb.HostInfoRequest{})
if err != nil {
conn.Close()
return nil, fmt.Errorf("failed to get host info: %w", err)
}
return &Client{
client: client,
conn: conn,
host: docker.Host{
ID: info.Host.Id,
Name: info.Host.Name,
NCPU: int(info.Host.CpuCores),
MemTotal: int64(info.Host.Memory),
Endpoint: endpoint,
},
client: client,
conn: conn,
endpoint: endpoint,
}, nil
}
@@ -138,7 +126,8 @@ func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *docker.Log
m, err := resp.Event.Message.UnmarshalNew()
if err != nil {
log.Fatalf("cannot unpack message %v", err)
log.Error().Err(err).Msg("agent client: failed to unmarshal message")
continue
}
var message any
@@ -148,8 +137,10 @@ func sendLogs(stream pb.AgentService_StreamLogsClient, events chan<- *docker.Log
case *pb.ComplexMessage:
message = jsonBytesToOrderedMap(m.Data)
default:
log.Fatalf("agent client: unknown type %T", m)
log.Error().Type("message", m).Msg("agent client: unknown message type")
continue
}
events <- &docker.LogEvent{
@@ -187,7 +178,7 @@ func (c *Client) StreamRawBytes(ctx context.Context, containerID string, since t
if err == io.EOF || err == context.Canceled {
return
} else {
log.Warnf("error while streaming raw bytes %v", err)
log.Error().Err(err).Msg("agent client: failed to receive raw bytes")
return
}
}
@@ -252,22 +243,18 @@ func (c *Client) StreamNewContainers(ctx context.Context, containers chan<- dock
return rpcErrToErr(err)
}
started := resp.Container.Started.AsTime()
containers <- docker.Container{
ID: resp.Container.Id,
Name: resp.Container.Name,
Image: resp.Container.Image,
Labels: resp.Container.Labels,
Group: resp.Container.Group,
ImageID: resp.Container.ImageId,
Created: resp.Container.Created.AsTime(),
State: resp.Container.State,
Status: resp.Container.Status,
Health: resp.Container.Health,
Host: resp.Container.Host,
Tty: resp.Container.Tty,
StartedAt: &started,
StartedAt: resp.Container.Started.AsTime(),
Command: resp.Container.Command,
}
}
@@ -290,28 +277,20 @@ func (c *Client) FindContainer(containerID string) (docker.Container, error) {
})
}
var startedAt *time.Time
if response.Container.Started != nil {
started := response.Container.Started.AsTime()
startedAt = &started
}
return docker.Container{
ID: response.Container.Id,
Name: response.Container.Name,
Image: response.Container.Image,
Labels: response.Container.Labels,
Group: response.Container.Group,
ImageID: response.Container.ImageId,
Created: response.Container.Created.AsTime(),
State: response.Container.State,
Status: response.Container.Status,
Health: response.Container.Health,
Host: response.Container.Host,
Tty: response.Container.Tty,
Command: response.Container.Command,
StartedAt: response.Container.Started.AsTime(),
Stats: utils.RingBufferFrom(300, stats),
StartedAt: startedAt,
}, nil
}
@@ -333,36 +312,65 @@ func (c *Client) ListContainers() ([]docker.Container, error) {
})
}
var startedAt *time.Time
if container.Started != nil {
started := container.Started.AsTime()
startedAt = &started
}
containers = append(containers, docker.Container{
ID: container.Id,
Name: container.Name,
Image: container.Image,
Labels: container.Labels,
Group: container.Group,
ImageID: container.ImageId,
Created: container.Created.AsTime(),
State: container.State,
Status: container.Status,
Health: container.Health,
Host: container.Host,
Tty: container.Tty,
Stats: utils.RingBufferFrom(300, stats),
Command: container.Command,
StartedAt: startedAt,
StartedAt: container.Started.AsTime(),
Stats: utils.RingBufferFrom(300, stats),
})
}
return containers, nil
}
func (c *Client) Host() docker.Host {
return c.host
func (c *Client) Host() (docker.Host, error) {
info, err := c.client.HostInfo(context.Background(), &pb.HostInfoRequest{})
if err != nil {
return docker.Host{
Endpoint: c.endpoint,
Type: "agent",
Available: false,
}, err
}
return docker.Host{
ID: info.Host.Id,
Name: info.Host.Name,
NCPU: int(info.Host.CpuCores),
MemTotal: int64(info.Host.Memory),
Endpoint: c.endpoint,
Type: "agent",
DockerVersion: info.Host.DockerVersion,
AgentVersion: info.Host.AgentVersion,
}, nil
}
func (c *Client) ContainerAction(containerId string, action docker.ContainerAction) error {
var containerAction pb.ContainerAction
switch action {
case docker.Start:
containerAction = pb.ContainerAction_Start
case docker.Stop:
containerAction = pb.ContainerAction_Stop
case docker.Restart:
containerAction = pb.ContainerAction_Restart
}
_, err := c.client.ContainerAction(context.Background(), &pb.ContainerActionRequest{ContainerId: containerId, Action: containerAction})
return err
}
func (c *Client) Close() error {
+36 -43
View File
@@ -94,41 +94,42 @@ func init() {
client = &MockedClient{}
client.On("ListContainers").Return([]docker.Container{
{
ID: "123456",
Name: "test",
Host: "localhost",
ID: "123456",
Name: "test",
Host: "localhost",
State: "running",
},
}, nil)
client.On("Host").Return(docker.Host{
ID: "localhost",
Endpoint: "local",
Name: "local",
})
client.On("ContainerEvents", mock.Anything, mock.AnythingOfType("chan<- docker.ContainerEvent")).Return(nil).Run(func(args mock.Arguments) {
time.Sleep(5 * time.Second)
})
client.On("FindContainer", "123456").Return(docker.Container{
ID: "123456",
Name: "test",
Host: "localhost",
Image: "test",
ImageID: "test",
StartedAt: &time.Time{},
State: "running",
Status: "running",
Health: "healthy",
Group: "test",
Command: "test",
Created: time.Time{},
Tty: true,
ID: "123456",
Name: "test",
Host: "localhost",
Image: "test",
State: "running",
Health: "healthy",
Group: "test",
Command: "test",
Tty: true,
Labels: map[string]string{
"test": "test",
},
Stats: utils.NewRingBuffer[docker.ContainerStat](300),
}, nil)
go RunServer(client, certs, lis)
server, _ := NewServer(client, certs, "test")
go server.Serve(lis)
}
func bufDialer(ctx context.Context, address string) (net.Conn, error) {
@@ -144,19 +145,15 @@ func TestFindContainer(t *testing.T) {
container, _ := rpc.FindContainer("123456")
assert.Equal(t, container, docker.Container{
ID: "123456",
Name: "test",
Host: "localhost",
Image: "test",
ImageID: "test",
StartedAt: &time.Time{},
State: "running",
Status: "running",
Health: "healthy",
Group: "test",
Command: "test",
Created: time.Time{},
Tty: true,
ID: "123456",
Name: "test",
Host: "localhost",
Image: "test",
State: "running",
Health: "healthy",
Group: "test",
Command: "test",
Tty: true,
Labels: map[string]string{
"test": "test",
},
@@ -174,19 +171,15 @@ func TestListContainers(t *testing.T) {
assert.Equal(t, containers, []docker.Container{
{
ID: "123456",
Name: "test",
Host: "localhost",
Image: "test",
ImageID: "test",
StartedAt: &time.Time{},
State: "running",
Status: "running",
Health: "healthy",
Group: "test",
Command: "test",
Created: time.Time{},
Tty: true,
ID: "123456",
Name: "test",
Host: "localhost",
Image: "test",
State: "running",
Health: "healthy",
Group: "test",
Command: "test",
Tty: true,
Labels: map[string]string{
"test": "test",
},
+233 -96
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc v5.27.1
// protoc v5.27.3
// source: rpc.proto
package pb
@@ -839,6 +839,99 @@ func (x *StreamContainerStartedResponse) GetContainer() *Container {
return nil
}
type ContainerActionRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
Action ContainerAction `protobuf:"varint,2,opt,name=action,proto3,enum=protobuf.ContainerAction" json:"action,omitempty"`
}
func (x *ContainerActionRequest) Reset() {
*x = ContainerActionRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_rpc_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ContainerActionRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ContainerActionRequest) ProtoMessage() {}
func (x *ContainerActionRequest) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[17]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ContainerActionRequest.ProtoReflect.Descriptor instead.
func (*ContainerActionRequest) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{17}
}
func (x *ContainerActionRequest) GetContainerId() string {
if x != nil {
return x.ContainerId
}
return ""
}
func (x *ContainerActionRequest) GetAction() ContainerAction {
if x != nil {
return x.Action
}
return ContainerAction_Start
}
type ContainerActionResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *ContainerActionResponse) Reset() {
*x = ContainerActionResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_rpc_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ContainerActionResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ContainerActionResponse) ProtoMessage() {}
func (x *ContainerActionResponse) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[18]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ContainerActionResponse.ProtoReflect.Descriptor instead.
func (*ContainerActionResponse) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{18}
}
var File_rpc_proto protoreflect.FileDescriptor
var file_rpc_proto_rawDesc = []byte{
@@ -924,58 +1017,72 @@ var file_rpc_proto_rawDesc = []byte{
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x32, 0x91, 0x06, 0x0a, 0x0c,
0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x55, 0x0a, 0x0e,
0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1f,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x0d, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x00, 0x30, 0x01, 0x12, 0x57, 0x0a, 0x10, 0x4c, 0x6f, 0x67, 0x73, 0x42, 0x65, 0x74, 0x77,
0x65, 0x65, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x73, 0x12, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x4c, 0x6f, 0x67, 0x73, 0x42, 0x65, 0x74, 0x77, 0x65, 0x65, 0x6e, 0x44,
0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f, 0x67,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x57, 0x0a,
0x0e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x61, 0x77, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12,
0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x52, 0x61, 0x77, 0x42, 0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x52, 0x61, 0x77, 0x42, 0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x51, 0x0a, 0x0c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x4e, 0x0a, 0x0b, 0x53, 0x74, 0x72,
0x65, 0x61, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x6f, 0x0a, 0x16, 0x53, 0x74, 0x72,
0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72,
0x74, 0x65, 0x64, 0x12, 0x27, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53,
0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74,
0x61, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x08, 0x48, 0x6f,
0x73, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x6f, 0x73,
0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42,
0x13, 0x5a, 0x11, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x67, 0x65, 0x6e,
0x74, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x6d, 0x0a, 0x16, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x19, 0x0a, 0x17, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xeb, 0x06, 0x0a, 0x0c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x52, 0x0a,
0x0d, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1e,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x00, 0x12, 0x4b, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x12,
0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f,
0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x57,
0x0a, 0x10, 0x4c, 0x6f, 0x67, 0x73, 0x42, 0x65, 0x74, 0x77, 0x65, 0x65, 0x6e, 0x44, 0x61, 0x74,
0x65, 0x73, 0x12, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x6f,
0x67, 0x73, 0x42, 0x65, 0x74, 0x77, 0x65, 0x65, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x57, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x52, 0x61, 0x77, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x61, 0x77, 0x42, 0x79,
0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x61, 0x77, 0x42,
0x79, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01,
0x12, 0x51, 0x0a, 0x0c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73,
0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x00, 0x30, 0x01, 0x12, 0x4e, 0x0a, 0x0b, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x53, 0x74, 0x61,
0x74, 0x73, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x00, 0x30, 0x01, 0x12, 0x6f, 0x0a, 0x16, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x12, 0x27, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x53, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x00, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x49, 0x6e, 0x66, 0x6f,
0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x6f, 0x73, 0x74,
0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x0f, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x00, 0x42, 0x13, 0x5a, 0x11, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f,
0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -990,7 +1097,7 @@ func file_rpc_proto_rawDescGZIP() []byte {
return file_rpc_proto_rawDescData
}
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
var file_rpc_proto_goTypes = []any{
(*ListContainersRequest)(nil), // 0: protobuf.ListContainersRequest
(*ListContainersResponse)(nil), // 1: protobuf.ListContainersResponse
@@ -1009,49 +1116,55 @@ var file_rpc_proto_goTypes = []any{
(*HostInfoResponse)(nil), // 14: protobuf.HostInfoResponse
(*StreamContainerStartedRequest)(nil), // 15: protobuf.StreamContainerStartedRequest
(*StreamContainerStartedResponse)(nil), // 16: protobuf.StreamContainerStartedResponse
(*Container)(nil), // 17: protobuf.Container
(*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp
(*LogEvent)(nil), // 19: protobuf.LogEvent
(*ContainerEvent)(nil), // 20: protobuf.ContainerEvent
(*ContainerStat)(nil), // 21: protobuf.ContainerStat
(*Host)(nil), // 22: protobuf.Host
(*ContainerActionRequest)(nil), // 17: protobuf.ContainerActionRequest
(*ContainerActionResponse)(nil), // 18: protobuf.ContainerActionResponse
(*Container)(nil), // 19: protobuf.Container
(*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp
(*LogEvent)(nil), // 21: protobuf.LogEvent
(*ContainerEvent)(nil), // 22: protobuf.ContainerEvent
(*ContainerStat)(nil), // 23: protobuf.ContainerStat
(*Host)(nil), // 24: protobuf.Host
(ContainerAction)(0), // 25: protobuf.ContainerAction
}
var file_rpc_proto_depIdxs = []int32{
17, // 0: protobuf.ListContainersResponse.containers:type_name -> protobuf.Container
17, // 1: protobuf.FindContainerResponse.container:type_name -> protobuf.Container
18, // 2: protobuf.StreamLogsRequest.since:type_name -> google.protobuf.Timestamp
19, // 3: protobuf.StreamLogsResponse.event:type_name -> protobuf.LogEvent
18, // 4: protobuf.LogsBetweenDatesRequest.since:type_name -> google.protobuf.Timestamp
18, // 5: protobuf.LogsBetweenDatesRequest.until:type_name -> google.protobuf.Timestamp
18, // 6: protobuf.StreamRawBytesRequest.since:type_name -> google.protobuf.Timestamp
18, // 7: protobuf.StreamRawBytesRequest.until:type_name -> google.protobuf.Timestamp
20, // 8: protobuf.StreamEventsResponse.event:type_name -> protobuf.ContainerEvent
21, // 9: protobuf.StreamStatsResponse.stat:type_name -> protobuf.ContainerStat
22, // 10: protobuf.HostInfoResponse.host:type_name -> protobuf.Host
17, // 11: protobuf.StreamContainerStartedResponse.container:type_name -> protobuf.Container
0, // 12: protobuf.AgentService.ListContainers:input_type -> protobuf.ListContainersRequest
2, // 13: protobuf.AgentService.FindContainer:input_type -> protobuf.FindContainerRequest
4, // 14: protobuf.AgentService.StreamLogs:input_type -> protobuf.StreamLogsRequest
6, // 15: protobuf.AgentService.LogsBetweenDates:input_type -> protobuf.LogsBetweenDatesRequest
7, // 16: protobuf.AgentService.StreamRawBytes:input_type -> protobuf.StreamRawBytesRequest
9, // 17: protobuf.AgentService.StreamEvents:input_type -> protobuf.StreamEventsRequest
11, // 18: protobuf.AgentService.StreamStats:input_type -> protobuf.StreamStatsRequest
15, // 19: protobuf.AgentService.StreamContainerStarted:input_type -> protobuf.StreamContainerStartedRequest
13, // 20: protobuf.AgentService.HostInfo:input_type -> protobuf.HostInfoRequest
1, // 21: protobuf.AgentService.ListContainers:output_type -> protobuf.ListContainersResponse
3, // 22: protobuf.AgentService.FindContainer:output_type -> protobuf.FindContainerResponse
5, // 23: protobuf.AgentService.StreamLogs:output_type -> protobuf.StreamLogsResponse
5, // 24: protobuf.AgentService.LogsBetweenDates:output_type -> protobuf.StreamLogsResponse
8, // 25: protobuf.AgentService.StreamRawBytes:output_type -> protobuf.StreamRawBytesResponse
10, // 26: protobuf.AgentService.StreamEvents:output_type -> protobuf.StreamEventsResponse
12, // 27: protobuf.AgentService.StreamStats:output_type -> protobuf.StreamStatsResponse
16, // 28: protobuf.AgentService.StreamContainerStarted:output_type -> protobuf.StreamContainerStartedResponse
14, // 29: protobuf.AgentService.HostInfo:output_type -> protobuf.HostInfoResponse
21, // [21:30] is the sub-list for method output_type
12, // [12:21] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
19, // 0: protobuf.ListContainersResponse.containers:type_name -> protobuf.Container
19, // 1: protobuf.FindContainerResponse.container:type_name -> protobuf.Container
20, // 2: protobuf.StreamLogsRequest.since:type_name -> google.protobuf.Timestamp
21, // 3: protobuf.StreamLogsResponse.event:type_name -> protobuf.LogEvent
20, // 4: protobuf.LogsBetweenDatesRequest.since:type_name -> google.protobuf.Timestamp
20, // 5: protobuf.LogsBetweenDatesRequest.until:type_name -> google.protobuf.Timestamp
20, // 6: protobuf.StreamRawBytesRequest.since:type_name -> google.protobuf.Timestamp
20, // 7: protobuf.StreamRawBytesRequest.until:type_name -> google.protobuf.Timestamp
22, // 8: protobuf.StreamEventsResponse.event:type_name -> protobuf.ContainerEvent
23, // 9: protobuf.StreamStatsResponse.stat:type_name -> protobuf.ContainerStat
24, // 10: protobuf.HostInfoResponse.host:type_name -> protobuf.Host
19, // 11: protobuf.StreamContainerStartedResponse.container:type_name -> protobuf.Container
25, // 12: protobuf.ContainerActionRequest.action:type_name -> protobuf.ContainerAction
0, // 13: protobuf.AgentService.ListContainers:input_type -> protobuf.ListContainersRequest
2, // 14: protobuf.AgentService.FindContainer:input_type -> protobuf.FindContainerRequest
4, // 15: protobuf.AgentService.StreamLogs:input_type -> protobuf.StreamLogsRequest
6, // 16: protobuf.AgentService.LogsBetweenDates:input_type -> protobuf.LogsBetweenDatesRequest
7, // 17: protobuf.AgentService.StreamRawBytes:input_type -> protobuf.StreamRawBytesRequest
9, // 18: protobuf.AgentService.StreamEvents:input_type -> protobuf.StreamEventsRequest
11, // 19: protobuf.AgentService.StreamStats:input_type -> protobuf.StreamStatsRequest
15, // 20: protobuf.AgentService.StreamContainerStarted:input_type -> protobuf.StreamContainerStartedRequest
13, // 21: protobuf.AgentService.HostInfo:input_type -> protobuf.HostInfoRequest
17, // 22: protobuf.AgentService.ContainerAction:input_type -> protobuf.ContainerActionRequest
1, // 23: protobuf.AgentService.ListContainers:output_type -> protobuf.ListContainersResponse
3, // 24: protobuf.AgentService.FindContainer:output_type -> protobuf.FindContainerResponse
5, // 25: protobuf.AgentService.StreamLogs:output_type -> protobuf.StreamLogsResponse
5, // 26: protobuf.AgentService.LogsBetweenDates:output_type -> protobuf.StreamLogsResponse
8, // 27: protobuf.AgentService.StreamRawBytes:output_type -> protobuf.StreamRawBytesResponse
10, // 28: protobuf.AgentService.StreamEvents:output_type -> protobuf.StreamEventsResponse
12, // 29: protobuf.AgentService.StreamStats:output_type -> protobuf.StreamStatsResponse
16, // 30: protobuf.AgentService.StreamContainerStarted:output_type -> protobuf.StreamContainerStartedResponse
14, // 31: protobuf.AgentService.HostInfo:output_type -> protobuf.HostInfoResponse
18, // 32: protobuf.AgentService.ContainerAction:output_type -> protobuf.ContainerActionResponse
23, // [23:33] is the sub-list for method output_type
13, // [13:23] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
}
func init() { file_rpc_proto_init() }
@@ -1265,6 +1378,30 @@ func file_rpc_proto_init() {
return nil
}
}
file_rpc_proto_msgTypes[17].Exporter = func(v any, i int) any {
switch v := v.(*ContainerActionRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_rpc_proto_msgTypes[18].Exporter = func(v any, i int) any {
switch v := v.(*ContainerActionResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -1272,7 +1409,7 @@ func file_rpc_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_rpc_proto_rawDesc,
NumEnums: 0,
NumMessages: 17,
NumMessages: 19,
NumExtensions: 0,
NumServices: 1,
},
+39 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.4.0
// - protoc v5.27.1
// - protoc v5.27.3
// source: rpc.proto
package pb
@@ -28,6 +28,7 @@ const (
AgentService_StreamStats_FullMethodName = "/protobuf.AgentService/StreamStats"
AgentService_StreamContainerStarted_FullMethodName = "/protobuf.AgentService/StreamContainerStarted"
AgentService_HostInfo_FullMethodName = "/protobuf.AgentService/HostInfo"
AgentService_ContainerAction_FullMethodName = "/protobuf.AgentService/ContainerAction"
)
// AgentServiceClient is the client API for AgentService service.
@@ -43,6 +44,7 @@ type AgentServiceClient interface {
StreamStats(ctx context.Context, in *StreamStatsRequest, opts ...grpc.CallOption) (AgentService_StreamStatsClient, error)
StreamContainerStarted(ctx context.Context, in *StreamContainerStartedRequest, opts ...grpc.CallOption) (AgentService_StreamContainerStartedClient, error)
HostInfo(ctx context.Context, in *HostInfoRequest, opts ...grpc.CallOption) (*HostInfoResponse, error)
ContainerAction(ctx context.Context, in *ContainerActionRequest, opts ...grpc.CallOption) (*ContainerActionResponse, error)
}
type agentServiceClient struct {
@@ -281,6 +283,16 @@ func (c *agentServiceClient) HostInfo(ctx context.Context, in *HostInfoRequest,
return out, nil
}
func (c *agentServiceClient) ContainerAction(ctx context.Context, in *ContainerActionRequest, opts ...grpc.CallOption) (*ContainerActionResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ContainerActionResponse)
err := c.cc.Invoke(ctx, AgentService_ContainerAction_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AgentServiceServer is the server API for AgentService service.
// All implementations must embed UnimplementedAgentServiceServer
// for forward compatibility
@@ -294,6 +306,7 @@ type AgentServiceServer interface {
StreamStats(*StreamStatsRequest, AgentService_StreamStatsServer) error
StreamContainerStarted(*StreamContainerStartedRequest, AgentService_StreamContainerStartedServer) error
HostInfo(context.Context, *HostInfoRequest) (*HostInfoResponse, error)
ContainerAction(context.Context, *ContainerActionRequest) (*ContainerActionResponse, error)
mustEmbedUnimplementedAgentServiceServer()
}
@@ -328,6 +341,9 @@ func (UnimplementedAgentServiceServer) StreamContainerStarted(*StreamContainerSt
func (UnimplementedAgentServiceServer) HostInfo(context.Context, *HostInfoRequest) (*HostInfoResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method HostInfo not implemented")
}
func (UnimplementedAgentServiceServer) ContainerAction(context.Context, *ContainerActionRequest) (*ContainerActionResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ContainerAction not implemented")
}
func (UnimplementedAgentServiceServer) mustEmbedUnimplementedAgentServiceServer() {}
// UnsafeAgentServiceServer may be embedded to opt out of forward compatibility for this service.
@@ -521,6 +537,24 @@ func _AgentService_HostInfo_Handler(srv interface{}, ctx context.Context, dec fu
return interceptor(ctx, in, info, handler)
}
func _AgentService_ContainerAction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ContainerActionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AgentServiceServer).ContainerAction(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AgentService_ContainerAction_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AgentServiceServer).ContainerAction(ctx, req.(*ContainerActionRequest))
}
return interceptor(ctx, in, info, handler)
}
// AgentService_ServiceDesc is the grpc.ServiceDesc for AgentService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -540,6 +574,10 @@ var AgentService_ServiceDesc = grpc.ServiceDesc{
MethodName: "HostInfo",
Handler: _AgentService_HostInfo_Handler,
},
{
MethodName: "ContainerAction",
Handler: _AgentService_ContainerAction_Handler,
},
},
Streams: []grpc.StreamDesc{
{
+105 -29
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc v5.27.1
// protoc v5.27.3
// source: types.proto
package pb
@@ -22,6 +22,55 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ContainerAction int32
const (
ContainerAction_Start ContainerAction = 0
ContainerAction_Stop ContainerAction = 1
ContainerAction_Restart ContainerAction = 2
)
// Enum value maps for ContainerAction.
var (
ContainerAction_name = map[int32]string{
0: "Start",
1: "Stop",
2: "Restart",
}
ContainerAction_value = map[string]int32{
"Start": 0,
"Stop": 1,
"Restart": 2,
}
)
func (x ContainerAction) Enum() *ContainerAction {
p := new(ContainerAction)
*p = x
return p
}
func (x ContainerAction) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ContainerAction) Descriptor() protoreflect.EnumDescriptor {
return file_types_proto_enumTypes[0].Descriptor()
}
func (ContainerAction) Type() protoreflect.EnumType {
return &file_types_proto_enumTypes[0]
}
func (x ContainerAction) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ContainerAction.Descriptor instead.
func (ContainerAction) EnumDescriptor() ([]byte, []int) {
return file_types_proto_rawDescGZIP(), []int{0}
}
type Container struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -30,7 +79,7 @@ type Container struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"`
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // deprecated
State string `protobuf:"bytes,5,opt,name=state,proto3" json:"state,omitempty"`
ImageId string `protobuf:"bytes,6,opt,name=ImageId,proto3" json:"ImageId,omitempty"`
Created *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created,proto3" json:"created,omitempty"`
@@ -519,6 +568,8 @@ type Host struct {
OsType string `protobuf:"bytes,8,opt,name=osType,proto3" json:"osType,omitempty"`
CpuCores uint32 `protobuf:"varint,9,opt,name=cpuCores,proto3" json:"cpuCores,omitempty"`
Memory uint64 `protobuf:"varint,10,opt,name=memory,proto3" json:"memory,omitempty"`
AgentVersion string `protobuf:"bytes,11,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"`
DockerVersion string `protobuf:"bytes,12,opt,name=dockerVersion,proto3" json:"dockerVersion,omitempty"`
}
func (x *Host) Reset() {
@@ -623,6 +674,20 @@ func (x *Host) GetMemory() uint64 {
return 0
}
func (x *Host) GetAgentVersion() string {
if x != nil {
return x.AgentVersion
}
return ""
}
func (x *Host) GetDockerVersion() string {
if x != nil {
return x.DockerVersion
}
return ""
}
var File_types_proto protoreflect.FileDescriptor
var file_types_proto_rawDesc = []byte{
@@ -698,7 +763,7 @@ var file_types_proto_rawDesc = []byte{
0x09, 0x52, 0x07, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12,
0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f,
0x73, 0x74, 0x22, 0xe5, 0x02, 0x0a, 0x04, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x73, 0x74, 0x22, 0xaf, 0x03, 0x0a, 0x04, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03,
@@ -716,13 +781,21 @@ var file_types_proto_rawDesc = []byte{
0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63,
0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x63,
0x70, 0x75, 0x43, 0x6f, 0x72, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72,
0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x1a,
0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x13, 0x5a, 0x11, 0x69, 0x6e,
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x62, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12,
0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x56, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x6f, 0x63, 0x6b,
0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62,
0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x3a, 0x02, 0x38, 0x01, 0x2a, 0x33, 0x0a, 0x0f, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74,
0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07,
0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x10, 0x02, 0x42, 0x13, 0x5a, 0x11, 0x69, 0x6e, 0x74,
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x62, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -737,28 +810,30 @@ func file_types_proto_rawDescGZIP() []byte {
return file_types_proto_rawDescData
}
var file_types_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_types_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_types_proto_goTypes = []any{
(*Container)(nil), // 0: protobuf.Container
(*ContainerStat)(nil), // 1: protobuf.ContainerStat
(*LogEvent)(nil), // 2: protobuf.LogEvent
(*SimpleMessage)(nil), // 3: protobuf.SimpleMessage
(*ComplexMessage)(nil), // 4: protobuf.ComplexMessage
(*ContainerEvent)(nil), // 5: protobuf.ContainerEvent
(*Host)(nil), // 6: protobuf.Host
nil, // 7: protobuf.Container.LabelsEntry
nil, // 8: protobuf.Host.LabelsEntry
(*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
(*anypb.Any)(nil), // 10: google.protobuf.Any
(ContainerAction)(0), // 0: protobuf.ContainerAction
(*Container)(nil), // 1: protobuf.Container
(*ContainerStat)(nil), // 2: protobuf.ContainerStat
(*LogEvent)(nil), // 3: protobuf.LogEvent
(*SimpleMessage)(nil), // 4: protobuf.SimpleMessage
(*ComplexMessage)(nil), // 5: protobuf.ComplexMessage
(*ContainerEvent)(nil), // 6: protobuf.ContainerEvent
(*Host)(nil), // 7: protobuf.Host
nil, // 8: protobuf.Container.LabelsEntry
nil, // 9: protobuf.Host.LabelsEntry
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
(*anypb.Any)(nil), // 11: google.protobuf.Any
}
var file_types_proto_depIdxs = []int32{
9, // 0: protobuf.Container.created:type_name -> google.protobuf.Timestamp
9, // 1: protobuf.Container.started:type_name -> google.protobuf.Timestamp
7, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
1, // 3: protobuf.Container.stats:type_name -> protobuf.ContainerStat
10, // 4: protobuf.LogEvent.message:type_name -> google.protobuf.Any
9, // 5: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
8, // 6: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
10, // 0: protobuf.Container.created:type_name -> google.protobuf.Timestamp
10, // 1: protobuf.Container.started:type_name -> google.protobuf.Timestamp
8, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
2, // 3: protobuf.Container.stats:type_name -> protobuf.ContainerStat
11, // 4: protobuf.LogEvent.message:type_name -> google.protobuf.Any
10, // 5: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
9, // 6: protobuf.Host.labels:type_name -> protobuf.Host.LabelsEntry
7, // [7:7] is the sub-list for method output_type
7, // [7:7] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
@@ -862,13 +937,14 @@ func file_types_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_types_proto_rawDesc,
NumEnums: 0,
NumEnums: 1,
NumMessages: 9,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_types_proto_goTypes,
DependencyIndexes: file_types_proto_depIdxs,
EnumInfos: file_types_proto_enumTypes,
MessageInfos: file_types_proto_msgTypes,
}.Build()
File_types_proto = out.File
+57 -38
View File
@@ -5,15 +5,16 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"net"
"fmt"
"github.com/goccy/go-json"
"time"
"github.com/amir20/dozzle/internal/agent/pb"
"github.com/amir20/dozzle/internal/docker"
"github.com/rs/zerolog/log"
orderedmap "github.com/wk8/go-ordered-map/v2"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
@@ -24,16 +25,19 @@ import (
)
type server struct {
client docker.Client
store *docker.ContainerStore
client docker.Client
store *docker.ContainerStore
version string
pb.UnimplementedAgentServiceServer
}
func NewServer(client docker.Client) pb.AgentServiceServer {
func newServer(client docker.Client, dozzleVersion string) pb.AgentServiceServer {
return &server{
client: client,
store: docker.NewContainerStore(context.Background(), client),
client: client,
version: dozzleVersion,
store: docker.NewContainerStore(context.Background(), client),
}
}
@@ -178,17 +182,15 @@ func (s *server) FindContainer(ctx context.Context, in *pb.FindContainerRequest)
Id: container.ID,
Name: container.Name,
Image: container.Image,
ImageId: container.ImageID,
Command: container.Command,
Created: timestamppb.New(container.Created),
State: container.State,
Status: container.Status,
Health: container.Health,
Host: container.Host,
Tty: container.Tty,
Labels: container.Labels,
Group: container.Group,
Started: timestamppb.New(*container.StartedAt),
Started: timestamppb.New(container.StartedAt),
},
}, nil
}
@@ -212,25 +214,18 @@ func (s *server) ListContainers(ctx context.Context, in *pb.ListContainersReques
})
}
var startedAt *timestamppb.Timestamp
if container.StartedAt != nil {
startedAt = timestamppb.New(*container.StartedAt)
}
pbContainers = append(pbContainers, &pb.Container{
Id: container.ID,
Name: container.Name,
Image: container.Image,
ImageId: container.ImageID,
Created: timestamppb.New(container.Created),
State: container.State,
Status: container.Status,
Health: container.Health,
Host: container.Host,
Tty: container.Tty,
Labels: container.Labels,
Group: container.Group,
Started: startedAt,
Started: timestamppb.New(container.StartedAt),
Stats: pbStats,
Command: container.Command,
})
@@ -245,10 +240,12 @@ func (s *server) HostInfo(ctx context.Context, in *pb.HostInfoRequest) (*pb.Host
host := s.client.Host()
return &pb.HostInfoResponse{
Host: &pb.Host{
Id: host.ID,
Name: host.Name,
CpuCores: uint32(host.NCPU),
Memory: uint64(host.MemTotal),
Id: host.ID,
Name: host.Name,
CpuCores: uint32(host.NCPU),
Memory: uint64(host.MemTotal),
DockerVersion: host.DockerVersion,
AgentVersion: s.version,
},
}, nil
}
@@ -266,16 +263,14 @@ func (s *server) StreamContainerStarted(in *pb.StreamContainerStartedRequest, ou
Id: container.ID,
Name: container.Name,
Image: container.Image,
ImageId: container.ImageID,
Created: timestamppb.New(container.Created),
State: container.State,
Status: container.Status,
Health: container.Health,
Host: container.Host,
Tty: container.Tty,
Labels: container.Labels,
Group: container.Group,
Started: timestamppb.New(*container.StartedAt),
Started: timestamppb.New(container.StartedAt),
},
})
case <-out.Context().Done():
@@ -284,11 +279,36 @@ func (s *server) StreamContainerStarted(in *pb.StreamContainerStartedRequest, ou
}
}
func RunServer(client docker.Client, certificates tls.Certificate, listener net.Listener) {
func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequest) (*pb.ContainerActionResponse, error) {
var action docker.ContainerAction
switch in.Action {
case pb.ContainerAction_Start:
action = docker.Start
case pb.ContainerAction_Stop:
action = docker.Stop
case pb.ContainerAction_Restart:
action = docker.Restart
default:
return nil, status.Error(codes.InvalidArgument, "invalid action")
}
err := s.client.ContainerActions(action, in.ContainerId)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.ContainerActionResponse{}, nil
}
func NewServer(client docker.Client, certificates tls.Certificate, dozzleVersion string) (*grpc.Server, error) {
caCertPool := x509.NewCertPool()
c, err := x509.ParseCertificate(certificates.Certificate[0])
if err != nil {
log.Fatalf("failed to parse certificate: %v", err)
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
caCertPool.AddCert(c)
@@ -303,19 +323,18 @@ func RunServer(client docker.Client, certificates tls.Certificate, listener net.
creds := credentials.NewTLS(tlsConfig)
grpcServer := grpc.NewServer(grpc.Creds(creds))
pb.RegisterAgentServiceServer(grpcServer, NewServer(client))
pb.RegisterAgentServiceServer(grpcServer, newServer(client, dozzleVersion))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Infof("gRPC server listening on %s", listener.Addr().String())
if err := grpcServer.Serve(listener); err != nil {
log.Fatalf("failed to serve: %v", err)
}
return grpcServer, nil
}
func logEventToPb(event *docker.LogEvent) *pb.LogEvent {
var message *anypb.Any
if event.Message == nil {
log.Fatal().Interface("event", event).Msg("agent server: message is nil. This should not happen.")
}
switch data := event.Message.(type) {
case string:
message, _ = anypb.New(&pb.SimpleMessage{
@@ -332,7 +351,7 @@ func logEventToPb(event *docker.LogEvent) *pb.LogEvent {
})
default:
log.Fatalf("agent server: unknown type %T", event.Message)
log.Error().Type("message", event.Message).Msg("agent server: unknown message type")
}
return &pb.LogEvent{
+4 -4
View File
@@ -2,16 +2,16 @@ package analytics
import (
"bytes"
"encoding/json"
"fmt"
"github.com/goccy/go-json"
"net/http"
"net/http/httputil"
log "github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
func SendBeacon(e BeaconEvent) error {
log.Tracef("sending beacon: %+v", e)
log.Trace().Interface("event", e).Msg("sending beacon")
jsonValue, err := json.Marshal(e)
if err != nil {
return err
@@ -33,7 +33,7 @@ func SendBeacon(e BeaconEvent) error {
if err != nil {
return err
}
log.Debugf("%v", string(dump))
log.Debug().Str("response", string(dump)).Msg("google analytics returned non-2xx status code")
return fmt.Errorf("google analytics returned non-2xx status code: %v", response.Status)
}
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"net/http"
"strings"
log "github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
type contextKey string
@@ -49,6 +49,6 @@ func (p *proxyAuthContext) AuthMiddleware(next http.Handler) http.Handler {
}
func (p *proxyAuthContext) CreateToken(username, password string) (string, error) {
log.Fatalf("CreateToken not implemented for proxy auth")
log.Fatal().Msg("CreateToken not implemented in proxy auth")
return "", nil
}
+6 -5
View File
@@ -12,7 +12,7 @@ import (
"time"
"github.com/go-chi/jwtauth/v5"
log "github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)
@@ -90,11 +90,11 @@ func decodeUsersFromFile(path string) (UserDatabase, error) {
for username, user := range users.Users {
user.Username = username
if user.Password == "" {
log.Fatalf("User %s has no password", username)
log.Fatal().Msgf("User %s has an empty password", username)
}
if len(user.Password) != 64 {
log.Fatalf("User %s has an invalid password hash", username)
log.Fatal().Str("password", user.Password).Msgf("User %s has an invalid password hash", username)
}
if user.Name == "" {
@@ -115,7 +115,7 @@ func (u *UserDatabase) readFileIfChanged() error {
}
if info.ModTime().After(u.LastRead) {
log.Infof("Found changes to %s. Updating users...", u.Path)
log.Info().Msg("Reloading user database")
users, err := decodeUsersFromFile(u.Path)
if err != nil {
return err
@@ -129,7 +129,8 @@ func (u *UserDatabase) readFileIfChanged() error {
func (u *UserDatabase) Find(username string) *User {
if err := u.readFileIfChanged(); err != nil {
log.Errorf("Error reading users file: %s", err)
log.Error().Err(err).Msg("Failed to read user database")
return nil
}
user, ok := u.Users[username]
if !ok {
+2 -6
View File
@@ -4,7 +4,7 @@ import (
"sync"
"time"
log "github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
type Cache[T any] struct {
@@ -36,11 +36,7 @@ func (c *Cache[T]) GetWithHit() (T, error, bool) {
}
c.Timestamp = time.Now()
}
if hit {
log.Debugf("Cache hit for %T", c.Data)
} else {
log.Debugf("Cache miss for %T", c.Data)
}
log.Debug().Bool("hit", hit).Type("data", c.Data).Msg("Cache hit")
return c.Data, nil, hit
}
+86 -74
View File
@@ -4,8 +4,6 @@ import (
"context"
"fmt"
"io"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
@@ -22,7 +20,7 @@ import (
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
type StdType int
@@ -82,19 +80,20 @@ type httpClient struct {
}
func NewClient(cli DockerCLI, filters filters.Args, host Host) Client {
info, err := cli.Info(context.Background())
if err != nil {
log.Errorf("unable to get docker info: %v", err)
log.Error().Err(err).Msg("Failed to get docker info")
}
host.NCPU = info.NCPU
host.MemTotal = info.MemTotal
host.DockerVersion = info.ServerVersion
return &httpClient{
cli: cli,
filters: filters,
host: host,
info: info,
}
}
@@ -107,7 +106,7 @@ func NewLocalClient(f map[string][]string, hostname string) (Client, error) {
}
}
log.Debugf("filterArgs = %v", filterArgs)
log.Debug().Interface("filterArgs", filterArgs).Msg("Creating local client")
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
@@ -131,6 +130,7 @@ func NewLocalClient(f map[string][]string, hostname string) (Client, error) {
MemTotal: info.MemTotal,
NCPU: info.NCPU,
Endpoint: "local",
Type: "local",
}
if hostname != "" {
@@ -148,10 +148,10 @@ func NewRemoteClient(f map[string][]string, host Host) (Client, error) {
}
}
log.Debugf("filterArgs = %v", filterArgs)
log.Debug().Interface("filterArgs", filterArgs).Msg("Creating remote client")
if host.URL.Scheme != "tcp" {
log.Fatal("Only tcp scheme is supported")
return nil, fmt.Errorf("invalid scheme: %s", host.URL.Scheme)
}
opts := []client.Opt{
@@ -159,10 +159,10 @@ func NewRemoteClient(f map[string][]string, host Host) (Client, error) {
}
if host.ValidCerts {
log.Debugf("Using TLS client config with certs at: %s", filepath.Dir(host.CertPath))
log.Debug().Str("caCertPath", host.CACertPath).Str("certPath", host.CertPath).Str("keyPath", host.KeyPath).Msg("Using TLS for remote client")
opts = append(opts, client.WithTLSClientConfig(host.CACertPath, host.CertPath, host.KeyPath))
} else {
log.Debugf("No valid certs found, using plain TCP")
log.Debug().Msg("Not using TLS for remote client")
}
opts = append(opts, client.WithAPIVersionNegotiation())
@@ -173,40 +173,20 @@ func NewRemoteClient(f map[string][]string, host Host) (Client, error) {
return nil, err
}
host.Type = "remote"
return NewClient(cli, filterArgs, host), nil
}
// Finds a container by id, skipping the filters
func (d *httpClient) FindContainer(id string) (Container, error) {
log.Debugf("finding container with id: %s", id)
var container Container
containers, err := d.ListContainers()
if err != nil {
return container, err
}
found := false
for _, c := range containers {
if c.ID == id {
container = c
found = true
break
}
}
if !found {
return container, fmt.Errorf("unable to find container with id: %s", id)
}
if json, err := d.cli.ContainerInspect(context.Background(), container.ID); err == nil {
container.Tty = json.Config.Tty
if startedAt, err := time.Parse(time.RFC3339Nano, json.State.StartedAt); err == nil {
utc := startedAt.UTC()
container.StartedAt = &utc
}
log.Debug().Str("id", id).Msg("Finding container")
if json, err := d.cli.ContainerInspect(context.Background(), id); err == nil {
return newContainerFromJSON(json, d.host.ID), nil
} else {
return container, err
return Container{}, err
}
return container, nil
}
func (d *httpClient) ContainerActions(action ContainerAction, containerID string) error {
@@ -223,6 +203,7 @@ func (d *httpClient) ContainerActions(action ContainerAction, containerID string
}
func (d *httpClient) ListContainers() ([]Container, error) {
log.Debug().Interface("filter", d.filters).Str("host", d.host.Name).Msg("Listing containers")
containerListOptions := container.ListOptions{
Filters: d.filters,
All: true,
@@ -234,33 +215,7 @@ func (d *httpClient) ListContainers() ([]Container, error) {
var containers = make([]Container, 0, len(list))
for _, c := range list {
name := "no name"
if len(c.Names) > 0 {
name = strings.TrimPrefix(c.Names[0], "/")
}
group := ""
if c.Labels["dev.dozzle.group"] != "" {
group = c.Labels["dev.dozzle.group"]
}
container := Container{
ID: c.ID[:12],
Names: c.Names,
Name: name,
Image: c.Image,
ImageID: c.ImageID,
Command: c.Command,
Created: time.Unix(c.Created, 0),
State: c.State,
Status: c.Status,
Host: d.host.ID,
Health: findBetweenParentheses(c.Status),
Labels: c.Labels,
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
Group: group,
}
containers = append(containers, container)
containers = append(containers, newContainer(c, d.host.ID))
}
sort.Slice(containers, func(i, j int) bool {
@@ -321,9 +276,9 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
}
func (d *httpClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType StdType) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container")
log.Debug().Str("id", id).Time("since", since).Stringer("stdType", stdType).Str("host", d.host.Name).Msg("Streaming logs for container")
sinceQuery := since.Add(time.Millisecond).Format(time.RFC3339Nano)
sinceQuery := since.Add(-50 * time.Millisecond).Format(time.RFC3339Nano)
options := container.LogsOptions{
ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0,
@@ -364,6 +319,7 @@ func (d *httpClient) ContainerEvents(ctx context.Context, messages chan<- Contai
}
func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
log.Debug().Str("id", id).Time("from", from).Time("to", to).Stringer("stdType", stdType).Str("host", d.host.Name).Msg("Fetching logs between dates for container")
options := container.LogsOptions{
ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0,
@@ -372,8 +328,6 @@ func (d *httpClient) ContainerLogsBetweenDates(ctx context.Context, id string, f
Until: to.Format(time.RFC3339Nano),
}
log.Debugf("fetching logs from Docker with option: %+v", options)
reader, err := d.cli.ContainerLogs(ctx, id, options)
if err != nil {
return nil, err
@@ -398,11 +352,69 @@ func (d *httpClient) SystemInfo() system.Info {
return d.info
}
var PARENTHESIS_RE = regexp.MustCompile(`\(([a-zA-Z]+)\)`)
func findBetweenParentheses(s string) string {
if results := PARENTHESIS_RE.FindStringSubmatch(s); results != nil {
return results[1]
func newContainer(c types.Container, host string) Container {
name := "no name"
if c.Labels["dev.dozzle.name"] != "" {
name = c.Labels["dev.dozzle.name"]
} else if len(c.Names) > 0 {
name = strings.TrimPrefix(c.Names[0], "/")
}
group := ""
if c.Labels["dev.dozzle.group"] != "" {
group = c.Labels["dev.dozzle.group"]
}
return Container{
ID: c.ID[:12],
Name: name,
Image: c.Image,
Command: c.Command,
Created: time.Unix(c.Created, 0),
State: c.State,
Host: host,
Labels: c.Labels,
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
Group: group,
}
return ""
}
func newContainerFromJSON(c types.ContainerJSON, host string) Container {
name := "no name"
if c.Config.Labels["dev.dozzle.name"] != "" {
name = c.Config.Labels["dev.dozzle.name"]
} else if len(c.Name) > 0 {
name = strings.TrimPrefix(c.Name, "/")
}
group := ""
if c.Config.Labels["dev.dozzle.group"] != "" {
group = c.Config.Labels["dev.dozzle.group"]
}
container := Container{
ID: c.ID[:12],
Name: name,
Image: c.Config.Image,
Command: strings.Join(c.Config.Entrypoint, " ") + " " + strings.Join(c.Config.Cmd, " "),
State: c.State.Status,
Host: host,
Labels: c.Config.Labels,
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
Group: group,
Tty: c.Config.Tty,
}
if startedAt, err := time.Parse(time.RFC3339Nano, c.State.StartedAt); err == nil {
container.StartedAt = startedAt.UTC()
}
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
container.Created = createdAt.UTC()
}
if c.State.Health != nil {
container.Health = strings.ToLower(c.State.Health.Status)
}
return container
}
+6 -51
View File
@@ -156,7 +156,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
Follow: true,
Tail: "100",
Timestamps: true,
Since: "2021-01-01T00:00:00.001Z"}
Since: "2020-12-31T23:59:59.95Z"}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
@@ -183,22 +183,10 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
}
func Test_dockerClient_FindContainer_happy(t *testing.T) {
containers := []types.Container{
{
ID: "abcdefghijklmnopqrst",
Names: []string{"/z_test_container"},
},
{
ID: "1234567890_abcxyzdef",
Names: []string{"/a_test_container"},
},
}
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{State: state}, Config: &container.Config{Tty: false}}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijklmnopqrst", State: state}, Config: &container.Config{Tty: false}}
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
@@ -210,20 +198,10 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
proxy.AssertExpectations(t)
}
func Test_dockerClient_FindContainer_error(t *testing.T) {
containers := []types.Container{
{
ID: "abcdefghijklmnopqrst",
Names: []string{"/z_test_container"},
},
{
ID: "1234567890_abcxyzdef",
Names: []string{"/a_test_container"},
},
}
func Test_dockerClient_FindContainer_error(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
proxy.On("ContainerInspect", mock.Anything, "not_valid").Return(types.ContainerJSON{}, errors.New("not found"))
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
_, err := client.FindContainer("not_valid")
@@ -233,24 +211,12 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
}
func Test_dockerClient_ContainerActions_happy(t *testing.T) {
containers := []types.Container{
{
ID: "abcdefghijklmnopqrst",
Names: []string{"/z_test_container"},
},
{
ID: "1234567890_abcxyzdef",
Names: []string{"/a_test_container"},
},
}
proxy := new(mockedProxy)
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{State: state}, Config: &container.Config{Tty: false}}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijkl", State: state}, Config: &container.Config{Tty: false}}
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
proxy.On("ContainerStart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
proxy.On("ContainerStop", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
@@ -272,21 +238,10 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
}
func Test_dockerClient_ContainerActions_error(t *testing.T) {
containers := []types.Container{
{
ID: "abcdefghijklmnopqrst",
Names: []string{"/z_test_container"},
},
{
ID: "1234567890_abcxyzdef",
Names: []string{"/a_test_container"},
},
}
proxy := new(mockedProxy)
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
proxy.On("ContainerInspect", mock.Anything, "random-id").Return(types.ContainerJSON{}, errors.New("not found"))
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
proxy.On("ContainerStop", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
proxy.On("ContainerRestart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
+65 -32
View File
@@ -7,8 +7,9 @@ import (
"sync/atomic"
"github.com/puzpuzpuz/xsync/v3"
lop "github.com/samber/lo/parallel"
log "github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"golang.org/x/sync/semaphore"
)
type ContainerStore struct {
@@ -42,15 +43,18 @@ func NewContainerStore(ctx context.Context, client Client) *ContainerStore {
return s
}
var ErrContainerNotFound = errors.New("container not found")
var (
ErrContainerNotFound = errors.New("container not found")
maxFetchParallelism = int64(30)
)
func (s *ContainerStore) checkConnectivity() error {
if s.connected.CompareAndSwap(false, true) {
go func() {
log.Debugf("subscribing to docker events from container store %s", s.client.Host())
log.Debug().Str("host", s.client.Host().Name).Msg("docker store subscribing docker events")
err := s.client.ContainerEvents(s.ctx, s.events)
if !errors.Is(err, context.Canceled) {
log.Errorf("docker store unexpectedly disconnected from docker events from %s with %v", s.client.Host(), err)
log.Error().Err(err).Str("host", s.client.Host().Name).Msg("docker store unexpectedly disconnected from docker events")
}
s.connected.Store(false)
}()
@@ -59,10 +63,35 @@ func (s *ContainerStore) checkConnectivity() error {
return err
} else {
s.containers.Clear()
lop.ForEach(containers, func(c Container, _ int) {
container, _ := s.client.FindContainer(c.ID)
s.containers.Store(c.ID, &container)
for _, c := range containers {
s.containers.Store(c.ID, &c)
}
running := lo.Filter(containers, func(item Container, index int) bool {
return item.State == "running"
})
sem := semaphore.NewWeighted(maxFetchParallelism)
for i, c := range running {
if err := sem.Acquire(s.ctx, 1); err != nil {
log.Error().Err(err).Msg("failed to acquire semaphore")
break
}
go func(c Container, i int) {
defer sem.Release(1)
if container, err := s.client.FindContainer(c.ID); err == nil {
s.containers.Store(c.ID, &container)
}
}(c, i)
}
if err := sem.Acquire(s.ctx, maxFetchParallelism); err != nil {
log.Error().Err(err).Msg("failed to acquire semaphore")
}
log.Debug().Int("containers", len(containers)).Msg("finished initializing container store")
}
}
@@ -85,19 +114,15 @@ func (s *ContainerStore) ListContainers() ([]Container, error) {
}
func (s *ContainerStore) FindContainer(id string) (Container, error) {
list, err := s.ListContainers()
if err != nil {
return Container{}, err
}
s.wg.Wait()
container, ok := s.containers.Load(id)
for _, c := range list {
if c.ID == id {
return c, nil
}
if ok {
return *container, nil
} else {
log.Warn().Str("id", id).Msg("container not found")
return Container{}, ErrContainerNotFound
}
log.Warnf("container %s not found in store", id)
return Container{}, ErrContainerNotFound
}
func (s *ContainerStore) Client() Client {
@@ -107,7 +132,6 @@ func (s *ContainerStore) Client() Client {
func (s *ContainerStore) SubscribeEvents(ctx context.Context, events chan<- ContainerEvent) {
go func() {
if s.statsCollector.Start(s.ctx) {
log.Debug("clearing container stats as stats collector has been stopped")
s.containers.Range(func(_ string, c *Container) bool {
c.Stats.Clear()
return true
@@ -146,28 +170,37 @@ func (s *ContainerStore) init() {
for {
select {
case event := <-s.events:
log.Tracef("received event: %+v", event)
log.Trace().Str("event", event.Name).Str("id", event.ActorID).Msg("received container event")
switch event.Name {
case "start":
if container, err := s.client.FindContainer(event.ActorID); err == nil {
log.Debugf("container %s started", container.ID)
s.containers.Store(container.ID, &container)
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
select {
case containers <- container:
case <-c.Done():
}
return true
list, _ := s.client.ListContainers()
// make sure the container is in the list of containers when using filter
valid := lo.ContainsBy(list, func(item Container) bool {
return item.ID == container.ID
})
if valid {
log.Debug().Str("id", container.ID).Msg("container started")
s.containers.Store(container.ID, &container)
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
select {
case containers <- container:
case <-c.Done():
}
return true
})
}
}
case "destroy":
log.Debugf("container %s destroyed", event.ActorID)
log.Debug().Str("id", event.ActorID).Msg("container destroyed")
s.containers.Delete(event.ActorID)
case "die":
s.containers.Compute(event.ActorID, func(c *Container, loaded bool) (*Container, bool) {
if loaded {
log.Debugf("container %s died", c.ID)
log.Debug().Str("id", c.ID).Msg("container died")
c.State = "exited"
return c, false
} else {
@@ -182,7 +215,7 @@ func (s *ContainerStore) init() {
s.containers.Compute(event.ActorID, func(c *Container, loaded bool) (*Container, bool) {
if loaded {
log.Debugf("health status for container %s is %s", c.ID, healthy)
log.Debug().Str("id", c.ID).Str("health", healthy).Msg("container health status changed")
c.Health = healthy
return c, false
} else {

Some files were not shown because too many files have changed in this diff Show More