Compare commits
	
		
			2 Commits
		
	
	
		
			08380b2ca3
			...
			eaeecc926a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| eaeecc926a | |||
| 51950cb7d2 | 
							
								
								
									
										13
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | # Changelog | ||||||
|  | 
 | ||||||
|  | ## [0.1.0] - 2023-09-16 | ||||||
|  | ### Added | ||||||
|  | - Web interface | ||||||
|  | - Fully featured RestFullAPI v1; | ||||||
|  | - Monitoring free space in storage; | ||||||
|  | - Deleting an archive or ticket also deletes physical files; | ||||||
|  | - Flexible deployment configuration using environment variables; | ||||||
|  | - Dockerized app, the image size is less than 150mb; | ||||||
|  | - Support sqlite3 and PostgreSQL^15; | ||||||
|  | - Whitenoise Static management; | ||||||
|  | - healthcheck checking application availability; | ||||||
							
								
								
									
										324
									
								
								README-ru.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								README-ru.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,324 @@ | |||||||
|  | # LOGS-COLLECTOR | ||||||
|  | 
 | ||||||
|  | ```sh | ||||||
|  | █░░ █▀█ █▀▀ █▀ ▄▄ █▀▀ █▀█ █░░ █░░ █▀▀ █▀▀ ▀█▀ █▀█ █▀█ | ||||||
|  | █▄▄ █▄█ █▄█ ▄█ ░░ █▄▄ █▄█ █▄▄ █▄▄ ██▄ █▄▄ ░█░ █▄█ █▀▄ | ||||||
|  | ``` | ||||||
|  | ###  [English lang: README.md](README.md) | ||||||
|  | 
 | ||||||
|  | ###  [CHANGELOG.md](CHANGELOG.md) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Цель | ||||||
|  | 
 | ||||||
|  | Если вы являетесь разработчиком ПО которое в дальнейшем клиенты используют в своей инфраструктуре, вы должны понимать, как иногда бывает трудно изучить проблему с ПО не имея доступа к серверу на котором это ПО работает. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Для решения этой задачи вы можете настраивать ПО на автоматическую отправку обезличенных отчетов о сбоях например использовать Sentry. Это не всегда приемлемо для клиента, к тому же информация может быть не полной или клиенту требуется повышенная конфиденциальность. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | В таком случае вы можете попросить клиента отправить вам нужные лог файлы и изучить их в последствии. Но тут возникает другая проблема вам нужен безопасный способ передачи этих файлов как для вас так и для клиента. | ||||||
|  | Это мог быть FTP, SFTP, облако etc. Но что если вы не хотите давать клиенту данные для аутентификации и авторизации? | ||||||
|  | 
 | ||||||
|  | Возможно у вас есть доступ к серверу клиента и вы можете прочитать лог файлы на месте. И казалось бы проблема решена. Но на сервере клиента могут отсутствовать инструменты для удобного изучения лог файлов. | ||||||
|  | Даже если сотрудник поддержки может забрать себе нужные файлы и изучить их локально, возникает проблема распространения этих файлов между другими сотрудниками. | ||||||
|  | 
 | ||||||
|  | Logs-collector позволяет решить эти задачи. | ||||||
|  | 
 | ||||||
|  | Logs-collector является удаленным хранилищем и может принимать и отдавать файлы. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Термины | ||||||
|  | - Платформа: это ПО разработанное вашей компанией | ||||||
|  | - Тикет: это номер связанный с тикетом в вашей help desk системе | ||||||
|  | - Архив: это загруженный лог файл (поддерживается любой формат) | ||||||
|  | 
 | ||||||
|  | ## Как это работает? | ||||||
|  | 
 | ||||||
|  | - Создаете платформы | ||||||
|  | - Создаете тикет связанный с платформой и номером  | ||||||
|  | - Передаете клиенту уникальный токен тикета | ||||||
|  | - Клиент загружает архив лог файлов | ||||||
|  | - Скачиваете архив (находите решение проблемы) | ||||||
|  | - Удаляете архив или тикет или отмечаете тикет решенным | ||||||
|  | 
 | ||||||
|  | ## Особенности | ||||||
|  | 
 | ||||||
|  | - Централизованное хранилище | ||||||
|  | - Для загрузки файла не нужно давать auth credentials | ||||||
|  | - Каждый токен на загрузку уникален и связан только с одним тикетом | ||||||
|  | - Токен имеет ограничение на количество попыток и время жизни | ||||||
|  | - Загрузить файл можно из консоли или через веб | ||||||
|  | - Полнофункциональный RestFullAPI v1 | ||||||
|  | - Мониторинг свободного пространства в хранилище | ||||||
|  | - Удаление архива или тикета так же удаляет физические файлы | ||||||
|  | - Приложение соответствует архитектуре приложения 12 факторов | ||||||
|  | - Гибкая настройка развертывания переменными окружения | ||||||
|  | - Приложение докеризировано, размер образа меньше 150mb | ||||||
|  | - Может работать как с sqlite3 так и с PostgreSQL^15 | ||||||
|  | - Управление статикой без настройки для этого веб сервера | ||||||
|  | - healthcheck проверка доступности приложения | ||||||
|  | 
 | ||||||
