Tag Archives: go

Моніторинг випадкової змінної за допомогою Telegraf -> InfluxDB -> Grafana

В цій публікації я розкажу про те як побудувати графік зміни якоїсь змінної в реальному часі. Наприклад якоїсь ціни, чи кількості запитів до сервера. Ключові слова: Docker, Docker compose, time series database, InfluxDB, Grafana, Telegraf. Всі крім докера будуть пояснені детально, докер – лише використовуватись для економії часу на інсталяцію.

В тренді зараз криптовалюти, тому давайте для прикладу будемо моніторити курс Litecoin до гривні. Для цього достатньо зробити GET запит https://api.coinmarketcap.com/v2/ticker/2/?convert=UAH. Для Bitcoin замініть id після /ticker/ з 2 на 1. (Документація з API). Він повертає JSON, формат якого розберемо трохи пізнішео. Бо нам ще треба встановити, налаштувати і запустити три програми для того щоб вони одна з одною працювали. Ну звісно в наш час це вручну ніхто не робить, тому ось вам готова конфігурація docker-compose.yml:

version: '3'
services: 
    influxdb:
      image: influxdb:latest
      container_name: influxdb
      ports:
        - "8086:8086"
      networks:
        - back-tier

    telegraf:
      image: telegraf:latest
      container_name: telegraf
      volumes:
        - ./telegraf.conf:/etc/telegraf/telegraf.conf:ro
      networks:
        - back-tier


    grafana:
      image: grafana/grafana:latest
      container_name: grafana
      ports:
        - "3000:3000"
      networks:
        - back-tier


networks:
  back-tier:

Записуєте його в директорію проекту, командуєте docker-compose up, і насолоджуєтесь логами всіх трьох сервісів. Правда вискочить помилка, бо конфігурація

volumes:
        - ./telegraf.conf:/etc/telegraf/telegraf.conf:ro

означає “покласти файл telegraf.conf з поточної директорії, в контейнер за шляхом /etc/telegraf/telegraf.conf”, а ми цей файл не написали. Для того треба спершу розібратись що таке Telegraf, чим він займатиметься, і як.

Telegraf, як пишуть на його сайті – це агент для збирання метрик і запису їх в InfluxDB, чи якісь інші можливі місця. Його файл конфігурації довгий, але важливі лише два місця:

[[outputs.influxdb]]
  # Конфігурація виведення даних в InfluxDB
  urls = ["http://influxdb:8086"] # HTTP інтерфейс InfluxDB. 
  ## Ім'я домену influxdb буде показувати на контейнер influxdb, тому що docker-compose так робить мережі

  ## База даних в яку писати метрики (telegraf її створить якщо буде потреба).
  database = "telegraf"

... 

[[inputs.http]]
  ## Брати дані http запитами
  urls = [ # звідки
    "https://api.coinmarketcap.com/v2/ticker/2/?convert=UAH"
  ]
  method = "GET" # методом GET
  data_format = "json" # розшифровувати як JSON

Якщо такий файл в нас є, то композ запустить всі три сервіси успішно, і Telegraf почне писати щось в InfluxDB. Пора подивитись що з того вийде. Щоб зайти в інтерфейс командного рядка Influxdb треба виконати команду

docker exec -it influxdb influx

А тоді:

> SHOW DATABASES
name: databases
name
----
_internal
telegraf
> use telegraf
Using database telegraf
> SHOW MEASUREMENTS
name: measurements
name
----
http

Бачим що Telegraf пише все в одну “таблицю” (measurement) – http. Але це насправді не страшно, бо в InfluxDB важливі не так measurements, як series – measurement з унікальним набором тегів (полів що індексуються). Крім них ще є fields (поля, які містять дані і не індексуються). Подивимось які в нас теги і поля (це майже те саме що схема таблиці в реляційних БД):

> SHOW FIELD KEYS FROM http 
name: http
fieldKey                           fieldType
--------                           ---------
data_circulating_supply            float
data_id                            float
data_last_updated                  float
data_max_supply                    float
data_quotes_UAH_market_cap         float
data_quotes_UAH_percent_change_1h  float
data_quotes_UAH_percent_change_24h float
data_quotes_UAH_percent_change_7d  float
data_quotes_UAH_price              float
data_quotes_UAH_volume_24h         float
data_quotes_USD_market_cap         float
data_quotes_USD_percent_change_1h  float
data_quotes_USD_percent_change_24h float
data_quotes_USD_percent_change_7d  float
data_quotes_USD_price              float
data_quotes_USD_volume_24h         float
data_rank                          float
data_total_supply                  float
metadata_timestamp                 float
> SHOW TAG KEYS FROM http
name: http
tagKey
------
host
url
> SHOW TAG VALUES FROM http WITH KEY IN ("host", "url")
name: http
key  value
---  -----
host 42bdec9c8910
url  https://api.coinmarketcap.com/v2/ticker/2/?convert=UAH

Бачимо що теги – це хост на якому запущений агент телеграфа що прислав дані (дивне в нього id, але це id контейнера). Не знаю чому не ім’я, думаю якось можна змінити, але це не дуже важливо якщо в нас один сервер з Telegraf. І адреса ресурсу який моніторить Telegraf. Тому можна бути спокійним з “таблиці” http можна буде вибрати окремі значення за тегом.

