
, destinat pentru colectarea, transformarea și trimiterea datelor de loguri, metrici și evenimente.
→
Scris în limbajul Rust, aceasta se remarcă printr-o performanță ridicată și un consum redus de memorie comparativ cu alternativele. În plus, s-a acordat o mare atenție funcțiilor legate de corectitudine, în special capacității de a salva evenimentele netrimise în buffer pe disc și de rotire a fișierelor.
Arhitectural, Vector este un router de evenimente, care primește mesaje din una sau mai multe surse, aplicând opțional transformări, și trimițându-le către una sau mai multe stinguri..
Vector este un substitut pentru filebeat și logstash, putând îndeplini ambele roluri (primirea și trimiterea logurilor), mai multe detalii despre ele .
Dacă în Logstash lanțul este construit ca input → filter → output, atunci în Vector este → →
Exemplele pot fi vizualizate în documentație.
Această instrucțiune este o versiune revizuită a instructajului de la . În instrucțiunea originală există procesare geoip. La testarea geoip din rețeaua internă, vector mă lăsa să primesc o eroare.
Aug 05 06:25:31.889 DEBUG transform{name=nginx_parse_rename_fields type=rename_fields}: vector::transforms::rename_fields: Câmpul nu exista câmp=«geoip.country_name» rate_limit_secs=30Dacă cineva trebuie să proceseze geoip, contactați instrucțiunea originală de la .
Vom configura legătura Nginx (loguri de acces) → Vector (Client | Filebeat) → Vector (Server | Logstash) → separat în Clickhouse și separat Elasticsearch. Vom instala 4 servere. Deși se pot face toate cu 3 servere.

Schema este aproximativ așa.
Dezactivăm Selinux pe toate serverele tale
sed -i 's/^SELINUX=.*$/SELINUX=disabled/g' /etc/selinux/config
rebootPe toate serverele instalăm emulatorul serverului HTTP + utilitare
Ca emulator de server HTTP vom folosi de la
Nodejs-stub-server nu are rpm. creăm un rpm pentru el. rpm-ul va fi creat cu ajutorul
Adăugăm repository-ul antonpatsev/nodejs-stub-server
yum -y install yum-plugin-copr epel-release
yes | yum copr enable antonpatsev/nodejs-stub-serverInstalăm nodejs-stub-server, Apache benchmark și multiplexorul de terminale screen pe toate serverele
yum -y install stub_http_server screen mc httpd-tools screenAm corectat în fișierul /var/lib/stub_http_server/stub_http_server.js timpul de răspuns al stub_http_server pentru a avea mai multe loguri.
var max_sleep = 10;Vom lansa stub_http_server.
systemctl start stub_http_server
systemctl enable stub_http_serverpe serverul 3
ClickHouse utilizează un set de instrucțiuni SSE 4.2, așa că, dacă nu se specifică altceva, suportul său în procesorul utilizat devine o cerință suplimentară pentru sistem. Iată comanda pentru a verifica dacă procesorul actual suportă SSE 4.2:
grep -q sse4_2 /proc/cpuinfo && echo "SSE 4.2 suportat" || echo "SSE 4.2 nu este suportat"Mai întâi trebuie să conectezi repository-ul oficial:
sudo yum install -y yum-utils
sudo rpm --import https://repo.clickhouse.tech/CLICKHOUSE-KEY.GPG
sudo yum-config-manager --add-repo https://repo.clickhouse.tech/rpm/stable/x86_64Pentru a instala pachetele, trebuie să executați următoarele comenzi:
sudo yum install -y clickhouse-server clickhouse-clientPermitem clickhouse-server să asculte interfața de rețea în fișierul /etc/clickhouse-server/config.xml
0.0.0.0Schimbăm nivelul de logare de la trace la debug
debug
Setările de compresie sunt standard:
min_compress_block_size 65536
max_compress_block_size 1048576Pentru a activa compresia Zstd, configurarea recomandată ar fi să nu o modificăm, ci mai bine să aplicăm DDL.