|  | ## Безопасность | ||||||
|  | 
 | ||||||
|  | - Токен на загрузку не связан с авторизацией | ||||||
|  | - Токен на загрузку обладает высокой энтропией. | ||||||
|  | - Двухфакторная аутентификация для пользователей | ||||||
|  | - Для скачивания файла - 2FA должна быть принудительно включена | ||||||
|  | - Админ панель пропатчена на принудительное использование 2FA | ||||||
|  | - Пользователь в контейнере является не привилегированным  | ||||||
|  | - Стандартные методы защиты Django и DRF | ||||||
|  | 
 | ||||||
|  | ## Установка | ||||||
|  | 
 | ||||||
|  | ### Из docker образа: | ||||||
|  | - Создайте директорию для приложения где вам удобно | ||||||
|  | - Создайте файл docker-compose.yml в директории приложения | ||||||
|  | - Создайте файл .env в директории приложения | ||||||
|  | - Наполните файл .env требуемыми переменными окружения см. ниже | ||||||
|  | 
 | ||||||
|  | >Пример файла с использованием хранилища докер и sqlite как база данных по умолчанию: | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | version: "3" | ||||||
|  | 
 | ||||||
|  | # to set environment variables: | ||||||
|  | # create a .env file in the same directory as docker-compose.yaml | ||||||
|  | 
 | ||||||
|  | services: | ||||||
|  |   server: | ||||||
|  |     image: mois3y/logs_collector:0.1.0 | ||||||
|  |     container_name: logs-collector | ||||||
|  |     restart: unless-stopped | ||||||
|  |     env_file: | ||||||
|  |       - ./.env | ||||||
|  |     ports: | ||||||
|  |       - "80:8000" | ||||||
|  |     volumes: | ||||||
|  |       - /etc/timezone:/etc/timezone:ro  # optional | ||||||
|  |       - /etc/localtime:/etc/localtime:ro  # optional | ||||||
|  |       - logs_collector_data:/data | ||||||
|  | 
 | ||||||
|  | volumes: | ||||||
|  |   logs_collector_data: | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Из исходников: | ||||||
|  | - Клонируйте репозиторий | ||||||
|  | - docker-compose.yaml уже есть в директории с проектом | ||||||
|  | - создайте в корне проекта файл .env | ||||||
|  | - наполните .env требуемыми переменными окружения см. ниже | ||||||
|  | - соберите образ и запустите контейнер в фоне: | ||||||
|  |    | ||||||
|  | ```sh | ||||||
|  | docker-compose up -d --build | ||||||
|  | ``` | ||||||
|  | - Вы можете создать свой файл и внести нужные правки: | ||||||
|  | #### docker-compose-example-psql.yaml c PostgreSQL по умолчанию: | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | services: | ||||||
|  |   logs_collector: | ||||||
|  |     container_name: logs-collector | ||||||
|  |     build: | ||||||
|  |       context: . | ||||||
|  |       args: | ||||||
|  |         - VERSION=${VERSION} | ||||||
|  |         - SRC_DIR=${SRC_DIR} | ||||||
|  |         - SCRIPTS_DIR=${SCRIPTS_DIR} | ||||||
|  |         - APP_DIR=${APP_DIR} | ||||||
|  |         - DATA_DIR=${DATA_DIR} | ||||||
|  |         - WEB_PORT=${WEB_PORT} | ||||||
|  |         - USER_NAME=${USER_NAME} | ||||||
|  |         - USER_GROUP=${USER_GROUP} | ||||||
|  |         - APP_UID=${APP_UID} | ||||||
|  |         - APP_GID=${APP_GID} | ||||||
|  |     ports: | ||||||
|  |       - "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}" | ||||||
|  |     volumes: | ||||||
|  |       - type: volume | ||||||
|  |         source: logs_collector_data | ||||||
|  |         target: ${APP_DIR}/data | ||||||
|  |     env_file: | ||||||
|  |       - ./.env | ||||||
|  |     depends_on: | ||||||
|  |       - db | ||||||
|  |        | ||||||
|  |   db: | ||||||
|  |     image: postgres:15-alpine3.18 | ||||||
|  |     container_name: psql-collector | ||||||
|  |     volumes: | ||||||
|  |       - logs_collector_psql_data:/var/lib/postgresql/data/ | ||||||
|  |     env_file: | ||||||
|  |       - ./.env | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | volumes: | ||||||
|  |   logs_collector_data: | ||||||
|  |   logs_collector_psql_data: | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### docker-compose-example-psql.yaml c sqlite и bind-mount: | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | version: "3" | ||||||
|  | 
 | ||||||