А от fields – дійсно багато. Яке з них – ціна Litecoin? Ну, для цього треба подивитись який JSON нам віддав coinmarketcap:

{
    "data": {
        "id": 2, 
        "name": "Litecoin", 
        "symbol": "LTC", 
        "website_slug": "litecoin", 
        "rank": 6, 
        "circulating_supply": 57387708.0, 
        "total_supply": 57387708.0, 
        "max_supply": 84000000.0, 
        "quotes": {
            "UAH": {
                "price": 2041.6651371095, 
                "volume_24h": 6550472764.2681465, 
                "market_cap": 117166483500.0, 
                "percent_change_1h": 0.08, 
                "percent_change_24h": 1.43, 
                "percent_change_7d": -6.39
            }, 
            "USD": {
                "price": 77.8638, 
                "volume_24h": 249818000.0, 
                "market_cap": 4468425048.0, 
                "percent_change_1h": 0.08, 
                "percent_change_24h": 1.43, 
                "percent_change_7d": -6.39
            }
        }, 
        "last_updated": 1531505650
    }, 
    "metadata": {
        "timestamp": 1531505337, 
        "error": null
    }
}

Ціна лежить в data.quotes.UAH.price, тому думаю нас цікавить поле data_quotes_UAH_price. Спробуємо запит:

> SELECT data_quotes_UAH_price FROM http WHERE time >= now() - 1h
name: http
time                data_quotes_UAH_price
----                ---------------------
1531595740000000000 2007.3576069685
1531595750000000000 2007.3576069685
1531595760000000000 2007.3576069685
1531595770000000000 2006.7466581361
1531595780000000000 2006.7466581361
1531595790000000000 2006.7466581361
1531595800000000000 2006.7466581361
1531595810000000000 2006.7466581361
...

О, це щось з чого можна будувати графік! І цим займеться Grafana.

Вона в нашій системі працює на порті 3000, тому заходимо на http://localhost:3000/ , входимо як USER: admin, PASSWORD: admin, змінюємо пароль, натискаємо “Create datasource”, заповнюємо форму для InfluxDB:

Заповнення джерела даних в Grafana

Внизу треба ще не забути вибрати базу даних “telegraf”, і натиснути “Save & test”. Якщо вискочило зелене повідомлення (а не червоне про помилку), то можна продовжувати.

Натискаємо плюсик -> Create -> Dashboard, додаємо панель “Graph”. У вкладці “Metrics” вибираємо датасорс InfluxDB і пишемо запит. Там є конструктор запитів, виглядає все так:

Побудова графіка за запитом

Але при бажанні можна справа натиснути кнопку меню, вибрати “Toggle edit mode”, і відредагувати запит як SQL:

SELECT mean("data_quotes_UAH_price") FROM "http" WHERE $timeFilter GROUP BY time($__interval) fill(null)

Бачимо що Grafanа вставляє в запит свої змінні, що дозволяє інтерактивно перебудовувати графік. Змінна $timeFilter містить щось на зразок now() - 1h залежно від того що користувач вибере в полі вгорі дашборда:

Вибір інтервалу часу