Nu am găsit cum să aplic compresia zstd prin DDL în Google. Așa că am lăsat-o așa.
Colegi, cine folosește compresia zstd în Clickhouse — vă rog să împărtășiți instrucțiunile.
Pentru a porni serverul ca demon, executați:
service clickhouse-server startAcum să trecem la configurarea Clickhouse
Accesăm Clickhouse
clickhouse-client -h 172.26.10.109 -m172.26.10.109 — IP-ul serverului unde este instalat Clickhouse.
Să creăm baza de date vector
CREATE DATABASE vector;Vom verifica dacă baza de date există.
show databases;Creăm tabelul vector.logs.
/* Это таблица где хранятся логи как есть */
CREATE TABLE vector.logs
(
`node_name` String,
`timestamp` DateTime,
`server_name` String,
`user_id` String,
`request_full` String,
`request_user_agent` String,
`request_http_host` String,
`request_uri` String,
`request_scheme` String,
`request_method` String,
`request_length` UInt64,
`request_time` Float32,
`request_referrer` String,
`response_status` UInt16,
`response_body_bytes_sent` UInt64,
`response_content_type` String,
`remote_addr` IPv4,
`remote_port` UInt32,
`remote_user` String,
`upstream_addr` IPv4,
`upstream_port` UInt32,
`upstream_bytes_received` UInt64,
`upstream_bytes_sent` UInt64,
`upstream_cache_status` String,
`upstream_connect_time` Float32,
`upstream_header_time` Float32,
`upstream_response_length` UInt64,
`upstream_response_time` Float32,
`upstream_status` UInt16,
`upstream_content_type` String,
INDEX idx_http_host request_http_host TYPE set(0) GRANULARITY 1
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY timestamp
TTL timestamp + toIntervalMonth(1)
SETTINGS index_granularity = 8192;Verificăm dacă au fost create tabelele. Rulăm clickhouse-client și facem o interogare.
Trecem în baza de date vector.
use vector;
Ok.
0 rows in set. Elapsed: 0.001 sec.Privim tabelele.
show tables;
┌─name────────────────┐
│ logs │
└─────────────────────┘Instalarea elasticsearch pe al patrulea server pentru a trimite aceleași date în Elasticsearch pentru comparație cu Clickhouse
Adăugăm cheia publică rpm
rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearchCreăm 2 repozitorii:
/etc/yum.repos.d/elasticsearch.repo
[elasticsearch]
name=Elasticsearch repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=0
autorefresh=1
type=rpm-md/etc/yum.repos.d/kibana.repo
[kibana-7.x]
name=Kibana repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-mdVom instala elasticsearch și kibana
yum install -y kibana elasticsearchDeoarece va fi într-un singur exemplar, în fișierul /etc/elasticsearch/elasticsearch.yml trebuie să adăugăm:
discovery.type: single-nodePentru ca vector să poată trimite date în elasticsearch de pe un alt server, vom modifica network.host.
network.host: 0.0.0.0Pentru a ne conecta la kibana, vom modifica parametrul server.host în fișierul /etc/kibana/kibana.yml
server.host: "0.0.0.0"Pornim și activăm elasticsearch la început
systemctl enable elasticsearch
systemctl start elasticsearchși kibana
systemctl enable kibana
systemctl start kibanaConfigurarea Elasticsearch pentru modul single-node 1 shard, 0 replica. Cel mai probabil veți avea un cluster format dintr-un număr mare de servere și nu va trebui să faceți acest lucru.
Pentru viitoarele indexuri, actualizăm șablonul implicit:
curl -X PUT http://localhost:9200/_template/default -H 'Content-Type: application/json' -d '{"index_patterns": ["*"],"order": -1,"settings": {"number_of_shards": "1","number_of_replicas": "0"}}' Instalare ca substitut pentru Logstash pe al doilea server
yum install -y https://packages.timber.io/vector/0.9.X/vector-x86_64.rpm mc httpd-tools screenVom configura Vector ca substitut pentru Logstash. Edităm fișierul /etc/vector/vector.toml
# /etc/vector/vector.toml
data_dir = "/var/lib/vector"
[sources.nginx_input_vector]
# General
type = "vector"
address = "0.0.0.0:9876"
shutdown_timeout_secs = 30
[transforms.nginx_parse_json]
inputs = [ "nginx_input_vector" ]
type = "json_parser"
[transforms.nginx_parse_add_defaults]
inputs = [ "nginx_parse_json" ]
type = "lua"
version = "2"
hooks.process = """
function (event, emit)
function split_first(s, delimiter)
result = {};
for match in (s..delimiter):gmatch("(.-)"..delimiter) do
table.insert(result, match);
end
return result[1];
end
function split_last(s, delimiter)
result = {};
for match in (s..delimiter):gmatch("(.-)"..delimiter) do
table.insert(result, match);
end
return result[#result];
end
event.log.upstream_addr = split_first(split_last(event.log.upstream_addr, ', '), ':')
event.log.upstream_bytes_received = split_last(event.log.upstream_bytes_received, ', ')
event.log.upstream_bytes_sent = split_last(event.log.upstream_bytes_sent, ', ')
event.log.upstream_connect_time = split_last(event.log.upstream_connect_time, ', ')
event.log.upstream_header_time = split_last(event.log.upstream_header_time, ', ')
event.log.upstream_response_length = split_last(event.log.upstream_response_length, ', ')
event.log.upstream_response_time = split_last(event.log.upstream_response_time, ', ')
event.log.upstream_status = split_last(event.log.upstream_status, ', ')
if event.log.upstream_addr == "" then
event.log.upstream_addr = "127.0.0.1"
end
if (event.log.upstream_bytes_received == "-" or event.log.upstream_bytes_received == "") then
event.log.upstream_bytes_received = "0"
end
if (event.log.upstream_bytes_sent == "-" or event.log.upstream_bytes_sent == "") then
event.log.upstream_bytes_sent = "0"
end
if event.log.upstream_cache_status == "" then
event.log.upstream_cache_status = "DISABLED"
end
if (event.log.upstream_connect_time == "-" or event.log.upstream_connect_time == "") then
event.log.upstream_connect_time = "0"
end
if (event.log.upstream_header_time == "-" or event.log.upstream_header_time == "") then
event.log.upstream_header_time = "0"
end
if (event.log.upstream_response_length == "-" or event.log.upstream_response_length == "") then
event.log.upstream_response_length = "0"
end
if (event.log.upstream_response_time == "-" or event.log.upstream_response_time == "") then
event.log.upstream_response_time = "0"
end
if (event.log.upstream_status == "-" or event.log.upstream_status == "") then
event.log.upstream_status = "0"
end
emit(event)
end
"""
[transforms.nginx_parse_remove_fields]
inputs = [ "nginx_parse_add_defaults" ]
type = "remove_fields"
fields = ["data", "file", "host", "source_type"]
[transforms.nginx_parse_coercer]
type = "coercer"
inputs = ["nginx_parse_remove_fields"]
types.request_length = "int"
types.request_time = "float"
types.response_status = "int"
types.response_body_bytes_sent = "int"
types.remote_port = "int"
types.upstream_bytes_received = "int"
types.upstream_bytes_send = "int"
types.upstream_connect_time = "float"
types.upstream_header_time = "float"
types.upstream_response_length = "int"
types.upstream_response_time = "float"
types.upstream_status = "int"
types.timestamp = "timestamp"
[sinks.nginx_output_clickhouse]
inputs = ["nginx_parse_coercer"]
type = "clickhouse"
database = "vector"
healthcheck = true
host = "http://172.26.10.109:8123" # Адрес Clickhouse
table = "logs"
encoding.timestamp_format = "unix"
buffer.type = "disk"
buffer.max_size = 104900000
buffer.when_full = "block"
request.in_flight_limit = 20
[sinks.elasticsearch]
type = "elasticsearch"
inputs = ["nginx_parse_coercer"]
compression = "none"
healthcheck = true
# 172.26.10.116 - сервер где установен elasticsearch
host = "http://172.26.10.116:9200"
index = "vector-%Y-%m-%d"Puteți corecta secțiunea transforms.nginx_parse_add_defaults.
Deoarece folosește aceste configurații pentru un CDN mic și acolo în upstream_* pot veni mai multe valori
De exemplu:
"upstream_addr": "128.66.0.10:443, 128.66.0.11:443, 128.66.0.12:443"
"upstream_bytes_received": "-, -, 123"
"upstream_status": "502, 502, 200"Dacă aceasta nu este situația dumneavoastră, atunci această secțiune poate fi simplificată
Vom crea configurația service pentru systemd /etc/systemd/system/vector.service
# /etc/systemd/system/vector.service
[Unit]
Description=Vector
After=network-online.target
Requires=network-online.target
[Service]
User=vector
Group=vector
ExecStart=/usr/bin/vector
ExecReload=/bin/kill -HUP $MAINPID
Restart=no
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=vector
[Install]
WantedBy=multi-user.targetDupă crearea tabelelor, putem porni Vector
systemctl enable vector
systemctl start vectorLogurile vector pot fi vizualizate astfel
journalctl -f -u vectorÎn loguri ar trebui să există următoarele înregistrări
INFO vector::topology::builder: Healthcheck: Passed.
INFO vector::topology::builder: Healthcheck: Passed.Pe client (server web) — serverul 1
Pe serverul cu nginx, trebuie să dezactivați ipv6, deoarece în tabelul logs din clickhouse se folosește câmpul upstream_addr IPv4, deoarece nu folosesc ipv6 în rețea. Dacă ipv6 nu este dezactivat, vor apărea erori:
DB::Exception: Invalid IPv4 value.: (while read the value of key upstream_addr)Posibil cititorii, să adauge suport pentru ipv6.
Creăm fișierul /etc/sysctl.d/98-disable-ipv6.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1Aplicăm configurațiile
sysctl --systemVom instala nginx.
Am adăugat fișierul de repository nginx /etc/yum.repos.d/nginx.repo
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=trueVom instala pachetul nginx
yum install -y nginxPentru început, trebuie să configurăm formatul logurilor în Nginx în fișierul /etc/nginx/nginx.conf
user nginx;
# trebuie să setați procesele worker în funcție de nucleele CPU, nginx nu beneficiază de setarea mai multor procese decât acestea
worker_processes auto; # unele versiuni recente le calculează automat
# numărul descriptorilor de fișiere utilizați pentru nginx
# limita pentru FD-urile maxime pe server este de obicei setată de sistemul de operare.
# dacă nu setați FD-uri, atunci se vor utiliza setările sistemului de operare, care sunt în mod implicit 2000
worker_rlimit_nofile 100000;
error_log \/var\/log\/nginx\/error.log warn;
pid \/var\/run\/nginx.pid;
# oferă contextul fișierului de configurare în care sunt specificate directivele care afectează procesarea conexiunilor.
events {
# determină cât de mulți clienți vor fi serviți per worker
# max clienți = worker_connections * worker_processes
# max clienți este, de asemenea, limitat de numărul de conexiuni socket disponibile pe sistem (~64k)
worker_connections 4000;
# optimizat pentru a deservi mulți clienți cu fiecare fir, esențial pentru linux -- pentru mediu de testare
use epoll;
# acceptă cât mai multe conexiuni posibil, poate inunda conexiunile worker dacă este setat prea scăzut -- pentru mediu de testare
multi_accept on;
}
http {
include "/etc/nginx/mime.types";
default_type application\/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
log_format vector escape=json
'{'
'"node_name":"nginx-vector",'
'"timestamp":"$time_iso8601",'
'"server_name":"$server_name",'
'"request_full": "$request",'
'"request_user_agent":"$http_user_agent",'
'"request_http_host":"$http_host",'
'"request_uri":"$request_uri",'
'"request_scheme": "$scheme",'
'"request_method":"$request_method",'
'"request_length":"$request_length",'
'"request_time": "$request_time",'
'"request_referrer":"$http_referer",'
'"response_status": "$status",'
'"response_body_bytes_sent":"$body_bytes_sent",'
'"response_content_type":"$sent_http_content_type",'
'"remote_addr": "$remote_addr",'
'"remote_port": "$remote_port",'
'"remote_user": "$remote_user",'
'"upstream_addr": "$upstream_addr",'
'"upstream_bytes_received": "$upstream_bytes_received",'
'"upstream_bytes_sent": "$upstream_bytes_sent",'
'"upstream_cache_status":"$upstream_cache_status",'
'"upstream_connect_time":"$upstream_connect_time",'
'"upstream_header_time":"$upstream_header_time",'
'"upstream_response_length":"$upstream_response_length",'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_status": "$upstream_status",'
'"upstream_content_type":"$upstream_http_content_type"'
'}';
access_log "/var/log/nginx/access.log" main;
access_log "/var/log/nginx/access.json.log" vector; # Log nou în format json
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include "/etc/nginx/conf.d/*.conf";
}Pentru a nu afecta configurația dumneavoastră curentă, Nginx permite să aveți mai multe directive access_log
access_log "/var/log/nginx/access.log" main; # Log standard
access_log "/var/log/nginx/access.json.log" vector; # Log nou în format jsonNu uitați să adăugați o regulă în logrotate pentru noile loguri (dacă fișierul log nu se termină cu .log)
Ștergem default.conf din "/etc/nginx/conf.d/"
rm -f "/etc/nginx/conf.d/default.conf"Adăugăm un virtual host în "/etc/nginx/conf.d/vhost1.conf"
server {
listen 80;
server_name vhost1;
location \/ {
proxy_pass http:\/\/172.26.10.106:8080;
}
}Adăugăm un host virtual \/etc\/nginx\/conf.d\/vhost2.conf
server {
listen 80;
server_name vhost2;
location \/ {
proxy_pass http:\/\/172.26.10.108:8080;
}
}Adăugăm un host virtual \/etc\/nginx\/conf.d\/vhost3.conf
server {
listen 80;
server_name vhost3;
location \/ {
proxy_pass http:\/\/172.26.10.109:8080;
}
}Adăugăm un host virtual \/etc\/nginx\/conf.d\/vhost4.conf
server {
listen 80;
server_name vhost4;
location \/ {
proxy_pass http:\/\/172.26.10.116:8080;
}
}Adăugăm în fișierul \/etc\/hosts hosts virtuale (172.26.10.106 ip-ul serverului unde este instalat nginx) pe toate serverele:
172.26.10.106 vhost1
172.26.10.106 vhost2
172.26.10.106 vhost3
172.26.10.106 vhost4Și dacă totul este gata atunci
nginx -t
systemctl restart nginxAcum vom instala
yum install -y https:\/\/packages.timber.io\/vector\/0.9.X\/vector-x86_64.rpmVom crea fișierul de configurare pentru systemd \/etc\/systemd\/system\/vector.service
[Unit]
Description=Vector
After=network-online.target
Requires=network-online.target
[Service]
User=vector
Group=vector
ExecStart=\/usr\/bin\/vector
ExecReload=\/bin\/kill -HUP $MAINPID
Restart=no
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=vector
[Install]
WantedBy=multi-user.targetȘi vom configura înlocuirea Filebeat în configurația \/etc\/vector\/vector.toml. Adresa IP 172.26.10.108 reprezintă adresa IP a serverului de loguri (Vector-Server)
data_dir = "\/var\/lib\/vector"
[sources.nginx_file]
type = "file"
include = [ "\/var\/log\/nginx\/access.json.log" ]
start_at_beginning = false
fingerprinting.strategy = "device_and_inode"
[sinks.nginx_output_vector]
type = "vector"
inputs = [ "nginx_file" ]
address = "172.26.10.108:9876"Nu uitați să adăugați utilizatorul vector în grupul necesar pentru a putea citi fișierele de log. De exemplu, nginx în centos creează loguri cu permisiuni de grup adm.
usermod -a -G adm vectorSă pornim serviciul vector
systemctl enable vector
systemctl start vectorLogurile vector pot fi vizualizate astfel
journalctl -f -u vectorÎn loguri ar trebui să existe o astfel de înregistrare
INFO vector::topology::builder: Healthcheck: Passed.Testare de încărcare
Testarea se desfășoară folosind Apache benchmark.
Pe toate serverele a fost instalat pachetul httpd-tools
Pornim testarea folosind Apache benchmark de pe 4 servere diferite în screen. Mai întâi pornim multiplexorul de terminale screen, apoi începem testarea folosind Apache benchmark. Cum să lucrați cu screen puteți găsi în .
De pe serverul 1
while true; do ab -H "User-Agent: 1server" -c 100 -n 10 -t 10 http:\/\/vhost1\/. sleep 1; doneDe pe serverul 2
while true; do ab -H "User-Agent: 2server" -c 100 -n 10 -t 10 http:\/\/vhost2\/. sleep 1; doneDe pe serverul 3
while true; do ab -H "User-Agent: 3server" -c 100 -n 10 -t 10 http:\/\/vhost3\/. sleep 1; doneDe pe serverul 4
while true; do ab -H "User-Agent: 4server" -c 100 -n 10 -t 10 http:\/\/vhost4\/. sleep 1; doneVerificăm datele în Clickhouse
Accesăm Clickhouse
clickhouse-client -h 172.26.10.109 -mFacem o interogare SQL
SELECT * FROM vector.logs;
┌─node_name────┬───────────timestamp─┬─server_name─┬─user_id─┬─request_full───┬─request_user_agent─┬─request_http_host─┬─request_uri─┬─request_scheme─┬─request_method─┬─request_length─┬─request_time─┬─request_referrer─┬─response_status─┬─response_body_bytes_sent─┬─response_content_type─┬───remote_addr─┬─remote_port─┬─remote_user─┬─upstream_addr─┬─upstream_port─┬─upstream_bytes_received─┬─upstream_bytes_sent─┬─upstream_cache_status─┬─upstream_connect_time─┬─upstream_header_time─┬─upstream_response_length─┬─upstream_response_time─┬─upstream_status─┬─upstream_content_type─┐
│ nginx-vector │ 2020-08-07 04:32:42 │ vhost1 │ │ GET \/ HTTP\/1.0 │ 1server │ vhost1 │ \/ │ http │ GET │ 66 │ 0.028 │ │ 404 │ 27 │ │ 172.26.10.106 │ 45886 │ │ 172.26.10.106 │ 0 │ 109 │ 97 │ DISABLED │ 0 │ 0.025 │ 27 │ 0.029 │ 404 │ │
└──────────────┴─────────────────────┴─────────────┴─────────┴────────────────┴────────────────────┴───────────────────┴─────────────┴────────────────┴────────────────┴────────────────┴──────────────┴──────────────────┴─────────────────┴──────────────────────────┴───────────────────────┴───────────────┴─────────────┴─────────────┴───────────────┴───────────────┴─────────────────────────┴─────────────────────┴───────────────────────┴───────────────────────┴──────────────────────┴──────────────────────────┴────────────────────────┴─────────────────┴───────────────────────Să aflăm dimensiunea tabelelor în Clickhouse
select concat(database, '.', table) as table,
formatReadableSize(sum(bytes)) as size,
sum(rows) as rows,
max(modification_time) as latest_modification,
sum(bytes) as bytes_size,
any(engine) as engine,
formatReadableSize(sum(primary_key_bytes_in_memory)) as primary_keys_size
from system.parts
where active
group by database, table
order by bytes_size desc;Să aflăm cât spațiu au ocupat logurile în Clickhouse.

Dimensiunea tabelului logs este de 857.19 MB.

Dimensiunea acelorași date în indexul Elasticsearch ocupă 4,5GB.
Dacă în vector nu se specifică datele în parametrii din Clickhouse, acestea ocupă de 4500/857,19 = 5,24 ori mai puțin decât în Elasticsearch.
În vector, câmpul compression este utilizat implicit.
Chat Telegram pe
Chat Telegram pe
Chat Telegram pe ""
Sursa: habr.com