|  | # to set environment variables: | ||||||
|  | # create a .env file in the same directory as docker-compose.yaml | ||||||
|  | 
 | ||||||
|  | services: | ||||||
|  |   logs_collector: | ||||||
|  |     container_name: logs-collector | ||||||
|  |     build: | ||||||
|  |       context: . | ||||||
|  |       args: | ||||||
|  |         - VERSION=${VERSION} | ||||||
|  |         - SRC_DIR=${SRC_DIR} | ||||||
|  |         - SCRIPTS_DIR=${SCRIPTS_DIR} | ||||||
|  |         - APP_DIR=${APP_DIR} | ||||||
|  |         - DATA_DIR=${DATA_DIR} | ||||||
|  |         - WEB_PORT=${WEB_PORT} | ||||||
|  |         - USER_NAME=${USER_NAME} | ||||||
|  |         - USER_GROUP=${USER_GROUP} | ||||||
|  |         - APP_UID=${APP_UID} | ||||||
|  |         - APP_GID=${APP_GID} | ||||||
|  |     ports: | ||||||
|  |       - "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}" | ||||||
|  |     volumes: | ||||||
|  |       - "/opt/collector/data:${DATA_DIR}" | ||||||
|  |       - "/opt/collector/data/db.sqlite3:${DATA_DIR}/db.sqlite3" | ||||||
|  |     env_file: | ||||||
|  |       - /.env | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 🔴 | ||||||
|  | 
 | ||||||
|  | ❗ВАЖНО❗ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Если вы используете bind-mount и монтируете его в хранилище приложения, помните | ||||||
|  | пользователь в контейнере не привилегирован UID 1000 если примонтированный файл | ||||||
|  | или директория будет принадлежать root приложение не сможет его прочитать и | ||||||
|  | следовательно работать. | ||||||
|  | 
 | ||||||
|  | В продакшн среде используйте приложение за вашим любимым обратным прокси. | ||||||
|  | 
 | ||||||
|  | Просто добавьте его в стек docker-compose.yaml | ||||||
|  | 
 | ||||||
|  | >Можно этого не делать, но Gunicorn рекомендуют придерживаться этого правила. | ||||||
|  | > | ||||||
|  | >Я солидарен с ними, так что вас предупредили) | ||||||
|  | 
 | ||||||
|  | 🔴 | ||||||
|  | 
 | ||||||
|  | ## Переменные окружения: | ||||||
|  | >Приложение можно настроить, для этого передайте следующие возможные переменные | ||||||
|  | >окружения. | ||||||
|  | >Если переменная не передана, будет использоваться переменная окружения по умолчанию | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  |  █▀▄ ░░█ ▄▀█ █▄░█ █▀▀ █▀█ ▀ | ||||||
|  |  █▄▀ █▄█ █▀█ █░▀█ █▄█ █▄█ ▄ | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | | ENV                  | DEFAULT         | INFO                     | | ||||||
|  | | -------------------- | --------------- | ------------------------ | | ||||||
|  | | SECRET_KEY           | j9QGbvM9Z4otb47 | ❗change this immediately| | ||||||
|  | | DEBUG                | False           | use only False in prod   | | ||||||
|  | | ALLOWED_HOSTS        | '*'             | list separated by commas | | ||||||
|  | | CSRF_TRUSTED_ORIGINS |                 | list separated by commas | | ||||||
|  | | DB_URL               |                 | url for connect db       | | ||||||
|  | | TZ                   | 'UTC'           | server timezone          | | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | [CSRF_TRUSTED_ORIGINS](https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins) | ||||||
|  | 
 | ||||||
|  | Требуется в среде докер в продакшн окружении | ||||||
|  | принимает список url разделенных запятой | ||||||
|  | >http://localhost,http://*.domain.com,http://127.0.0.1,http://0.0.0.0 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | [DB_URL](https://django-environ.readthedocs.io/en/latest/quickstart.html) | ||||||
|  | 
 | ||||||
|  | Нужно указывать если вы хотите использовать PostgreSQL | ||||||
|  | Эти данные должны совпадать с переменными контейнера PostgreSQL | ||||||
|  | 
 | ||||||
|  | | ENV               | VALUE          | | ||||||
|  | | ----------------- | -------------- | | ||||||
|  | | POSTGRES_USER     | admin          | | ||||||
|  | | POSTGRES_PASSWORD | ddkwndkjdX7RrP | | ||||||
|  | | POSTGRES_DB       | collector      | | ||||||
|  | 
 | ||||||
|  | Пример: | ||||||
|  | 
 | ||||||