Ну як, почуваєтесь трошки фінансистами? Я ні, я аналіз даних в універі проспав :(, і взагалі мені це все для того аби рахувати запити до сервера.

А, ну і ввесь код, можна взяти на https://github.com/bunyk/docker-influxdb-grafana

Програмістські експерименти

За що я люблю програмування – так це за те що на відміну від, якої небудь історії, на будь-яке питання можна знайти відповідь в ході експерименту, без будь-яких “авторитетних джерел”.

– Чи копіюється map-а, при присвоєнні?
– Хвилину.

package main

import "fmt"

func main() {
fmt.Println("- Чи копіюється map-а, при присвоєнні?")
a := make(map[string]string)
a["answer"] = "Так"
b := a
b["answer"] = "Ні"
fmt.Println("-", a["answer"])
}

– Ні

Якщо звісно не запитання “нам обрати технологію А чи Б?” І звісно ресурсу на те щоб реалізувати рішення в обох і порівняти нема. Тоді й з’являються релігійні суперечки про те в кого мова потужніша.

Символи вікіпедії, детальніший аналіз

Щодо моєї попередньої публікації, мені зауважили що є різні простори імен. Тому сьогодні буде аналіз лише тих символів які можна зустріти в статтях (а не в категоріях, шаблонах, на сторінках користувачів і проектів), розбивка по різних алфавітах (може кому треба буде для частотного аналізу).

Загалом статті складаються з 3 613 435 448 символів, з яких різних 23 717. Вони розподілені так (намальовано лише початок розподілу, бо решта – неозрброєним оком не видно):

Ось таблиця найвживаніших 256, де спершу йде номер в рейтингу, потім символ, а останнє число – скільки раз він зустрічається. Якщо символ невидимий – то в дужках вказаний його номер в таблиці юнікоду:

1) пробіл (32) 442790853
2) а 160531292
3) о 157877564
4) н 138215454
5) і 117277229
6) р 105263631
7) и 98886082
8) е 88928818
9) т 87767363
10) | 80797095
11) в 78200503
12) с 75999527
13) ] 73604421
14) [ 73599732
15) л 70900301
16) к 69356359
17) новий рядок (10) 57091749
18) e 52089242
19) у 50198598
20) д 48912768
21) = 46361768
22) м 46138865
23) t 41512840
24) п 40914157
25) a 39627848
26) r 36179188
27) n 36087343
28) я 34788627
29) ь 33853333
30) . 32022232
31) s 31881844
32) i 30972299
33) з 30652689
34) o 30492305
35) 1 30006539
36) l 29367163
37) г 28746761
38) 0 27570049
39) й 26579527
40) - 24869609
41) ' 24751079
42) б 23634039
43) 2 21423701
44) , 21261521
45) / 20051917
46) c 19845819
47) p 19831300
48) ц 19183628
49) } 18895464
50) { 18889983
51) ч 18359390
52) d 17813691
53) b 16517162
54) 9 16240786
55) g 15262611
56) х 14936744
57) u 14902347
58) f 14303148
59) ї 13406838
60) h 13050086
61) m 12543340
62) < 12111655
63) : 12102999
64) > 12094377
65) ж 11923903
66) 3 10947193
67) 5 10647152
68) " 10484943
69) 4 10091884
70) w 9560852
71) К 9364738
72) П 9190333
73) 8 9177988
74) ю 9157643
75) ; 9037575
76) 6 8918805
77) С 8870091
78) ) 8867662
79) ( 8851064
80) ш 8821755
81) 7 8188158
82) ф 7340920
83) А 7166375
84) y 7039008
85) & 7006502
86) В 6869441
87) _ 6022842
88) М 5970386
89) є 5956858
90) k 5778567
91) Г 5745696
92) * 5712005
93) — 5519856
94) Л 4852833
95) S 4825612
96) Р 4712055
97) v 4646612
98) Н 4635296
99) Д 4580191
100) A 4422679
101) D 4409130
102) Т 4356396
103) щ 4143981
104) Б 4112548
105) У 4017973
106) R 3910061
107) Ф 3848824
108) I 3805943
109) « 3732952
110) » 3730320
111) C 3706725
112) О 3626277
113) E 3501507
114) P 3206479
115) І 3168901
116) F 3042662
117) З 2886284
118) L 2875664
119) T 2762480
120) % 2668723
121) B 2665154
122) M 2605927
123) N 2471967
124) G 2455406
125) # 2432361
126) x 2377037
127) ! 2307947
128) z 1683501
129) H 1652473
130) V 1627598
131) j 1580981
132) K 1542325
133) Ч 1534632
134) Х 1497880
135) U 1430170
136) Ш 1413847
137) O 1364521
138) Е 1326679
139) W 1071605
140) Ц 970737
141) – 873533
142) J 866094
143) ? 838918
144) Q 767843
145) Я 759389
146) Є 680158
147) X 636113
148) Y 617782
149) Ж 607830
150) ы 548375
151)   511929
152) Ю 503103
153) q 490449
154) + 467382
155) ́ 381075
156) \ 343889
157) 328026
158) Z 313673
159) ґ 293475
160) Й 286295
161) № 279189
162) é 263700
163) И 240639
164) Ґ 138039
165) ° 134968
166) ’ 129122
167) Щ 112586
168) ½ 92609
169) э 89526
170) … 88401
171) ó 87628
172) â 84646
173) ~ 75512
174) ü 67713
175) ^ 66068
176) ł 65770
177) Э 65262
178) ă 63282
179) á 62358
180) ъ 60515
181) ţ 60459
182) Ї 59899
183) • 58273
184) ² 57176
185) − 51293
186) × 51201
187) ё 50264
188) ä 49539
189) “ 47666
190) ö 47657
191) $ 46091
192) † 42959
193) ş 41864
194) è 41182
195) α 39774
196) í 38230
197) „ 34637
198) ο 34016
199) → 28048
200) ا 27473
201) · 26648
202) ‎ 26007
203) ν 24624
204) ń 24312
205) ι 24220
206) ę 24171
207) Ь 24064
208) ± 23585
209) ′ 23492
210) ” 23461
211) τ 23408
212) ș 23387
213) ς 23318
214) ρ 22519
215) š 21622
216) ś 21499
217) à 21334
218) ą 20463
219) ў 19269
220) ​ 18949
221) ε 18309
222) č 18232
223) @ 18014
224) É 17552
225) ç 17282
226) λ 17177
227) ل 16987
228) ³ 15756
229) σ 14667
230) κ 14317
231) ż 13948
232) η 13811
233) μ 12411
234) ر 12083
235) ي 11783
236) ā 11684
237) ñ 11460
238) م 11324
239) ć 11226
240) υ 11130
241) ن 11115
242) ß 10974
243) π 10708
244) ί 10461
245) ë 9850
246) ı 9824
247) ú 9761
248) ž 9598
249) ά 9572
250) ț 9284
251) γ 9152
252) ‘ 9122
253) ա 8975
254) ѣ 8947
255) و 8929
256) қ 8841

Ось вони ж, по групах:

Український алфавіт

