dStats 0.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- dStats/__init__.py +0 -0
- dStats/asgi.py +16 -0
- dStats/settings.py +128 -0
- dStats/urls.py +24 -0
- dStats/views.py +308 -0
- dStats/wsgi.py +16 -0
- dStats-0.1.0.dist-info/LICENSE +13 -0
- dStats-0.1.0.dist-info/METADATA +158 -0
- dStats-0.1.0.dist-info/RECORD +11 -0
- dStats-0.1.0.dist-info/WHEEL +5 -0
- dStats-0.1.0.dist-info/top_level.txt +1 -0
dStats/__init__.py
ADDED
File without changes
|
dStats/asgi.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
"""
|
2
|
+
ASGI config for dStats project.
|
3
|
+
|
4
|
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
5
|
+
|
6
|
+
For more information on this file, see
|
7
|
+
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
8
|
+
"""
|
9
|
+
|
10
|
+
import os
|
11
|
+
|
12
|
+
from django.core.asgi import get_asgi_application
|
13
|
+
|
14
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dStats.settings")
|
15
|
+
|
16
|
+
application = get_asgi_application()
|
dStats/settings.py
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
"""
|
2
|
+
Django settings for dStats project.
|
3
|
+
|
4
|
+
Generated by 'django-admin startproject' using Django 5.1.4.
|
5
|
+
|
6
|
+
For more information on this file, see
|
7
|
+
https://docs.djangoproject.com/en/5.1/topics/settings/
|
8
|
+
|
9
|
+
For the full list of settings and their values, see
|
10
|
+
https://docs.djangoproject.com/en/5.1/ref/settings/
|
11
|
+
"""
|
12
|
+
|
13
|
+
from pathlib import Path
|
14
|
+
|
15
|
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
16
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
17
|
+
|
18
|
+
|
19
|
+
# Quick-start development settings - unsuitable for production
|
20
|
+
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
21
|
+
|
22
|
+
# SECURITY WARNING: keep the secret key used in production secret!
|
23
|
+
SECRET_KEY = "django-insecure-z)f1y6v3$3fa1=pbz2_mv&uhx=#cne(@=vy*)$1j-h(fyit+ri"
|
24
|
+
|
25
|
+
# SECURITY WARNING: don't run with debug turned on in production!
|
26
|
+
DEBUG = True
|
27
|
+
|
28
|
+
ALLOWED_HOSTS = ["*"]
|
29
|
+
|
30
|
+
|
31
|
+
# Application definition
|
32
|
+
|
33
|
+
INSTALLED_APPS = [
|
34
|
+
"daphne",
|
35
|
+
"django.contrib.admin",
|
36
|
+
"django.contrib.auth",
|
37
|
+
"django.contrib.contenttypes",
|
38
|
+
"django.contrib.sessions",
|
39
|
+
"django.contrib.messages",
|
40
|
+
"django.contrib.staticfiles",
|
41
|
+
"dStats",
|
42
|
+
]
|
43
|
+
|
44
|
+
MIDDLEWARE = [
|
45
|
+
"django.middleware.security.SecurityMiddleware",
|
46
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
47
|
+
"django.middleware.common.CommonMiddleware",
|
48
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
49
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
50
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
51
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
52
|
+
]
|
53
|
+
|
54
|
+
ROOT_URLCONF = "dStats.urls"
|
55
|
+
|
56
|
+
TEMPLATES = [
|
57
|
+
{
|
58
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
59
|
+
"DIRS": [],
|
60
|
+
"APP_DIRS": True,
|
61
|
+
"OPTIONS": {
|
62
|
+
"context_processors": [
|
63
|
+
"django.template.context_processors.debug",
|
64
|
+
"django.template.context_processors.request",
|
65
|
+
"django.contrib.auth.context_processors.auth",
|
66
|
+
"django.contrib.messages.context_processors.messages",
|
67
|
+
],
|
68
|
+
},
|
69
|
+
},
|
70
|
+
]
|
71
|
+
|
72
|
+
# WSGI_APPLICATION = "dStats.wsgi.application"
|
73
|
+
ASGI_APPLICATION = "dStats.asgi.application"
|
74
|
+
|
75
|
+
|
76
|
+
# Database
|
77
|
+
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
78
|
+
|
79
|
+
DATABASES = {
|
80
|
+
"default": {
|
81
|
+
"ENGINE": "django.db.backends.sqlite3",
|
82
|
+
"NAME": BASE_DIR / "db.sqlite3",
|
83
|
+
}
|
84
|
+
}
|
85
|
+
MIGRATION_MODULES = {
|
86
|
+
"dStats": None, # Disable migrations for dStats app
|
87
|
+
}
|
88
|
+
|
89
|
+
# Password validation
|
90
|
+
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
91
|
+
|
92
|
+
AUTH_PASSWORD_VALIDATORS = [
|
93
|
+
{
|
94
|
+
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
95
|
+
},
|
96
|
+
{
|
97
|
+
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
98
|
+
},
|
99
|
+
{
|
100
|
+
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
101
|
+
},
|
102
|
+
{
|
103
|
+
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
104
|
+
},
|
105
|
+
]
|
106
|
+
|
107
|
+
|
108
|
+
# Internationalization
|
109
|
+
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
110
|
+
|
111
|
+
LANGUAGE_CODE = "en-us"
|
112
|
+
|
113
|
+
TIME_ZONE = "UTC"
|
114
|
+
|
115
|
+
USE_I18N = True
|
116
|
+
|
117
|
+
USE_TZ = True
|
118
|
+
|
119
|
+
|
120
|
+
# Static files (CSS, JavaScript, Images)
|
121
|
+
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
122
|
+
|
123
|
+
STATIC_URL = "static/"
|
124
|
+
|
125
|
+
# Default primary key field type
|
126
|
+
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
127
|
+
|
128
|
+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
dStats/urls.py
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
"""
|
2
|
+
URL configuration for dStats project.
|
3
|
+
|
4
|
+
The `urlpatterns` list routes URLs to views. For more information please see:
|
5
|
+
https://docs.djangoproject.com/en/5.1/topics/http/urls/
|
6
|
+
Examples:
|
7
|
+
Function views
|
8
|
+
1. Add an import: from my_app import views
|
9
|
+
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
10
|
+
Class-based views
|
11
|
+
1. Add an import: from other_app.views import Home
|
12
|
+
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
13
|
+
Including another URLconf
|
14
|
+
1. Import the include() function: from django.urls import include, path
|
15
|
+
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
16
|
+
"""
|
17
|
+
|
18
|
+
from django.contrib import admin
|
19
|
+
from django.urls import path
|
20
|
+
from .views import DockerStatsView
|
21
|
+
|
22
|
+
urlpatterns = [
|
23
|
+
path("", DockerStatsView.as_view(), name="stats"),
|
24
|
+
]
|
dStats/views.py
ADDED
@@ -0,0 +1,308 @@
|
|
1
|
+
# views.py
|
2
|
+
import docker
|
3
|
+
import json
|
4
|
+
from dataclasses import asdict, dataclass
|
5
|
+
from typing import List, Dict
|
6
|
+
import random
|
7
|
+
from graphviz import Graph
|
8
|
+
import base64
|
9
|
+
from io import BytesIO
|
10
|
+
from django.views.generic import TemplateView
|
11
|
+
from django.http import JsonResponse
|
12
|
+
from django.utils.decorators import method_decorator
|
13
|
+
from django.views.decorators.csrf import csrf_exempt
|
14
|
+
|
15
|
+
|
16
|
+
# Keep the existing dataclass definitions
|
17
|
+
@dataclass
|
18
|
+
class Network:
|
19
|
+
name: str
|
20
|
+
gateway: str
|
21
|
+
internal: bool
|
22
|
+
isolated: bool
|
23
|
+
color: str
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass
|
27
|
+
class Interface:
|
28
|
+
endpoint_id: str
|
29
|
+
address: str
|
30
|
+
aliases: List[str]
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class PortMapping:
|
35
|
+
internal: int
|
36
|
+
external: int
|
37
|
+
protocol: str
|
38
|
+
|
39
|
+
|
40
|
+
@dataclass
|
41
|
+
class Container:
|
42
|
+
container_id: str
|
43
|
+
name: str
|
44
|
+
interfaces: List[Interface]
|
45
|
+
ports: List[PortMapping]
|
46
|
+
|
47
|
+
|
48
|
+
@dataclass
|
49
|
+
class Link:
|
50
|
+
container_id: str
|
51
|
+
endpoint_id: str
|
52
|
+
network_name: str
|
53
|
+
|
54
|
+
|
55
|
+
# Keep the existing color definitions
|
56
|
+
COLORS = [
|
57
|
+
"#1f78b4",
|
58
|
+
"#33a02c",
|
59
|
+
"#e31a1c",
|
60
|
+
"#ff7f00",
|
61
|
+
"#6a3d9a",
|
62
|
+
"#b15928",
|
63
|
+
"#a6cee3",
|
64
|
+
"#b2df8a",
|
65
|
+
"#fdbf6f",
|
66
|
+
"#cab2d6",
|
67
|
+
"#ffff99",
|
68
|
+
]
|
69
|
+
color_index = 0
|
70
|
+
|
71
|
+
|
72
|
+
def get_unique_color() -> str:
|
73
|
+
global color_index
|
74
|
+
if color_index < len(COLORS):
|
75
|
+
c = COLORS[color_index]
|
76
|
+
color_index += 1
|
77
|
+
else:
|
78
|
+
c = "#%06x" % random.randint(0, 0xFFFFFF)
|
79
|
+
return c
|
80
|
+
|
81
|
+
|
82
|
+
class DockerStatsView(TemplateView):
|
83
|
+
template_name = "dStats/index.html"
|
84
|
+
|
85
|
+
def get_context_data(self, **kwargs):
|
86
|
+
context = super().get_context_data(**kwargs)
|
87
|
+
return context
|
88
|
+
|
89
|
+
def generate_network_graph(self):
|
90
|
+
client = docker.from_env()
|
91
|
+
networks = self.get_networks(client)
|
92
|
+
containers, links = self.get_containers(client)
|
93
|
+
|
94
|
+
g = Graph(
|
95
|
+
comment="Docker Network Graph",
|
96
|
+
engine="sfdp",
|
97
|
+
format="png",
|
98
|
+
graph_attr={
|
99
|
+
"splines": "true",
|
100
|
+
"overlap": "false",
|
101
|
+
"nodesep": "2.0",
|
102
|
+
"ranksep": "2.0",
|
103
|
+
},
|
104
|
+
)
|
105
|
+
|
106
|
+
# Draw networks and containers
|
107
|
+
for network in networks.values():
|
108
|
+
self.draw_network(g, network)
|
109
|
+
|
110
|
+
for container in containers:
|
111
|
+
self.draw_container(g, container)
|
112
|
+
|
113
|
+
for link in links:
|
114
|
+
if link.network_name != "none":
|
115
|
+
self.draw_link(g, networks, link)
|
116
|
+
|
117
|
+
# Convert graph to base64 image
|
118
|
+
img_data = g.pipe()
|
119
|
+
encoded_img = base64.b64encode(img_data).decode("utf-8")
|
120
|
+
return encoded_img
|
121
|
+
|
122
|
+
def get_networks(self, client):
|
123
|
+
networks = {}
|
124
|
+
for net in sorted(client.networks.list(), key=lambda k: k.name):
|
125
|
+
try:
|
126
|
+
config = net.attrs["IPAM"]["Config"]
|
127
|
+
gateway = config[0]["Subnet"] if config else "0.0.0.0"
|
128
|
+
except (KeyError, IndexError):
|
129
|
+
continue
|
130
|
+
|
131
|
+
internal = net.attrs.get("Internal", False)
|
132
|
+
isolated = (
|
133
|
+
net.attrs.get("Options", {}).get(
|
134
|
+
"com.docker.network.bridge.enable_icc", "true"
|
135
|
+
)
|
136
|
+
== "false"
|
137
|
+
)
|
138
|
+
|
139
|
+
color = get_unique_color()
|
140
|
+
networks[net.name] = Network(net.name, gateway, internal, isolated, color)
|
141
|
+
|
142
|
+
networks["host"] = Network("host", "0.0.0.0", False, False, "#808080")
|
143
|
+
return networks
|
144
|
+
|
145
|
+
def get_containers(self, client):
|
146
|
+
containers = []
|
147
|
+
links = []
|
148
|
+
|
149
|
+
for container in client.containers.list():
|
150
|
+
interfaces = []
|
151
|
+
ports = []
|
152
|
+
|
153
|
+
for net_name, net_info in container.attrs["NetworkSettings"][
|
154
|
+
"Networks"
|
155
|
+
].items():
|
156
|
+
endpoint_id = net_info["EndpointID"]
|
157
|
+
aliases = net_info.get("Aliases", [])
|
158
|
+
interfaces.append(
|
159
|
+
Interface(endpoint_id, net_info["IPAddress"], aliases)
|
160
|
+
)
|
161
|
+
links.append(Link(container.id, endpoint_id, net_name))
|
162
|
+
|
163
|
+
port_mappings = container.attrs["NetworkSettings"]["Ports"]
|
164
|
+
if port_mappings:
|
165
|
+
for container_port, host_ports in port_mappings.items():
|
166
|
+
if host_ports:
|
167
|
+
for host_port in host_ports:
|
168
|
+
internal_port, protocol = container_port.split("/")
|
169
|
+
ports.append(
|
170
|
+
PortMapping(
|
171
|
+
int(internal_port),
|
172
|
+
int(host_port["HostPort"]),
|
173
|
+
protocol,
|
174
|
+
)
|
175
|
+
)
|
176
|
+
|
177
|
+
containers.append(
|
178
|
+
Container(container.id, container.name, interfaces, ports)
|
179
|
+
)
|
180
|
+
|
181
|
+
return containers, links
|
182
|
+
|
183
|
+
def draw_network(self, g: Graph, net: Network):
|
184
|
+
label = f"{{<gw_iface> {net.gateway} | {net.name}"
|
185
|
+
if net.internal:
|
186
|
+
label += " | Internal"
|
187
|
+
if net.isolated:
|
188
|
+
label += " | Containers isolated"
|
189
|
+
label += "}"
|
190
|
+
|
191
|
+
g.node(
|
192
|
+
f"network_{net.name}",
|
193
|
+
shape="record",
|
194
|
+
label=label,
|
195
|
+
fillcolor=net.color,
|
196
|
+
style="filled",
|
197
|
+
)
|
198
|
+
|
199
|
+
def draw_container(self, g: Graph, c: Container):
|
200
|
+
iface_labels = []
|
201
|
+
for iface in c.interfaces:
|
202
|
+
if iface.aliases:
|
203
|
+
iface_labels.append(f"{iface.address} ({', '.join(iface.aliases)})")
|
204
|
+
else:
|
205
|
+
iface_labels.append(iface.address)
|
206
|
+
|
207
|
+
port_labels = []
|
208
|
+
for port in c.ports:
|
209
|
+
port_labels.append(f"{port.internal}->{port.external}/{port.protocol}")
|
210
|
+
|
211
|
+
label = f"{c.name}\\n"
|
212
|
+
label += "Interfaces:\\n" + "\\n".join(iface_labels)
|
213
|
+
if port_labels:
|
214
|
+
label += "\\nPorts:\\n" + "\\n".join(port_labels)
|
215
|
+
|
216
|
+
g.node(
|
217
|
+
f"container_{c.container_id}",
|
218
|
+
shape="box",
|
219
|
+
label=label,
|
220
|
+
fillcolor="#ff9999",
|
221
|
+
style="filled",
|
222
|
+
)
|
223
|
+
|
224
|
+
def draw_link(self, g: Graph, networks: Dict[str, Network], link: Link):
|
225
|
+
g.edge(
|
226
|
+
f"container_{link.container_id}",
|
227
|
+
f"network_{link.network_name}",
|
228
|
+
color=networks[link.network_name].color,
|
229
|
+
)
|
230
|
+
|
231
|
+
@method_decorator(csrf_exempt)
|
232
|
+
def dispatch(self, *args, **kwargs):
|
233
|
+
return super().dispatch(*args, **kwargs)
|
234
|
+
|
235
|
+
def get_container_stats(self):
|
236
|
+
client = docker.from_env()
|
237
|
+
stats = []
|
238
|
+
|
239
|
+
for container in client.containers.list():
|
240
|
+
try:
|
241
|
+
container_stats = container.stats(stream=False)
|
242
|
+
|
243
|
+
# Calculate CPU percentage
|
244
|
+
cpu_total = float(
|
245
|
+
container_stats["cpu_stats"]["cpu_usage"]["total_usage"]
|
246
|
+
)
|
247
|
+
cpu_delta = cpu_total - float(
|
248
|
+
container_stats["precpu_stats"]["cpu_usage"]["total_usage"]
|
249
|
+
)
|
250
|
+
system_delta = float(
|
251
|
+
container_stats["cpu_stats"]["system_cpu_usage"]
|
252
|
+
) - float(container_stats["precpu_stats"]["system_cpu_usage"])
|
253
|
+
online_cpus = container_stats["cpu_stats"].get(
|
254
|
+
"online_cpus",
|
255
|
+
len(
|
256
|
+
container_stats["cpu_stats"]["cpu_usage"].get(
|
257
|
+
"percpu_usage", [1]
|
258
|
+
)
|
259
|
+
),
|
260
|
+
)
|
261
|
+
|
262
|
+
cpu_percent = 0.0
|
263
|
+
if system_delta > 0.0:
|
264
|
+
cpu_percent = (cpu_delta / system_delta) * 100.0 * online_cpus
|
265
|
+
|
266
|
+
# Memory usage
|
267
|
+
mem_usage = container_stats["memory_stats"].get("usage", 0)
|
268
|
+
mem_limit = container_stats["memory_stats"].get("limit", 1)
|
269
|
+
mem_percent = (mem_usage / mem_limit) * 100.0
|
270
|
+
mem_mb = mem_usage / (1024 * 1024)
|
271
|
+
|
272
|
+
# Network usage
|
273
|
+
net_usage = container_stats.get("networks", {})
|
274
|
+
network_in = sum([net.get("rx_bytes", 0) for net in net_usage.values()])
|
275
|
+
network_out = sum(
|
276
|
+
[net.get("tx_bytes", 0) for net in net_usage.values()]
|
277
|
+
)
|
278
|
+
|
279
|
+
stats.append(
|
280
|
+
{
|
281
|
+
"name": container.name,
|
282
|
+
"cpu_percent": f"{cpu_percent:.2f}%",
|
283
|
+
"memory_usage": f"{mem_mb:.2f} MB ({mem_percent:.2f}%)",
|
284
|
+
"network_io": f"IN: {network_in/1024:.2f} KB / OUT: {network_out/1024:.2f} KB",
|
285
|
+
}
|
286
|
+
)
|
287
|
+
|
288
|
+
except Exception as e:
|
289
|
+
stats.append(
|
290
|
+
{
|
291
|
+
"name": container.name,
|
292
|
+
"cpu_percent": "N/A",
|
293
|
+
"memory_usage": "N/A",
|
294
|
+
"network_io": "N/A",
|
295
|
+
}
|
296
|
+
)
|
297
|
+
|
298
|
+
return stats
|
299
|
+
|
300
|
+
def get(self, request, *args, **kwargs):
|
301
|
+
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
302
|
+
if request.GET.get("type") == "stats":
|
303
|
+
return JsonResponse({"stats": self.get_container_stats()})
|
304
|
+
elif request.GET.get("type") == "graph":
|
305
|
+
return JsonResponse({"graph": self.generate_network_graph()})
|
306
|
+
|
307
|
+
context = self.get_context_data(**kwargs)
|
308
|
+
return self.render_to_response(context)
|
dStats/wsgi.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
"""
|
2
|
+
WSGI config for dStats project.
|
3
|
+
|
4
|
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
5
|
+
|
6
|
+
For more information on this file, see
|
7
|
+
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
8
|
+
"""
|
9
|
+
|
10
|
+
import os
|
11
|
+
|
12
|
+
from django.core.wsgi import get_wsgi_application
|
13
|
+
|
14
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dStats.settings")
|
15
|
+
|
16
|
+
application = get_wsgi_application()
|
@@ -0,0 +1,13 @@
|
|
1
|
+
DO WHAT THE F*** YOU WANT TO PUBLIC LICENSE
|
2
|
+
Version 2, December 2004
|
3
|
+
|
4
|
+
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
5
|
+
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
7
|
+
copies of this license document, and changing it is allowed as long
|
8
|
+
as the name is changed.
|
9
|
+
|
10
|
+
DO WHAT THE F*** YOU WANT TO PUBLIC LICENSE
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
12
|
+
|
13
|
+
0. You just DO WHAT THE F*** YOU WANT TO.
|
@@ -0,0 +1,158 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: dStats
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A real-time web-based monitoring tool that provides performance stats for Docker containers and visualizes their network connectivity graph
|
5
|
+
Home-page: https://github.com/Arifcse21/dStats
|
6
|
+
Author: Abdullah Al Arif
|
7
|
+
Author-email: arifcse21@gmail.com
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.12.3
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
License-File: LICENSE
|
14
|
+
Requires-Dist: Django>=5.1.4
|
15
|
+
Requires-Dist: graphviz>=0.20.3
|
16
|
+
Requires-Dist: daphne>=4.1.2
|
17
|
+
Requires-Dist: requests>=2.32.3
|
18
|
+
Requires-Dist: black>=24.10.0
|
19
|
+
Dynamic: author
|
20
|
+
Dynamic: author-email
|
21
|
+
Dynamic: classifier
|
22
|
+
Dynamic: description
|
23
|
+
Dynamic: description-content-type
|
24
|
+
Dynamic: home-page
|
25
|
+
Dynamic: requires-dist
|
26
|
+
Dynamic: requires-python
|
27
|
+
Dynamic: summary
|
28
|
+
|
29
|
+
|
30
|
+
# **dStats**
|
31
|
+
|
32
|
+
**dStats** is a real-time web-based monitoring tool that provides performance stats for Docker containers and visualizes their network connectivity graph.
|
33
|
+
|
34
|
+
---
|
35
|
+
|
36
|
+
## **Deploy Container Directly**
|
37
|
+
Pull and run the container from Docker Hub:
|
38
|
+
|
39
|
+
```bash
|
40
|
+
docker pull arifcse21/dstats:latest
|
41
|
+
```
|
42
|
+
|
43
|
+
Run the container:
|
44
|
+
|
45
|
+
```bash
|
46
|
+
docker run -d --name docker-stats-web --privileged \
|
47
|
+
-v /var/run/docker.sock:/var/run/docker.sock \
|
48
|
+
-p 2743:2743 arifcse21/dstats:latest
|
49
|
+
```
|
50
|
+
|
51
|
+
---
|
52
|
+
|
53
|
+
## **Clone the Repository**
|
54
|
+
|
55
|
+
If you’d like to explore or modify the project, start by cloning the repository:
|
56
|
+
|
57
|
+
```bash
|
58
|
+
git clone https://github.com/Arifcse21/dStats.git
|
59
|
+
cd dStats
|
60
|
+
```
|
61
|
+
|
62
|
+
---
|
63
|
+
|
64
|
+
## **Run with Docker Manually**
|
65
|
+
|
66
|
+
Build the Docker image locally:
|
67
|
+
|
68
|
+
```bash
|
69
|
+
docker build -t dstats:latest .
|
70
|
+
```
|
71
|
+
|
72
|
+
Run the container:
|
73
|
+
|
74
|
+
```bash
|
75
|
+
docker run -d --name docker-stats-web --privileged \
|
76
|
+
-v /var/run/docker.sock:/var/run/docker.sock \
|
77
|
+
-p 2743:2743 dstats:latest
|
78
|
+
```
|
79
|
+
|
80
|
+
---
|
81
|
+
|
82
|
+
## **Run with Docker Compose**
|
83
|
+
|
84
|
+
Use Docker Compose for easier setup and teardown:
|
85
|
+
|
86
|
+
1. Build and start the services:
|
87
|
+
|
88
|
+
```bash
|
89
|
+
docker compose up -d
|
90
|
+
```
|
91
|
+
|
92
|
+
2. Stop and clean up the services:
|
93
|
+
|
94
|
+
```bash
|
95
|
+
docker compose down --remove-orphans --rmi all
|
96
|
+
```
|
97
|
+
|
98
|
+
---
|
99
|
+
|
100
|
+
## **Access the Application**
|
101
|
+
|
102
|
+
- Open your browser and go to:
|
103
|
+
**http://localhost:2743**
|
104
|
+
|
105
|
+
Here, you’ll find:
|
106
|
+
1. **Container Stats:** Real-time CPU, memory, and network I/O usage.
|
107
|
+
2. **Network Graph:** Visualization of container interconnections.
|
108
|
+
|
109
|
+
---
|
110
|
+
|
111
|
+
## **Contribute to dStats Project**
|
112
|
+
|
113
|
+
Thank you for considering contributing to dStats! We appreciate all efforts, big or small, to help improve the project.
|
114
|
+
|
115
|
+
### **How to Contribute**
|
116
|
+
|
117
|
+
We believe collaboration is key to building great software. Here’s how you can get involved:
|
118
|
+
|
119
|
+
1. **Report Issues**
|
120
|
+
Found a bug? Have a feature request? Open an issue [here](https://github.com/Arifcse21/dStats/issues).
|
121
|
+
|
122
|
+
2. **Suggest Enhancements**
|
123
|
+
Have an idea for improvement? Share it by opening a discussion or issue.
|
124
|
+
|
125
|
+
3. **Contribute Code**
|
126
|
+
Whether it’s fixing bugs, adding features, or enhancing documentation, here’s how to start:
|
127
|
+
- Fork this repository.
|
128
|
+
- Clone your fork:
|
129
|
+
```bash
|
130
|
+
git clone https://github.com/your-username/dStats.git
|
131
|
+
cd dStats
|
132
|
+
```
|
133
|
+
- Create a branch:
|
134
|
+
```bash
|
135
|
+
git checkout -b my-feature
|
136
|
+
```
|
137
|
+
- Commit your changes:
|
138
|
+
```bash
|
139
|
+
git commit -m "Add my feature"
|
140
|
+
```
|
141
|
+
- Push your branch:
|
142
|
+
```bash
|
143
|
+
git push origin my-feature
|
144
|
+
```
|
145
|
+
- Open a pull request on GitHub.
|
146
|
+
|
147
|
+
4. **Improve Documentation**
|
148
|
+
Good documentation helps everyone. Spot typos? Want to clarify something? Update the `README.md` or other docs and send us a PR.
|
149
|
+
|
150
|
+
---
|
151
|
+
|
152
|
+
### **Need Help?**
|
153
|
+
|
154
|
+
Feel free to reach out by opening a discussion on the repository. We’re here to help!
|
155
|
+
|
156
|
+
Thank you for being part of this project. Together, we can make dStats even better. 🎉
|
157
|
+
|
158
|
+
---
|
@@ -0,0 +1,11 @@
|
|
1
|
+
dStats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
dStats/asgi.py,sha256=MSQjGWX7GgrxE5_GvTsz8PRIiXwgCcx3PIkkUsQ1_tU,389
|
3
|
+
dStats/settings.py,sha256=D73YEbwOWk54QosU21YvjSTVJvxkHTqUXiel76Nxpl4,3379
|
4
|
+
dStats/urls.py,sha256=R7CHnIif6hO8wr-C_HH7ew3-ZT8KtnE31YO9XT1Q8kU,816
|
5
|
+
dStats/views.py,sha256=mjxkwpwwZcS2rAemyaLzfrEOx3WmMHTj3LFGW5ruUwI,9427
|
6
|
+
dStats/wsgi.py,sha256=5CVIfhfO5NEUV3gXOmrPPjLNkeararRk6fCzkghNI_Q,389
|
7
|
+
dStats-0.1.0.dist-info/LICENSE,sha256=qjNhYHFWgrHF5vENj51WBRO85HtL38pwfTIg8huGj08,483
|
8
|
+
dStats-0.1.0.dist-info/METADATA,sha256=dkOcRoxHFYkSXru_0xTWsarzPLLS8XdZIHlr7Qsn8_c,3805
|
9
|
+
dStats-0.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
10
|
+
dStats-0.1.0.dist-info/top_level.txt,sha256=ZqX7qLq-LiHI4j3UAw9S9kHfeD094Jtxc5sdnrcNhU8,7
|
11
|
+
dStats-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
dStats
|