|  | #### psql://admin:ddkwndkjdX7RrP@psql-collector:5432/collector | ||||||
|  | - Протокол: **psql://** | ||||||
|  | - Пользователь: **admin** | ||||||
|  | - Пароль: **ddkwndkjdX7RrP** | ||||||
|  | - IP адрес: **psql-collector** | ||||||
|  | - Порт: **5432** | ||||||
|  | - Имя БД: **collector** | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | █▀▀ █░█ █▄░█ █ █▀▀ █▀█ █▀█ █▄░█ ▀ | ||||||
|  | █▄█ █▄█ █░▀█ █ █▄▄ █▄█ █▀▄ █░▀█ ▄ | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | | ENV                         | DEFAULT        | | ||||||
|  | | --------------------------- | -------------- | | ||||||
|  | | GUNICORN_BIND               | '0.0.0.0:8000' | | ||||||
|  | | GUNICORN_BACKLOG            | 2048           | | ||||||
|  | | GUNICORN_WORKERS            | 2              | | ||||||
|  | | GUNICORN_WORKER_CLASS       | 'sync'         | | ||||||
|  | | GUNICORN_WORKER_CONNECTIONS | 1000           | | ||||||
|  | | GUNICORN_THREADS            | 1              | | ||||||
|  | | GUNICORN_TIMEOUT            | 3600           | | ||||||
|  | | GUNICORN_KEEPALIVE          | 2              | | ||||||
|  | | GUNICORN_LOGLEVEL           | 'info'         | | ||||||
|  | 
 | ||||||