2) а 160531292
3) о 157877564
4) н 138215454
5) і 117277229
6) р 105263631
7) и 98886082
8) е 88928818
9) т 87767363
11) в 78200503
12) с 75999527
15) л 70900301
16) к 69356359
19) у 50198598
20) д 48912768
22) м 46138865
24) п 40914157
28) я 34788627
29) ь 33853333
33) з 30652689
37) г 28746761
39) й 26579527
42) б 23634039
48) ц 19183628
51) ч 18359390
56) х 14936744
59) ї 13406838
65) ж 11923903
71) К 9364738
72) П 9190333
74) ю 9157643
77) С 8870091
80) ш 8821755
82) ф 7340920
83) А 7166375
86) В 6869441
88) М 5970386
89) є 5956858
91) Г 5745696
94) Л 4852833
96) Р 4712055
98) Н 4635296
99) Д 4580191
102) Т 4356396
103) щ 4143981
104) Б 4112548
105) У 4017973
107) Ф 3848824
112) О 3626277
115) І 3168901
117) З 2886284
133) Ч 1534632
134) Х 1497880
136) Ш 1413847
138) Е 1326679
140) Ц 970737
145) Я 759389
146) Є 680158
149) Ж 607830
152) Ю 503103
159) ґ 293475
160) Й 286295
163) И 240639
164) Ґ 138039
167) Щ 112586
182) Ї 59899
207) Ь 24064

Англійський алфавіт

18) e 52089242
23) t 41512840
25) a 39627848
26) r 36179188
27) n 36087343
31) s 31881844
32) i 30972299
34) o 30492305
36) l 29367163
46) c 19845819
47) p 19831300
52) d 17813691
53) b 16517162
55) g 15262611
57) u 14902347
58) f 14303148
60) h 13050086
61) m 12543340
70) w 9560852
84) y 7039008
90) k 5778567
95) S 4825612
97) v 4646612
100) A 4422679
101) D 4409130
106) R 3910061
108) I 3805943
111) C 3706725
113) E 3501507
114) P 3206479
116) F 3042662
118) L 2875664
119) T 2762480
121) B 2665154
122) M 2605927
123) N 2471967
124) G 2455406
126) x 2377037
128) z 1683501
129) H 1652473
130) V 1627598
131) j 1580981
132) K 1542325
135) U 1430170
137) O 1364521
139) W 1071605
142) J 866094
144) Q 767843
147) X 636113
148) Y 617782
153) q 490449
158) Z 313673

Цифри

35) 1 30006539
38) 0 27570049
43) 2 21423701
54) 9 16240786
66) 3 10947193
67) 5 10647152
69) 4 10091884
73) 8 9177988
76) 6 8918805
81) 7 8188158

Пунктуація

1) пробіл (32) 442790853
17) новий рядок (10) 57091749
10) | 80797095
13) ] 73604421
14) [ 73599732
21) = 46361768
30) . 32022232
40) – 24869609
41) ‘ 24751079
44) , 21261521
45) / 20051917
49) } 18895464
50) { 18889983
62) < 12111655
63) : 12102999
64) > 12094377
68) ” 10484943
75) ; 9037575
78) ) 8867662
79) ( 8851064
85) & 7006502
87) _ 6022842
92) * 5712005
93) — 5519856
109) « 3732952
110) » 3730320
120) % 2668723
125) # 2432361
127) ! 2307947
141) – 873533
143) ? 838918
151) нерозривний пробіл (160) 511929
154) + 467382
155) ́ 381075
156) \ 343889
157) TAB (9) 328026
161) № 279189
165) ° 134968
166) ’ 129122
168) ½ 92609
170) … 88401
173) ~ 75512
175) ^ 66068
183) • 58273
184) ² 57176
185) − 51293
186) × 51201
189) “ 47666
191) $ 46091
192) † 42959
197) „ 34637
199) → 28048
200) ا 27473
201) · 26648
202) символ зліва-направо 26007
208) ± 23585
209) ′ 23492
210) ” 23461
220) пробіл нульової довжини (8203) 18949
223) @ 18014
228) ³ 15756
252) ‘ 9122

Латиниця з наворотами

162) é 263700
171) ó 87628
172) â 84646
174) ü 67713
176) ł 65770
178) ă 63282
179) á 62358
181) ţ 60459
188) ä 49539
190) ö 47657
193) ş 41864
194) è 41182
196) í 38230
204) ń 24312
206) ę 24171
212) ș 23387
215) š 21622
216) ś 21499
217) à 21334
218) ą 20463
222) č 18232
224) É 17552
225) ç 17282
231) ż 13948
236) ā 11684
237) ñ 11460
239) ć 11226
242) ß 10974
245) ë 9850
246) ı 9824
247) ú 9761
248) ž 9598
250) ț 9284

Застарілі кириличні букви

150) ы 548375
169) э 89526
177) Э 65262
180) ъ 60515
187) ё 50264
219) ў 19269
254) ѣ 8947
256) қ 8841

Грецькі букви

195) α 39774
198) ο 34016
203) ν 24624
205) ι 24220
211) τ 23408
213) ς 23318
214) ρ 22519
221) ε 18309
226) λ 17177
229) σ 14667
230) κ 14317
232) η 13811
233) μ 12411
240) υ 11130
243) π 10708
244) ί 10461
249) ά 9572
251) γ 9152

Арабські

227) ل 16987
234) ر 12083
235) ي 11783
238) م 11324
241) ن 11115
255) و 8929

Вірменська

253) ա 8975

Парні символи

Минулого разу мені стало цікаво чому кількість дужок не співпадає. Може варто було б написати правильний алгоритм зі стеком, але я поки напевне спробую знайти статті де просто кількість дужок не співпадає аби подивитись в чому річ.

