mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 12:14:44 +00:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 293bf9e021 | |||
| fa87939676 | |||
| d482dcd667 | |||
| 65cdb306f8 | |||
| 7179b630a5 | |||
| 8ee18ce9f4 | |||
| a5ffd88884 | |||
| 328a277cff | |||
| 0d58f2bfca | |||
| 73414c6a8d | |||
| 95fafd12c5 | |||
| cdf6334cef | |||
| 6791c53d43 | |||
| 0cbc08fa1a | |||
| 45c7224d13 | |||
| 51a3f1f03b | |||
| 9ddbe6fbb0 | |||
| a62331a488 | |||
| 2a3af94467 | |||
| 42e423fd2e | |||
| 9a146ae62a | |||
| 1518d80aad | |||
| a1efb8e340 | |||
| 7d42bd2b02 | |||
| 9f2f8b8245 | |||
| 057d7ad712 | |||
| 712e1d389a | |||
| c1fcafba43 | |||
| 74063a0c72 | |||
| 12e7ee33b7 | |||
| db33e28117 | |||
| df1a650980 | |||
| a937615747 | |||
| d13163b0d9 | |||
| 9ea7024c2a | |||
| 85271a595a | |||
| 62a2ef9eb4 | |||
| 64790daea7 | |||
| 0798de3b3a | |||
| 0a03923587 | |||
| 3a0eaf1609 | |||
| a6620ff315 | |||
| aec8139a19 | |||
| afcb80938e | |||
| 36f3308e1d | |||
| b6091ff944 | |||
| 4bb007fdfd | |||
| a56fea1728 | |||
| 6a1a412f47 | |||
| eaf20059ba | |||
| a0db92401f | |||
| 3f8728c821 | |||
| 4319415e1d | |||
| b54611fb65 | |||
| b0012633aa | |||
| 8cbd04812a | |||
| 1dea4f1059 | |||
| 3d9e025b39 | |||
| acd9946bc8 | |||
| 9f743132dd | |||
| 621a3d98bd | |||
| bf574c6b83 | |||
| 9ace6eccdf | |||
| 3d152b13dc | |||
| 10f868d2c4 | |||
| 077eeb7917 | |||
| 37f0249065 | |||
| aacf0474e4 | |||
| c1bf6b15c9 | |||
| fa7c7ca62d | |||
| 0d3ed03e04 | |||
| 7f735d26db | |||
| 9467e3d7d5 | |||
| 3072709f7e | |||
| d1a308b3b6 | |||
| 9dfdfbf8b4 | |||
| acbb2a8d24 | |||
| 6c5dcb7a62 | |||
| aa824453a5 | |||
| bdf19587f2 | |||
| 1637b37cc1 | |||
| 4eb73e96c5 | |||
| 69fb227519 | |||
| ffc4559047 | |||
| 05fc3f04ec | |||
| 84be3b1648 | |||
| cabe2c803a | |||
| f5a9669249 | |||
| c382b25b97 | |||
| 9db280e7b4 | |||
| 22c1473c4b | |||
| f53c4d7d3d | |||
| 2514c666c4 | |||
| b38a02f5ba | |||
| db1cccc61f | |||
| db75e0f3e9 | |||
| fe149dc8b2 | |||
| 3a77e2b187 | |||
| 6d4be52006 | |||
| ddcd625a70 | |||
| 1b9f25e64f | |||
| 73fa622721 | |||
| 0e652cb936 | |||
| 657643e6ce | |||
| b4277603cf | |||
| d72edefac2 | |||
| df4b449bc4 | |||
| 74fda04ebb | |||
| 638889ed58 | |||
| f09e294f38 | |||
| de2e757665 | |||
| ed84b9a181 | |||
| c906b80f52 | |||
| e2a951ca3d | |||
| c91adb221a | |||
| 989f0f03ee | |||
| ba10c03a90 | |||
| 41859e64a5 | |||
| 5ef18bf508 | |||
| 19d0355d7c | |||
| cbed761a18 | |||
| abd9cebdbe | |||
| 3a988b8fdc | |||
| c87a7b1272 | |||
| ed351f9d07 | |||
| c4ff571eb5 | |||
| 2bd0031feb | |||
| 082bed2382 | |||
| 4aa315a471 | |||
| 79118368b1 | |||
| daf8cf815b | |||
| d2015fdebd | |||
| 989a650f12 |
@@ -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: .
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
shared-workspace-lockfile=false
|
||||
include-workspace-root=true
|
||||
ignore-workspace-root-check=true
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -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
@@ -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\
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Dozzle is a small lightweight application with a web based interface to monitor Docker logs. It doesn’t 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
|
||||
|
||||
[](https://goreportcard.com/report/github.com/amir20/dozzle)
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||
[](https://hub.docker.com/r/amir20/dozzle/)
|
||||

|
||||
@@ -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.
|
||||
|
||||
Vendored
+23
-2
@@ -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']>
|
||||
|
||||
Vendored
+9
-4
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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", []),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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,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" />
|
||||
|
||||
@@ -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"><null></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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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"><test>foo bar</test></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"><test>foo bar</test></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"><test>foo bar</test></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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">∞</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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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("", []));
|
||||
|
||||
@@ -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("", [], []));
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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}`;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,7 +23,6 @@ docker run --label dev.dozzle.group=myapp hello-world
|
||||
```
|
||||
|
||||
```yaml [docker-compose.yml]
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
image: hello-world
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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,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
@@ -6,6 +6,6 @@
|
||||
"dozzle": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/playfair-display": "^5.0.25"
|
||||
"@fontsource-variable/playfair-display": "^5.0.27"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+5
-5
@@ -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 |
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
dozzle:
|
||||
image: amir20/dozzle:pr-3097
|
||||
dozzle-service:
|
||||
image: amir20/dozzle:latest
|
||||
environment:
|
||||
- DOZZLE_LEVEL=debug
|
||||
- DOZZLE_MODE=swarm
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Vendored
+2
-6
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user