|  | [GUNICORN_*](https://docs.gunicorn.org/en/stable/settings.html) | ||||||
|  | 
 | ||||||
|  | Подробная информация о каждой переменной окружения доступна в официальной документации. | ||||||
|  | 
 | ||||||
|  | GUNICORN_BIND не изменяйте это так как переменная отвечает за прослушиваемый адрес и порт внутри контейнера. | ||||||
|  | 
 | ||||||
|  | GUNICORN_TIMEOUT по умолчанию установлена в 3600. Такой большой таймаут нужен для загрузки больших файлов. | ||||||
|  | Поскольку я старался сделать приложение минималистичным и не использовать менеджер задач загрузка файла идет в один поток. | ||||||
|  | 
 | ||||||
|  | Если время загрузки будет больше часа соединение разорвется, это особенность синхронной работы воркеров gunicorn если вам не хватает времени на загрузку вы можете увеличить это значение. | ||||||
|  | 
 | ||||||
|  | ❗ВАЖНО❗ | ||||||
|  | 
 | ||||||
|  | Gunicorn настроен писать в лог в следующем формате: | ||||||
|  | ```python | ||||||
|  | '%({X-Forwarded-For}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' | ||||||
|  | ``` | ||||||
|  | Это значит что в логе будет видно IP адрес запроса только из заголовка | ||||||
|  | 
 | ||||||
|  | **X-Forwarded-For** | ||||||
|  | 
 | ||||||
|  | В продакшн среде приложение должно быть за обратным прокси | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Помощники | ||||||
|  | В корне репозитория проекта есть директория scripts в ней лежит скрипт uploader.sh с помощью которого можно отправить файлы из консоли используя curl. | ||||||
|  | 
 | ||||||
|  | Синтаксис простой: | ||||||
|  | 
 | ||||||
|  | ```cmd | ||||||
|  | Usage: ./uploader.sh [options [parameters]] | ||||||
|  | 
 | ||||||
|  | Options: | ||||||
|  | 
 | ||||||
|  |  -f | --file     full path to upload file required | ||||||
|  |  -t | --token    access token             required | ||||||
|  |  -u | --url      target url               required | ||||||
|  |  -v | --version  print version | ||||||
|  |  -h | --help     print help | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Лицензия | ||||||
|  | 
 | ||||||
|  | GNU GPL 3.0 | ||||||
							
								
								
									
										330
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										330
									
								
								README.md
									
									
									
									
									
								
							| @ -1,3 +1,329 @@ | |||||||
| # logs-collector | # LOGS-COLLECTOR | ||||||
| 
 | 
 | ||||||
| Серверная сторона для получения и хранения лог файлов | ```sh | ||||||
|  | █░░ █▀█ █▀▀ █▀ ▄▄ █▀▀ █▀█ █░░ █░░ █▀▀ █▀▀ ▀█▀ █▀█ █▀█ | ||||||
|  | █▄▄ █▄█ █▄█ ▄█ ░░ █▄▄ █▄█ █▄▄ █▄▄ ██▄ █▄▄ ░█░ █▄█ █▀▄ | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ###  [CHANGELOG.md](CHANGELOG.md) | ||||||
|  | 
 | ||||||
|  | ###  [Russian lang: README.md](README-ru.md) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Purpose | ||||||
|  | 
 | ||||||
|  | If you are a developer of software that clients later use in their infrastructure, | ||||||
|  | you must understand how sometimes it can be difficult to research a problem | ||||||
|  | with software without access to the server on which this software runs. | ||||||
|  | 
 | ||||||
|  | To solve this problem, you can configure the software to automatically send  | ||||||
|  | anonymized crash reports, for example, use Sentry.  | ||||||
|  | This is not always acceptable to the client;  | ||||||
|  | Moreover, the information may not be complete or the client  | ||||||
|  | requires increased confidentiality. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Terms | ||||||
|  | - Platform: this is software developed by your company | ||||||
|  | - Ticket: this is the number associated with the ticket in your help desk system | ||||||
|  | - Archive: this is an uploaded log file (any format is supported) | ||||||
|  | 
 | ||||||
|  | ## How it works? | ||||||
|  | 
 | ||||||
|  | - Create platforms | ||||||
|  | - Create a ticket associated with the platform and number | ||||||
|  | - Transfer a unique ticket token to the client | ||||||
|  | - The client downloads an archive of log files | ||||||
|  | - Download the archive (find a solution to the problem) | ||||||
|  | - Delete the archive or ticket or mark the ticket as resolved | ||||||
|  |    | ||||||
|  | ## Features | ||||||
|  | 
 | ||||||
|  | - Centralized storage; | ||||||
|  | - To download a file you do not need to provide auth credentials; | ||||||
|  | - Each download token is unique and associated with only one ticket; | ||||||
|  | - The token has a limit on the number of attempts and lifetime; | ||||||
|  | - You can download the file from the console or via the web; | ||||||
|  | - Fully featured RestFullAPI v1; | ||||||
|  | - Monitoring free space in storage; | ||||||
|  | - Deleting an archive or ticket also deletes physical files; | ||||||
|  | - The application follows the 12 factors application architecture; | ||||||
|  | - Flexible deployment configuration using environment variables; | ||||||
|  | - The application is dockerized, the image size is less than 150mb; | ||||||
|  | - Can work with both sqlite3 and PostgreSQL^15; | ||||||
|  | - Static management without configuration for this web server; | ||||||
|  | - healthcheck checking application availability; | ||||||
|  | 
 | ||||||
|  | ## Security | ||||||
|  | 
 | ||||||
|  | - The download token is not associated with authorization | ||||||
|  | - The download token has high entropy. | ||||||
|  | - Two-factor authentication for users | ||||||
|  | - To download a file - 2FA must be forcibly enabled | ||||||
|  | - The admin panel has been patched to force the use of 2FA | ||||||
|  | - The user in the container is not privileged | ||||||
|  | - Standard Django and DRF protection methods | ||||||
|  | 
 | ||||||
|  | ## Install | ||||||
|  | 
 | ||||||
|  | ### From the docker image: | ||||||
|  | - Create a directory for the application wherever it is convenient for you | ||||||
|  | - Create a docker-compose.yml file in the application directory | ||||||
|  | - Create a .env file in the application directory | ||||||
|  | - Fill the .env file with the required environment variables, see below | ||||||
|  | 
 | ||||||
|  | >Example file using docker store and sqlite as default database: | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | version: "3" | ||||||
|  | 
 | ||||||
|  | # to set environment variables: | ||||||
|  | # create a .env file in the same directory as docker-compose.yaml | ||||||
|  | 
 | ||||||
|  | services: | ||||||
|  |   server: | ||||||
|  |     image: mois3y/logs_collector:0.1.0 | ||||||
|  |     container_name: logs-collector | ||||||
|  |     restart: unless-stopped | ||||||
|  |     env_file: | ||||||
|  |       - ./.env | ||||||
|  |     ports: | ||||||
|  |       - "80:8000" | ||||||
|  |     volumes: | ||||||
|  |       - /etc/timezone:/etc/timezone:ro  # optional | ||||||
|  |       - /etc/localtime:/etc/localtime:ro  # optional | ||||||
|  |       - logs_collector_data:/data | ||||||
|  | 
 | ||||||
|  | volumes: | ||||||
|  |   logs_collector_data: | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### From the source: | ||||||
|  | - Clone the repository | ||||||
|  | - docker-compose.yaml is already in the project directory | ||||||
|  | - create a .env file in the project root | ||||||
|  | - fill .env with the required environment variables, see below | ||||||
|  | - build the image and run the container in the background: | ||||||
|  |    | ||||||
|  | ```sh | ||||||
|  | docker-compose up -d --build | ||||||
|  | ``` | ||||||
|  | - You can create your own file and make the necessary edits: | ||||||
|  | #### docker-compose.yaml PostgreSQL by default: | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | services: | ||||||
|  |   logs_collector: | ||||||
|  |     container_name: logs-collector | ||||||
|  |     build: | ||||||
|  |       context: . | ||||||
|  |       args: | ||||||
|  |         - VERSION=${VERSION} | ||||||
|  |         - SRC_DIR=${SRC_DIR} | ||||||
|  |         - SCRIPTS_DIR=${SCRIPTS_DIR} | ||||||
|  |         - APP_DIR=${APP_DIR} | ||||||
|  |         - DATA_DIR=${DATA_DIR} | ||||||
|  |         - WEB_PORT=${WEB_PORT} | ||||||
|  |         - USER_NAME=${USER_NAME} | ||||||
|  |         - USER_GROUP=${USER_GROUP} | ||||||
|  |         - APP_UID=${APP_UID} | ||||||
|  |         - APP_GID=${APP_GID} | ||||||
|  |     ports: | ||||||
|  |       - "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}" | ||||||
|  |     volumes: | ||||||
|  |       - type: volume | ||||||
|  |         source: logs_collector_data | ||||||
|  |         target: ${APP_DIR}/data | ||||||
|  |     env_file: | ||||||
|  |       - ./.env | ||||||
|  |     depends_on: | ||||||
|  |       - db | ||||||
|  |        | ||||||
|  |   db: | ||||||
|  |     image: postgres:15-alpine3.18 | ||||||
|  |     container_name: psql-collector | ||||||
|  |     volumes: | ||||||
|  |       - logs_collector_psql_data:/var/lib/postgresql/data/ | ||||||
|  |     env_file: | ||||||
|  |       - ./.env | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | volumes: | ||||||
|  |   logs_collector_data: | ||||||
|  |   logs_collector_psql_data: | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### docker-compose-example-psql.yaml c sqlite и bind-mount: | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | version: "3" | ||||||
|  | 
 | ||||||
|  | # to set environment variables: | ||||||
|  | # create a .env file in the same directory as docker-compose.yaml | ||||||
|  | 
 | ||||||
|  | services: | ||||||
|  |   logs_collector: | ||||||
|  |     container_name: logs-collector | ||||||
|  |     build: | ||||||
|  |       context: . | ||||||
|  |       args: | ||||||
|  |         - VERSION=${VERSION} | ||||||
|  |         - SRC_DIR=${SRC_DIR} | ||||||
|  |         - SCRIPTS_DIR=${SCRIPTS_DIR} | ||||||
|  |         - APP_DIR=${APP_DIR} | ||||||
|  |         - DATA_DIR=${DATA_DIR} | ||||||
|  |         - WEB_PORT=${WEB_PORT} | ||||||
|  |         - USER_NAME=${USER_NAME} | ||||||
|  |         - USER_GROUP=${USER_GROUP} | ||||||
|  |         - APP_UID=${APP_UID} | ||||||
|  |         - APP_GID=${APP_GID} | ||||||
|  |     ports: | ||||||
|  |       - "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}" | ||||||
|  |     volumes: | ||||||
|  |       - "/opt/collector/data:${DATA_DIR}" | ||||||
|  |       - "/opt/collector/data/db.sqlite3:${DATA_DIR}/db.sqlite3" | ||||||
|  |     env_file: | ||||||
|  |       - /.env | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 🔴 | ||||||
|  | 
 | ||||||
|  | ❗IMPORTANT❗ | ||||||
|  | 
 | ||||||
|  | If you are using bind-mount and mounting it to your application's storage, | ||||||
|  | remember user in container is not privileged UID 1000 if mounted file | ||||||
|  | or the directory will belong to the root | ||||||
|  | application will not be able to read it and therefore work. | ||||||
|  | 
 | ||||||
|  | In a production environment, use the application behind your favorite reverse proxy. | ||||||
|  | 
 | ||||||
|  | Just add it to the docker-compose.yaml stack | ||||||
|  | 
 | ||||||
|  | >You don't have to do this, but Gunicorn recommends following this rule. | ||||||
|  | > | ||||||
|  | >I agree with them, so you have been warned) | ||||||
|  | 
 | ||||||