При цьому не є збалансованими майже всі дужки. Щодо круглих ясно, бо навіть в цій публікації в мене закриваючі масово переважають відкриваючі, але щодо квадратних і фігурних, які задають посилання і шаблони (трансклюзію сторінок). Якщо запустити програму яка стеком перевіряє парність:

То отримаємо нескінченний потік помилок у всіх статтях:

Швеція Закриваюча дужка ] без відкриваючої до неї біля "eritage Foundation]]]:\n* ВВП&nbsp;— $ 24"
Сумська область До кінця сторінки не закриті наступні дужки [[
Тернопільська область Закриваюча дужка ] без відкриваючої до неї біля "ьні вечори. Дебют]]»]] (Тернопіль), міжн"
Теліга Олена Іванівна Закриваюча дужка ] без відкриваючої до неї біля "нього шкільного віку] / [[Анна Багряна]]"
Західний Буг До кінця сторінки не закриті наступні дужки [
Андрухович Юрій Ігорович Закриваюча дужка ] без відкриваючої до неї біля ". Літ, 29 січня 2013]</ref> були наведен"
Данило Галицький Закриваюча дужка } без відкриваючої до неї біля "а Романівна]] ({Пом}} після [[1241]]), о"
29 Закриваюча дужка } без відкриваючої до неї біля "377a.htm}}{{ref-en}}}</ref>\n\n== Народили"

Переважкно люди в джерелах ставлять зайву закриваючу квадратну, або забувають відкрити квадратну, тому що якось дуже недбало ставляться до оформлення джерел. Але тих помилок так багато що я не знаю як їх всіх виправити. 😦 Може варто написати якийсь лінтер для вікіпедії, який би про ці помилки авторам повідомляв?

Скільки символів потрібно щоб написати вікіпедію?

Не так важливо скільки там людей говоритиме українською коли настане технологічна сингулярність. Важлива сумарна обчислювальна потужність інтелекту що володіє українською. 😉 І взагалі варто опановувати хоч якісь основи навчання машин – це професія в якій роботи ще не скоро замінять людей. Щоб навчити машину мови – їй треба багааатезно прикладів. І найбільший шмат української мови який можна легко згодувати машині – вікіпедія. Тому сьогодні ми спробуємо з’ясувати що потрібно щоб отримати цей масив тексту, і порахувати на ньому якусь простеньку статистику, для якої не треба бази даних а вистачить оперативної пам’яті.

Перше що нам потрібно – копія бази даних вікіпедії. Тому що вікіпедія містить більше чверті мільйона статтей, і навіть якщо ми робитимемо по запиту на секунду, що вікіпедія не схвалює для всяких там приватних павуків, то складання індексу займе в нас (750000 сек)/ 3600 / 24 = 8.68 діб > тижня. Тому заходимо на
https://dumps.wikimedia.org/ , вибираємо дамп який більше подобається, наприклад останній дамп що містить статті (без сторінок обговорень) української вікіпедії і ставимо на скачування.

А поки воно скачується підготуємось його розпаковувати. Ми скачуємо заархівований XML, який при розпаковуванні займає щось біля 5GB. Всередині є багато тисяч елементів , кожен з яких містить деталі про сторінку. Ось код на Go який містить функцію Read що розархівовує і водночас парсить XML, та повертає канал в який кидає сторінку за сторінкою, а в головній функції ітерується по всіх сторінках і підраховує кількість символів в їх тексті. В кінці виводить статистику:

Можна запускати його як go run main.go *.xml.bz2, а можна зробити go build і отримати скомпільований код, який вже запускати. Думаю за швидкістю вони співмірні.

Дізнаємось що українська вікіпедія станом на 3 березня (бо такий в мене дамп) складається з 4 138 529 133 символів, серед яких різних лише 24 190.

Різних символів не так вже й багато, для порівняння остання версія юнікоду має 136 755, але все одно забагато як на мене. Наприклад якщо захочеться скласти таблицю як часто який символ йде після даного (біграми), то в ній буде 585 156 100 клітинок. Це треба буде пару гігабайт оперативки аби порахувати лише диграми. Сторінок там приблизно 1 600 000 (а не 750 000), тому що крім статтей є ще шаблони, категорії, і тому подібні речі. Тому я думаю треба відкинути неважливі символи, наприклад взявши за поріг що символ має з’являтись у вікіпедії хоча б 16 000 разів (якщо менше, то шанс побачити його на випадково взятій сторінці менше 1%). А таких лише 229, і ось їхній номер в рейтингу, написання і кількість використань у вікіпедії:

