dStats 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|