|  | 🔴 | ||||||
|  | 
 | ||||||
|  | ## Environment: | ||||||
|  | >The application can be configured, | ||||||
|  | >to do this, pass the following possible variables surroundings. | ||||||
|  | >If no variable is passed, the default environment variable will be used | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  |  █▀▄ ░░█ ▄▀█ █▄░█ █▀▀ █▀█ ▀ | ||||||
|  |  █▄▀ █▄█ █▀█ █░▀█ █▄█ █▄█ ▄ | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | | ENV                  | DEFAULT         | INFO                     | | ||||||
|  | | -------------------- | --------------- | ------------------------ | | ||||||
|  | | SECRET_KEY           | j9QGbvM9Z4otb47 | ❗change this immediately| | ||||||
|  | | DEBUG                | False           | use only False in prod   | | ||||||
|  | | ALLOWED_HOSTS        | '*'             | list separated by commas | | ||||||
|  | | CSRF_TRUSTED_ORIGINS |                 | list separated by commas | | ||||||
|  | | DB_URL               |                 | url for connect db       | | ||||||
|  | | TZ                   | 'UTC'           | server timezone          | | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | [CSRF_TRUSTED_ORIGINS](https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins) | ||||||
|  | 
 | ||||||
|  | Required in a Docker environment in a production environment | ||||||
|  | accepts a list of urls separated by commas | ||||||
|  | >http://localhost,http://*.domain.com,http://127.0.0.1,http://0.0.0.0 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | [DB_URL](https://django-environ.readthedocs.io/en/latest/quickstart.html) | ||||||
|  | 
 | ||||||