229) 市: 17347
228) λ: 18004
227) ç: 18915
226) ε: 19037
225) č: 19328
224) ​: 19388
223) ñ: 19431
222) ل: 19786
221) ў: 20711
220) @: 21351
219) ą: 21381
218) É: 22425
217) ś: 22544
216) š: 22885
215) ′: 23578
214) ș: 23592
213) ρ: 23834
212) ±: 24008
211) ς: 24200
210) τ: 24465
209) ι: 25389
208) à: 25608
207) ę: 25616
206) ν: 25716
205) ń: 25991
204) ”: 29662
203) ا: 32288
202) ο: 35475
201) ‎: 36211
200) „: 38162
199) →: 38323
198) í: 40358
197) ş: 42223
196) α: 43669
195) †: 45094
194) Ь: 46660
193) è: 50851
192) ö: 50927
191) “: 51455
190) ä: 51800
189) −: 52985
188) $: 55963
187) ё: 55973
186) ²: 58338
185) ţ: 60505
184) ×: 61094
183) ă: 63880
182) ъ: 64358
181) á: 65593
180) ^: 68237
179) ł: 69450
178) Э: 69917
177) Ї: 72349
176) ü: 74505
175) â: 86866
174) ½: 92706
173) э: 97009
172) …: 98257
171) ~: 104201
170) ó: 113417
169) °: 138459
168) Щ: 144944
167) Ґ: 153541
166) ·: 170010
165) ’: 172548
164) é: 308459
163) №: 308648
162) И: 309865
161) Й: 333321
160) •: 349671
159) \: 352804
158) ґ: 364613
157) ́: 389166
156) Z: 408490
155) +: 522576
154)  : 555014
153) Ю: 573682
152) q: 574906
151) ы: 604142
150) : 630305
149) Ж: 690154
148) Y: 723229
147) X: 749583
146) Є: 765597
145) Q: 818419
144) Я: 930365
143) –: 942145
142) J: 961000
141) ?: 1046419
140) Ц: 1098377
139) W: 1185842
138) Е: 1540741
137) O: 1628778
136) Х: 1696070
135) Ш: 1724585
134) K: 1802117
133) V: 1805294
132) H: 1816527
131) Ч: 1855640
130) j: 1864115
129) z: 2061416
128) U: 2194056
127) G: 2744752
126) N: 2852071
125) M: 3061885
124) #: 3154223
123) L: 3181356
122) !: 3189165
121) B: 3369071
120) З: 3437745
119) x: 3452972
118) F: 3520991
117) T: 3583564
116) %: 3642707
115) І: 3655626
114) P: 3725526
113) E: 3972385
112) »: 3977385
111) «: 3980397
110) Ф: 4202078
109) I: 4388726
108) R: 4396989
107) У: 4451260
106) О: 4554868
105) Б: 4662681
104) C: 4700683
103) щ: 4747530
102) Т: 4828013
101) Р: 5285196
100) Д: 5297868
99) A: 5304565
98) Н: 5308271
97) D: 5412754
96) Л: 5463510
95) v: 5601059
94) S: 5771420
93) —: 5876110
92) Г: 6196594
91) *: 6308184
90) _: 6536077
89) М: 6714179
88) є: 6725025
87) k: 6994569
86) &: 7431439
85) А: 8011695
84) В: 8034456
83) ф: 8301528
82) y: 8544168
81) ;: 9651561
80) С: 9778231
79) 7: 9836255
78) ш: 10032005
77) ю: 10424706
76) П: 10504497
75) w: 10609713
74) (: 10822363
73) ): 10861049
72) 6: 10922925
71) К: 11311101
70) 8: 11392131
69) “: 11480435
68) 4: 11994852
67) 5: 12855281
66) 3: 13149018
65) ж: 13461373
64) >: 14339625
63) <: 14353972
62) ї: 14915551
61) h: 15043805
60) m: 15056183
59) х: 16421470
58) f: 16484180
57) u: 17314042
56) g: 17753361
55) b: 18992562
54) 9: 19917121
53) :: 20761022
52) ц: 21063193
51) ч: 21089655
50) d: 21132407
49) p: 22184976
48) /: 22493164
47) c: 22688474
46) {: 23253440
45) }: 23256779
44) ,: 24259060
43) 2: 25895208
42) б: 27028220
41) ': 27719818
40) -: 29433960
39) й: 29504263
38) г: 32647216
37) з: 34131397
36) 0: 34211416
35) l: 35713324
34) .: 36224151
33) s: 36570492
32) o: 36864174
31) 1: 37256409
30) ь: 37873838
29) i: 37914159
28) я: 40403600
27) n: 41935115
26) r: 43452098
25) п: 45627724
24) a: 47339118
23) t: 49644507
22) м: 51192263
21) =: 52585455
20) д: 54742090
19) у: 56282864
18) e: 60511715
17)
: 68855138
16) к: 77929326
15) л: 78327225
14) [: 81918527
13) ]: 81923177
12) с: 85389668
11) в: 88260931
10) |: 90345542
9) т: 99966314
8) е: 100782938
7) и: 110643874
6) р: 118651169
5) і: 130028472
4) н: 154575149
3) о: 178231842
2) а: 180553597
1) : 506900824

Цих 229 символів використовуються сумарно 4 136 342 085 разів, а це складає 99.95% всіх символів вікіпедії. Тому рештою думаю дійсно можна знехтувати.

Простеньке Go API з JWT авторизацією

Щоб щось зрозуміти іноді легше писати ніж читати (В мене була потреба зрозуміти як працює middleware jwt авторизації, і я цей код читати не міг. Довелось для розминки написати аналогічне, трохи помогло).

Якщо вам цікаво що таке JWT і для чого, то в двох словах – це можливість видати комусь право доступу до чогось без бази даних де б писало що ми йому таке право давали. Тобто сервер на якому користувач авторизується, і сервер до якого він отримує доступ – це можуть бути два абсолютно окремі сервери, які не те що не мають спільної бази даних, вони навіть не спілкуються мережею. Головне – правильні ключі.