|  | Must be specified if you want to use PostgreSQL | ||||||
|  | This data must match the PostgreSQL container variables | ||||||
|  | 
 | ||||||
|  | | ENV               | VALUE          | | ||||||
|  | | ----------------- | -------------- | | ||||||
|  | | POSTGRES_USER     | admin          | | ||||||
|  | | POSTGRES_PASSWORD | ddkwndkjdX7RrP | | ||||||
|  | | POSTGRES_DB       | collector      | | ||||||
|  | 
 | ||||||
|  | Example: | ||||||
|  | 
 | ||||||
|  | #### psql://admin:ddkwndkjdX7RrP@psql-collector:5432/collector | ||||||
|  | - Protocol: **psql://** | ||||||
|  | - User: **admin** | ||||||
|  | - Password: **ddkwndkjdX7RrP** | ||||||
|  | - Address: **psql-collector** | ||||||
|  | - Port: **5432** | ||||||
|  | - Database name: **collector** | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | █▀▀ █░█ █▄░█ █ █▀▀ █▀█ █▀█ █▄░█ ▀ | ||||||
|  | █▄█ █▄█ █░▀█ █ █▄▄ █▄█ █▀▄ █░▀█ ▄ | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | | ENV                         | DEFAULT        | | ||||||
|  | | --------------------------- | -------------- | | ||||||
|  | | GUNICORN_BIND               | '0.0.0.0:8000' | | ||||||
|  | | GUNICORN_BACKLOG            | 2048           | | ||||||
|  | | GUNICORN_WORKERS            | 2              | | ||||||
|  | | GUNICORN_WORKER_CLASS       | 'sync'         | | ||||||
|  | | GUNICORN_WORKER_CONNECTIONS | 1000           | | ||||||
|  | | GUNICORN_THREADS            | 1              | | ||||||
|  | | GUNICORN_TIMEOUT            | 3600           | | ||||||
|  | | GUNICORN_KEEPALIVE          | 2              | | ||||||
|  | | GUNICORN_LOGLEVEL           | 'info'         | | ||||||
|  | 
 | ||||||