Напишемо наступне супер просте API:
POST /login {user: “”, password: “”} – віддає нам JWT токен для дозволу запису
GET / – віддає нам список записів
POST / – з заголовком “Authorization: Bearer ” дозволяє додати новий запис до списку, якщо ми авторизовані.

Для початку зробимо все без авторизації:

package main
 
import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
)
 
func main() {
    initDB()
     
    http.HandleFunc("/", handler)  // За головну сторінку відповідатиме функція handler
    fmt.Println("Listening at 8080")
    http.ListenAndServe(":8080", nil) // Запускаємо сервер на якомусь порті
}
 
// "база даних"
var log []string
 
// Заповнюємо "базу" якимись даними
func initDB() {
    log = make([]string, 0)
    log = append(log, "Hello")
    log = append(log, "World")
}
 
func handler(w http.ResponseWriter, r *http.Request) {
    if r.Method == "GET" {
        data, err := json.Marshal(log) // серіалізуємо базу в JSON
        if errorHandler(w, err, http.StatusInternalServerError) { // якщо була помилка
            return
        }
        w.Write(data) 
    } else if r.Method == "POST" {
        bodybytes, err := ioutil.ReadAll(r.Body) // Читаємо все що передано
        if errorHandler(w, err, http.StatusInternalServerError) {
            return
        }
        log = append(log, string(bodybytes)) // і додаємо як текст в базу даних
        w.Write(bodybytes) // і повертаємо що додали
    } else { // невідомий метод
		errorHandler(w, fmt.Errorf("Method not allowed: %s", r.Method), http.StatusMethodNotAllowed)
    }
}
 
// Функція яка якщо отримує помилку пише у відповідь текст помилки з кодом, і повертає true,
// а якщо не було - просто повертає false.
func errorHandler(w http.ResponseWriter, err error, code int) bool {
    if err == nil {
        return false
    }
    fmt.Println(err)
    msg, _ := json.Marshal(map[string]string{
        "error": err.Error(),
    })
    w.Write(msg)
    return true
}

Запишемо це в файл наприклад main.go і запустимо go run main.go. Тепер можна перевірити все командами:

curl http://localhost:8080/ # Дає ["Hello","World"]
curl -X POST http://localhost:8080/ -d "it works!" # Дає it works!
curl http://localhost:8080/ # Дає ["Hello","World","it works!"]

Якщо працює – пора генерувати ключі. Бо ми ж не хочемо щоб будь-хто міг писати в нашу “базу даних”. Ключі в асиметричній криптографії завжди генеруються парами, один публічний, один приватний.

openssl genrsa -out key.rsa 1024
openssl rsa -in key.rsa -pubout > key.rsa.pub

Перевірте щоб файл key.rsa починався з рядка “—–BEGIN RSA PRIVATE KEY—–“, а key.rsa.pub – з “—–BEGIN PUBLIC KEY—–“. І не переплутайте, а то логін не буде безпечним і взагалі довго дебажити доведеться.

Приватний ключ використовується для підписування документа, тобто документ зашифровується цим ключем і додається до відкритого як підпис. Документом буде JSON Web Token, і він буде містити зрозумілі комп’ютеру твердження на зразок “власник цього токена має право записувати дані на сервер до такого-то числа, підписано сервером”. Токен буде видаватись якщо ми передамо правильні логін і пароль на “/login”. Можна вважати приватний ключ печаткою, а публічний – зразком печатки для порівняння.

Щоб підписувати токени ключами нам знадобиться бібліотека “jwt-go”. Встановлюється командою go get github.com/dgrijalva/jwt-go. Тепер нам треба трохи модифікувати функцію main() і додати бібліотек:

import ( // крім вищенаписаного додати ще ці, знадобиться:
	"crypto/rsa"
	"github.com/dgrijalva/jwt-go"
	"time"
)

func main() {
	initDB()
	err := loadKeys() // Завантаження ключів
	if err != nil {
		fmt.Println(err)
		return
	}
	
	http.HandleFunc("/login", loginHandler) // тут будемо видавати токен
	http.HandleFunc("/", handler) // а тут будемо іноді перевіряти

	fmt.Println("Listening at 8080")
	http.ListenAndServe(":8080", nil)
}

// В цих змінних зберігатимемо ключі
var PublicKey *rsa.PublicKey
var PrivateKey *rsa.PrivateKey

// Ця функція просто прочитає і розпарсить ключі у змінні вище, нічого цікавого
func loadKeys() error {
	pk, err := ioutil.ReadFile("./key.rsa")
	if err != nil {
		return err
	}
	PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM(pk)
	if err != nil {
		return err
	}

	pk, err = ioutil.ReadFile("./key.rsa.pub")
	if err != nil {
		return err
	}
	PublicKey, err = jwt.ParseRSAPublicKeyFromPEM(pk)
	return err
}

// Структура в яку ми розпакуємо запит до /login
type UserCredentials struct {
	Login    string `json:"login"`
	Password string `json:"password"`
}