|  | [GUNICORN_*](https://docs.gunicorn.org/en/stable/settings.html) | ||||||
|  | 
 | ||||||
|  | Detailed information about each environment variable is available in | ||||||
|  | the official documentation. | ||||||
|  | 
 | ||||||
|  | **GUNICORN_BIND** do not change this since the variable  | ||||||
|  | is responsible for the listening address and port inside the container. | ||||||
|  | 
 | ||||||
|  | **GUNICORN_TIMEOUT** is set to 3600 by default. | ||||||
|  | Such a large timeout is needed to download large files. | ||||||
|  | Since I tried to make the application minimalistic and not use a task manager, | ||||||
|  | the file is downloaded in one thread. | ||||||
|  | 
 | ||||||
|  | If the loading time is more than an hour, the connection will be broken, | ||||||
|  | this is a feature of the synchronous operation of gunicorn workers; | ||||||
|  | if you do not have enough time to load, you can increase this value. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ❗IMPORTANT❗ | ||||||
|  | 
 | ||||||
|  | Gunicorn is configured to write to the log in the following format: | ||||||
|  | ```python | ||||||
|  | '%({X-Forwarded-For}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' | ||||||
|  | ``` | ||||||
|  | This means that the log will show the IP address of the request only from the header | ||||||
|  | 
 | ||||||
|  | **X-Forwarded-For** | ||||||
|  | 
 | ||||||
|  | In a production environment, the application must be behind a reverse proxy | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Helpers | ||||||
|  | At the root of the project repository there is a scripts directory, | ||||||
|  | it contains the uploader.sh script with which you can send files | ||||||
|  | from the console using **curl**. | ||||||
|  | 
 | ||||||
|  | The syntax is simple: | ||||||
|  | 
 | ||||||
|  | ```cmd | ||||||
|  | Usage: ./uploader.sh [options [parameters]] | ||||||
|  | 
 | ||||||
|  | Options: | ||||||
|  | 
 | ||||||
|  |  -f | --file     full path to upload file required | ||||||
|  |  -t | --token    access token             required | ||||||
|  |  -u | --url      target url               required | ||||||
|  |  -v | --version  print version | ||||||
|  |  -h | --help     print help | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## License | ||||||
|  | 
 | ||||||
|  | GNU GPL 3.0 | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import os | import os | ||||||
|  | import logging | ||||||
| from django.core.management.base import BaseCommand | from django.core.management.base import BaseCommand | ||||||
| from django.apps import apps | from django.apps import apps | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| @ -6,6 +7,31 @@ from django.conf import settings | |||||||
| from django.db.models import FileField | from django.db.models import FileField | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  | 
 | ||||||
|  | logging.config.dictConfig({ | ||||||
|  |     'version': 1, | ||||||
|  |     'disable_existing_loggers': False, | ||||||
|  |     'formatters': { | ||||||
|  |         'console': { | ||||||
|  |             'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s' | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     'handlers': { | ||||||
|  |         'console': { | ||||||
|  |             'class': 'logging.StreamHandler', | ||||||
|  |             'formatter': 'console' | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     'loggers': { | ||||||
|  |         '': { | ||||||
|  |             'level': 'INFO', | ||||||
|  |             'handlers': ['console'] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||||
|     # HELP MESSAGE: |     # HELP MESSAGE: | ||||||
|     help_part1 = 'This command deletes all media files from' |     help_part1 = 'This command deletes all media files from' | ||||||
| @ -14,10 +40,12 @@ class Command(BaseCommand): | |||||||
|     help = f'{help_part1} {help_part2} {help_part3}' |     help = f'{help_part1} {help_part2} {help_part3}' | ||||||
| 
 | 
 | ||||||
|     def handle(self, *args, **options): |     def handle(self, *args, **options): | ||||||
|  |         logger.info('Start cleanup storage....') | ||||||
|         all_models = apps.get_models() |         all_models = apps.get_models() | ||||||
|         physical_files = set() |         physical_files = set() | ||||||
|         db_files = set() |         db_files = set() | ||||||
|         # Get all files from the database |         # Get all files from the database | ||||||
|  |         logger.info('Get all files from the database....') | ||||||
|         for model in all_models: |         for model in all_models: | ||||||
|             file_fields = [] |             file_fields = [] | ||||||
|             filters = Q() |             filters = Q() | ||||||
| @ -35,7 +63,9 @@ class Command(BaseCommand): | |||||||
|                     flat=True |                     flat=True | ||||||
|                 ).distinct() |                 ).distinct() | ||||||
|                 db_files.update(files) |                 db_files.update(files) | ||||||
|  |         logger.info(f'Find: {len(db_files)} files from the database') | ||||||
|         # Get all files from the MEDIA_ROOT, recursively |         # Get all files from the MEDIA_ROOT, recursively | ||||||
|  |         logger.info('Get all files from the MEDIA_ROOT, recursively....') | ||||||
|         media_root = getattr(settings, 'MEDIA_ROOT', None) |         media_root = getattr(settings, 'MEDIA_ROOT', None) | ||||||
|         if media_root is not None: |         if media_root is not None: | ||||||
|             for relative_root, dirs, files in os.walk(media_root): |             for relative_root, dirs, files in os.walk(media_root): | ||||||
| @ -46,14 +76,21 @@ class Command(BaseCommand): | |||||||
|                         os.path.relpath(relative_root, media_root), file_ |                         os.path.relpath(relative_root, media_root), file_ | ||||||
|                     ) |                     ) | ||||||
|                     physical_files.add(relative_file) |                     physical_files.add(relative_file) | ||||||
|  |         logger.info(f'Find: {len(physical_files)} files from the MEDIA_ROOT') | ||||||
|         # Compute the difference and delete those files |         # Compute the difference and delete those files | ||||||
|  |         logger.info('Compute the difference and delete those files....') | ||||||
|         deletables = physical_files - db_files |         deletables = physical_files - db_files | ||||||
|  |         logger.info(f'Find: {len(deletables)} orphan files') | ||||||
|         if deletables: |         if deletables: | ||||||
|             for file_ in deletables: |             for file_ in deletables: | ||||||
|  |                 logger.info(f"Delete orphan file: {file_}") | ||||||
|                 os.remove(os.path.join(media_root, file_)) |                 os.remove(os.path.join(media_root, file_)) | ||||||
|             # Bottom-up - delete all empty folders |             # Bottom-up - delete all empty folders | ||||||
|  |             logger.info('Bottom-up - delete all empty folders....') | ||||||
|             for relative_root, dirs, files in os.walk( |             for relative_root, dirs, files in os.walk( | ||||||
|                     media_root, topdown=False): |                     media_root, topdown=False): | ||||||
|                 for dir_ in dirs: |                 for dir_ in dirs: | ||||||
|                     if not os.listdir(os.path.join(relative_root, dir_)): |                     if not os.listdir(os.path.join(relative_root, dir_)): | ||||||
|                         os.rmdir(os.path.join(relative_root, dir_)) |                         os.rmdir(os.path.join(relative_root, dir_)) | ||||||
|  |             logger.info('Done! Storage has been cleaned up') | ||||||
|  |         logger.info('Done! Nothing to delete') | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user