// Якщо отримує запит з правильними логіном і паролем повертає нам
// підписаний токен для доступу до запису в "БД"
func loginHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		errorHandler(w, fmt.Errorf("Method not allowed: %s", r.Method), http.StatusMethodNotAllowed)
		return
	}
	var user UserCredentials
	var err error
	bodybytes, err := ioutil.ReadAll(r.Body)
	err = json.Unmarshal(bodybytes, &user)
	if errorHandler(w, err, http.StatusUnprocessableEntity) {
		return
	}
	if (user.Login != "LOGIN") || (user.Password != "PASSWORD") {
		errorHandler(w, fmt.Errorf("Bad credentials"), http.StatusForbidden)
		return
	}

	// якщо всі перевірки пройдено - згенерувати токен
	tokenString, err := getJWT()
	if errorHandler(w, err, http.StatusInternalServerError) {
		return
	}
	msg, _ := json.Marshal(map[string]string{
		"token": tokenString,
	})
	w.Write(msg)
}

// Функція що повертає нам текст токена, підписаний приватним ключем
func getJWT() (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
		// тут записуємо дозвіл на post (взагалі, можна що завгодно записувати)
		"allow": "post",
		// пишемо що токен дійсний пів години
		"exp": time.Now().Add(time.Minute * 30).Unix(),
	})
	return token.SignedString(PrivateKey)
}

Пробуємо дістати токен:

curl -X POST http://localhost:8080/login -d '{"login": "LOGIN", "password": "PASSWORD"}'
# Дає {"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbGxvdyI6InBvc3QiLCJleHAiOjE1MTI5NDE0NjF9.nKZOXO-sTEsmzVRFI3dfSl0MbO9v-fYBRZkS-GzpmlGgwho5NgxUASax0VnZX4v0mLKlI0Jt2bncDn4jZA1TZsmMTCkAetPslcrkWhIt5XaLASmnyzm_-VTCSoSSDtFydpr3pAceEfg41tuBeukhokh-focDGDSZLQTA4_MeY00"}
curl -X POST http://localhost:8080/login -d '{"login": "LOGIN", "password": "WRONG"}' 
# Дає {"error":"Bad credentials"}

Бачимо що токен – це три base64 стрічки записані через крапку. Є веб-сервіс що дозволяє розкодувати і подивитись що там. Перша – заголовок, описує формат токена:

{
  "alg": "RS256",
  "typ": "JWT"
}

Тіло – описує власне якісь твердження, в нашому випадку те що ми дозволяємо деякий час робити POST запити:

{
  "allow": "post",
  "exp": 1512941461
}

Тепер треба оновити основний handler, аби він перевіряв наявність і правильність токена.

func handler(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		data, err := json.Marshal(log)
		if errorHandler(w, err, http.StatusInternalServerError) {
			return
		}
		w.Write(data)
	} else if r.Method == "POST" {
		// Цього разу перед тим як робити POST
		allow, err := isPostAllowed(r)  // перевіряємо чи можна
		if errorHandler(w, err, http.StatusInternalServerError) {
			return
		}
		if !allow { // І якщо не можна - повертаємо помилку
			errorHandler(w, fmt.Errorf("Access denied"), http.StatusForbidden)
		}
		bodybytes, err := ioutil.ReadAll(r.Body)
		if errorHandler(w, err, http.StatusInternalServerError) {
			return
		}
		log = append(log, string(bodybytes))
		w.Write(bodybytes)
	} else {
		errorHandler(w, fmt.Errorf("Method not allowed: %s", r.Method), http.StatusMethodNotAllowed)
	}
}

// Перевірка дозволів
func isPostAllowed(r *http.Request) (bool, error) {
	bearer := r.Header.Get("Authorization") // Отримуємо заголовок Authorization
	prefixLen := len("Bearer ")
	if len(bearer) <= prefixLen {
		return false, fmt.Errorf("Authorization header is too short")
	}
	// Пробуємо парсити токен. Два аргументи - токен і функція що повертає потрібний ключ
	token, err := jwt.Parse(bearer[prefixLen:], func(token *jwt.Token) (interface{}, error) {
		return PublicKey, nil
	})
	if err != nil {
		return false, err
	}
	// Якщо вийшло розпарсити, токен валідний, то дістаємо твердження
	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		allow, ok := claims["allow"].(string) // дивимось чи написано щось про "allow"
		if ok && strings.Contains(allow, "post") { // і чи є там "post"
			return true, nil
		}
		return false, fmt.Errorf("Token does not have claim to allow this action")
	} else {
		return false, fmt.Errorf("Token is invalid")
	}
	return true, nil
}

Тепер попробуємо щось запостити без токена і з ним:

curl -X POST http://localhost:8080/ -d "it works!"
# Дає {"error":"Authorization header is too short"}
curl -X POST http://localhost:8080/ -d "it works!" -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbGxvdyI6InBvc3QiLCJleHAiOjE1MTI5NDE0NjF9.nKZOXO-sTEsmzVRFI3dfSl0MbO9v-fYBRZkS-GzpmlGgwho5NgxUASax0VnZX4v0mLKlI0Jt2bncDn4jZA1TZsmMTCkAetPslcrkWhIt5XaLASmnyzm_-VTCSoSSDtFydpr3pAceEfg41tuBeukhokh-focDGDSZLQTA4_MeY00"
# Дає it works

Працює! Отож, бачимо що JWT це дуже навіть просто, якщо не рахувати заморочок з дивними способами виклику функцій бібліотеки – виписуємо довідку що щось можна ставимо на неї печатку, а в іншому місці це перевіряємо.


Filed under: Кодерство, Павутина Tagged: go