diff --git a/CHANGELOG.md b/CHANGELOG.md index 3143d79..2f71b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [v1.1.0] - 2026-02-17 + +### Added +- Both English and Russian monitoring point names in metrics (as `mp_name` and `mp_name_ru` labels) and tasks JSON logs (as `MpName` and `MPNameRu` fields) + +### Removed +- All translation logic (`translator` module, `locations.json` file, and `ENG_MP_NAMES` ENV variable) + ## [v1.0.0] - 2025-12-03 ### Added diff --git a/README.md b/README.md index 330f67e..8739e4f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ style="text-align: justify"> - 🔄 **Automatic Metrics Collection**: Periodically fetches metrics from Ping-Admin API for multiple tasks - 📊 **Prometheus Integration**: Exposes metrics in standard Prometheus format at `/metrics` - 📈 **JSON Stats API**: Provides additional JSON endpoints for task statistics -- 🌍 **Location Translation**: Supports translation of location names via `locations.json` - 🚀 **Concurrent Processing**: Efficiently processes multiple tasks in parallel - 🔁 **Automatic Cleanup**: Removes stale metrics when monitoring points are no longer available - 🐳 **Docker Support**: Ready-to-use Docker image @@ -79,7 +78,6 @@ The exporter can be configured via command-line flags or environment variables. | `--listen-address` | `LISTEN_ADDRESS` | HTTP server listen address | `:8080` | | `--log-level` | `LOG_LEVEL` | Log level (debug, info, warn, error) | `info` | | `--locations-file` | `LOCATIONS_FILE` | Path to locations.json file | `locations.json` | -| `--eng-mp-names` | `ENG_MP_NAMES` | Translate MP names to English | `true` | | `--refresh-interval` | `REFRESH_INTERVAL` | Metrics refresh interval | `3m` | | `--api-update-delay` | `API_UPDATE_DELAY` | Ping-Admin API data update delay | `4m` | | `--api-data-time-step` | `API_DATA_TIME_STEP` | Time between API data points | `3m` | @@ -147,7 +145,7 @@ The exporter exposes the following Prometheus metrics: ### Monitoring Point Metrics -All MP metrics include labels: `task_id`, `task_name`, `mp_id`, `mp_name`, `mp_ip`, `mp_gps` +All MP metrics include labels: `task_id`, `task_name`, `mp_id`, `mp_name`, `mp_name_ru`,`mp_ip`, `mp_gps` - `apatit_mp_status` - Status of monitoring point (1 = up, 0 = down/stale) - `apatit_mp_data_status` - Status of the data for the monitoring point (1 = has data, 0 = no data) @@ -175,13 +173,11 @@ apatit/ │ ├── log/ # Logging setup │ ├── scheduler/ # Metrics and stats schedulers │ ├── server/ # HTTP server -│ ├── translator/ # Location name translation │ ├── utils/ # Utility functions │ └── version/ # Version information ├── deploy/ │ └── docker-compose.yaml # Docker Compose configuration ├── Dockerfile # Container image definition -├── locations.json # Location translation mappings └── go.mod # Go module definition ``` diff --git a/cmd/apatit/main.go b/cmd/apatit/main.go index 0b5ddfd..4fc93f5 100644 --- a/cmd/apatit/main.go +++ b/cmd/apatit/main.go @@ -15,7 +15,6 @@ import ( "apatit/internal/log" "apatit/internal/scheduler" "apatit/internal/server" - "apatit/internal/translator" ) // createExporters creates and returns a list of exporters for the specified tasks. @@ -42,7 +41,6 @@ func createExporters(apiClient *client.Client, cfg *config.Config) ([]*exporter. expConfig := &exporter.Config{ TaskID: taskID, - EngMPNames: cfg.EngMPNames, ApiUpdateDelay: cfg.ApiUpdateDelay, ApiDataTimeStep: cfg.ApiDataTimeStep, } @@ -92,11 +90,6 @@ func newApp(cfg *config.Config) (*application, error) { // Set logger for components log.Init(cfg.LogLevel) - // Set translator - if err := translator.Init(cfg.LocationsFilePath); err != nil { - logrus.Warnf("Failed to initialize translator, location names will not be translated: %v", err) - } - // Create API client apiClient := client.New(cfg.APIKey, nil, cfg.RequestDelay, cfg.RequestRetries, cfg.MaxRequestsPerSecond) diff --git a/deploy/grafana/monitoring-dashboard.json b/deploy/grafana/monitoring-dashboard.json index 14fedf7..3343c32 100644 --- a/deploy/grafana/monitoring-dashboard.json +++ b/deploy/grafana/monitoring-dashboard.json @@ -2170,7 +2170,7 @@ "showLegend": false }, "mergeValues": true, - "perPage": 20, + "perPage": 10, "rowHeight": 0.9, "showValue": "never", "tooltip": { diff --git a/internal/client/models.go b/internal/client/models.go index 0c33d38..cbb34a3 100644 --- a/internal/client/models.go +++ b/internal/client/models.go @@ -14,9 +14,10 @@ import ( // It contains TM (tochka monitoringa) info with TmID; TmName and TmRes (results with time, speed). // P.S. TM will be used as MP (Monitoring Point) after processing. type EntryRaw struct { - TmID string `json:"tm_id"` - TmName string `json:"tm_name"` - TmRes []*TmResRaw `json:"tm_res"` + TmID string `json:"tm_id"` + TmName string `json:"tm_name"` + TmNameEn string `json:"tm_name_en"` + TmRes []*TmResRaw `json:"tm_res"` } // TmResRaw @@ -90,6 +91,8 @@ type MonitoringPointRaw struct { ID string `json:"id"` // Monitoring Point Name Name string `json:"name"` + // Monitoring Point NameEn + NameEn string `json:"name_en"` // Monitoring Point IP IP string `json:"ip"` // Monitoring Point GPS @@ -116,6 +119,7 @@ type TasksLogsRaw struct { Descr *string `json:"descr"` Status *int `json:"status"` Tm *string `json:"tm"` + TmEn *string `json:"tm_en"` TmID *string `json:"tm_id"` Traceroute *string `json:"traceroute"` } @@ -140,6 +144,7 @@ type TaskInfo struct { type MonitoringPointInfo struct { ID string Name string + NameEn string IP string GPS string Status int64 @@ -150,6 +155,7 @@ type MonitoringPointInfo struct { type MonitoringPointEntry struct { ID string Name string + NameEn string Status int Result []*MonitoringPointConnectionResult } @@ -180,6 +186,7 @@ type TaskLog struct { Data string Description string Status int64 + MPNameRu string MPName string MPID string Traceroute string @@ -191,6 +198,7 @@ func (mp *MonitoringPointRaw) ProcessMonitoringPointInfo() *MonitoringPointInfo return &MonitoringPointInfo{ ID: mp.ID, Name: mp.Name, + NameEn: mp.NameEn, IP: mp.IP, GPS: mp.GPS, Status: parseInt(&mp.Status, "status"), @@ -218,6 +226,7 @@ func (e *EntryRaw) ProcessMonitoringPointEntry() *MonitoringPointEntry { entry := &MonitoringPointEntry{ ID: e.TmID, Name: e.TmName, + NameEn: e.TmNameEn, Result: make([]*MonitoringPointConnectionResult, 0, len(e.TmRes)), } @@ -247,7 +256,8 @@ func (t *TaskStatRaw) ProcessTaskEntry() *TaskStatEntry { Data: *resRaw.Data, Description: *resRaw.Descr, Status: int64(*resRaw.Status), - MPName: *resRaw.Tm, + MPName: *resRaw.TmEn, + MPNameRu: *resRaw.Tm, MPID: *resRaw.TmID, Traceroute: *resRaw.Traceroute, } @@ -283,79 +293,3 @@ func parseInt(s *string, fieldName string) int64 { } return i } - -//// Transpose -//// TransposedTaskLogs -//// is just a transposed TaskLog structure. -//type TransposedTaskLogs struct { -// Data []string `json:"Data"` -// Description []string `json:"Description"` -// Status []int64 `json:"Status"` -// MPName []string `json:"MPName"` -// MPID []string `json:"MPID"` -// Traceroute []string `json:"Traceroute"` -//} -// -//// TransposedTaskStatEntry -//// is a transposed 'TaskStatEntry'. -//type TransposedTaskStatEntry struct { -// TaskID string `json:"TaskID"` -// TaskName string `json:"TaskName"` -// TaskLogs TransposedTaskLogs `json:"TaskLogs"` -//} -// -//// transposes TaskLogs. -//func (entry *TaskStatEntry) Transpose() *TransposedTaskStatEntry { -// logCount := len(entry.TaskLogs) -// -// data := make([]string, 0, logCount) -// description := make([]string, 0, logCount) -// status := make([]int64, 0, logCount) -// mpName := make([]string, 0, logCount) -// mpID := make([]string, 0, logCount) -// traceroute := make([]string, 0, logCount) -// -// for _, log := range entry.TaskLogs { -// data = append(data, log.Data) -// description = append(description, log.Description) -// status = append(status, log.Status) -// mpName = append(mpName, log.MPName) -// mpID = append(mpID, log.MPID) -// traceroute = append(traceroute, log.Traceroute) -// } -// -// transposedEntry := &TransposedTaskStatEntry{ -// TaskID: entry.TaskID, -// TaskName: entry.TaskName, -// TaskLogs: TransposedTaskLogs{ -// Data: data, -// Description: description, -// Status: status, -// MPName: mpName, -// MPID: mpID, -// Traceroute: traceroute, -// }, -// } -// -// return transposedEntry -//} -// -//// --- Formatting Helpers --- -// -//// FormatMonitoringPointSliceToMap gets MonitoringPoint slice and returns map[ID]MonitoringPoint -//func FormatMonitoringPointSliceToMap(items []*MonitoringPointRaw) map[string]*MonitoringPointRaw { -// result := make(map[string]*MonitoringPointRaw, len(items)) -// for _, value := range items { -// result[value.ID] = value -// } -// return result -//} -// -//// FormatTaskSliceToMap gets TaskRaw slice and returns map[TaskID]Task -//func FormatTaskSliceToMap(items []*TaskRaw) map[string]*TaskRaw { -// result := make(map[string]*TaskRaw, len(items)) -// for _, value := range items { -// result[strconv.Itoa(value.ID)] = value -// } -// return result -//} diff --git a/internal/config/config.go b/internal/config/config.go index ee42af3..ee1861e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,7 +13,6 @@ import ( type Config struct { APIKey string TaskIDs []int - EngMPNames bool ApiUpdateDelay time.Duration ApiDataTimeStep time.Duration RefreshInterval time.Duration @@ -32,7 +31,6 @@ func New() (*Config, error) { flag.StringVar(&cfg.APIKey, "api-key", envString("API_KEY", ""), "API key for Ping-Admin") taskIDsStr := flag.String("task-ids", envString("TASK_IDS", ""), "Comma-separated list of task IDs") - flag.BoolVar(&cfg.EngMPNames, "eng-mp-names", envBool("ENG_MP_NAMES", true), "Translate monitoring points (MP) names to English") flag.DurationVar(&cfg.ApiUpdateDelay, "api-update-delay", envDuration("API_UPDATE_DELAY", 4*time.Minute), "Fixed Ping-Admin API delay for new data update") flag.DurationVar(&cfg.ApiDataTimeStep, "api-data-time-step", envDuration("API_DATA_TIME_STEP", 3*time.Minute), "Fixed Ping-Admin API time between data points") flag.DurationVar(&cfg.RefreshInterval, "refresh-interval", envDuration("REFRESH_INTERVAL", 3*time.Minute), "Exporter's refresh interval") @@ -102,16 +100,6 @@ func envDuration(key string, def time.Duration) time.Duration { return def } -// envBool bool env variables helper. -func envBool(key string, def bool) bool { - if v := os.Getenv(key); v != "" { - if b, err := strconv.ParseBool(v); err == nil { - return b - } - } - return def -} - // envInt int env variables helper. func envInt(env string, def int) int { if v, ok := os.LookupEnv(env); ok { diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go index a6ca2b9..8b3ab54 100644 --- a/internal/exporter/exporter.go +++ b/internal/exporter/exporter.go @@ -11,7 +11,6 @@ import ( "github.com/sirupsen/logrus" "apatit/internal/client" - "apatit/internal/translator" ) // Exporter collects metrics for a single task. @@ -159,9 +158,9 @@ func (e *Exporter) RefreshMetrics() ([]prometheus.Labels, error) { if item.Status > 1 || item.Status < 0 { e.log.WithFields( logrus.Fields{ - "mp_id": item.ID, + "mp_id": item.ID, "mp_name": item.Name, - "status": item.Status, + "status": item.Status, }).Errorf("incorrect monitoring points status: %d", item.Status) item.Status = 0 } @@ -183,28 +182,24 @@ func (e *Exporter) processTaskStatResults(taskStatResults *client.TaskStatEntry) for _, entry := range taskStatResults.TaskLogs { entry.Traceroute = strings.ReplaceAll(entry.Traceroute, "\\n", "\n") - entry.MPName = translator.GetEngLocation(entry.MPName) } } // processTaskStatGraphResultItem processes one record (monitoring point) and updates metrics. func (e *Exporter) processTaskStatGraphResultItem(item *client.MonitoringPointEntry, refreshStartTime time.Time) []prometheus.Labels { if len(item.Result) == 0 { - locationName := item.Name - if e.Config.EngMPNames { - locationName = translator.GetEngLocation(item.Name) - } MPDataStatus.WithLabelValues( - strconv.Itoa(e.taskInfo.ID), + strconv.Itoa(e.taskInfo.ID), e.taskInfo.ServiceName, item.ID, - locationName, - ).Set(0) + item.NameEn, + item.Name, + ).Set(0) e.log.WithFields( logrus.Fields{ - "mp_id": item.ID, - "mp_name": item.Name}).Warn("No results found for MP") + "mp_id": item.ID, + "mp_name": item.NameEn}).Warn("No results found for MP") return nil } @@ -217,22 +212,18 @@ func (e *Exporter) processTaskStatGraphResultItem(item *client.MonitoringPointEn } MPDataStatus.WithLabelValues( - strconv.Itoa(e.taskInfo.ID), + strconv.Itoa(e.taskInfo.ID), e.taskInfo.ServiceName, item.ID, processedLabels[0][LabelMPName], - ).Set(1) + processedLabels[0][LabelMPNameRu], + ).Set(1) return processedLabels } // buildLabels creates a set of Prometheus labels for a monitoring point. func (e *Exporter) buildLabels(item *client.MonitoringPointEntry) prometheus.Labels { - locationName := item.Name - if e.Config.EngMPNames { - locationName = translator.GetEngLocation(item.Name) - } - ipAddress := "unknown" gpsCoordinates := "unknown" for _, mp := range e.monitoringPoints { @@ -247,7 +238,8 @@ func (e *Exporter) buildLabels(item *client.MonitoringPointEntry) prometheus.Lab LabelTaskID: strconv.Itoa(e.taskInfo.ID), LabelTaskName: e.taskInfo.ServiceName, LabelMPID: item.ID, - LabelMPName: locationName, + LabelMPName: item.NameEn, + LabelMPNameRu: item.Name, LabelMPIP: ipAddress, LabelMPGPS: gpsCoordinates, } diff --git a/internal/exporter/labels.go b/internal/exporter/labels.go index 3b07928..8d4f9d7 100644 --- a/internal/exporter/labels.go +++ b/internal/exporter/labels.go @@ -11,6 +11,7 @@ const ( LabelTaskName = "task_name" LabelMPID = "mp_id" LabelMPName = "mp_name" + LabelMPNameRu = "mp_name_ru" LabelMPIP = "mp_ip" LabelMPGPS = "mp_gps" ) diff --git a/internal/exporter/metrics.go b/internal/exporter/metrics.go index 796bfd5..41cee5e 100644 --- a/internal/exporter/metrics.go +++ b/internal/exporter/metrics.go @@ -19,6 +19,7 @@ var ( LabelTaskName, LabelMPID, LabelMPName, + LabelMPNameRu, LabelMPIP, LabelMPGPS, } @@ -109,7 +110,7 @@ var ( Name: "data_status", Help: "Status of the data for the monitoring point (1 = has data, 0 = no data).", }, - []string{LabelTaskID, LabelTaskName, LabelMPID, LabelMPName}, + []string{LabelTaskID, LabelTaskName, LabelMPID, LabelMPName, LabelMPNameRu}, ) MPConnectSeconds = prometheus.NewGaugeVec( diff --git a/internal/scheduler/stats.go b/internal/scheduler/stats.go index 2c2c6ac..4ace0bc 100644 --- a/internal/scheduler/stats.go +++ b/internal/scheduler/stats.go @@ -14,7 +14,7 @@ import ( "apatit/internal/utils" ) -// runStatsScheduler starts a loop that periodically updates task stats and publish them +// RunStatsScheduler starts a loop that periodically updates task stats and publish them func RunStatsScheduler(exporters []*exporter.Exporter, cfg *config.Config, stop <-chan struct{}) { statsLog := logrus.WithField("component", "stats_scheduler") @@ -74,12 +74,6 @@ func RunStatsScheduler(exporters []*exporter.Exporter, cfg *config.Config, stop wg.Wait() statsLog.Infof("All exporters finished stats refresh cycle in %s.", time.Since(cycleStartTime)) - //// transpose stats - //transposedStats := make([]*client.TransposedTaskStatEntry, 0, len(allStats)) - //for _, originalStat := range allStats { - // transposedStats = append(transposedStats, originalStat.Transpose()) - //} - finalJSON, err := json.Marshal(allStats) if err != nil { statsLog.Errorf("Failed to marshal aggregated transposed stats to JSON: %v", err) diff --git a/internal/translator/translator.go b/internal/translator/translator.go deleted file mode 100644 index 9f43440..0000000 --- a/internal/translator/translator.go +++ /dev/null @@ -1,59 +0,0 @@ -// Package translator needs to translate monitoring points (aka 'tochka monitoringa' from RUS to ENG). -// It uses the predefined 'locations.json' file. -package translator - -import ( - "encoding/json" - "fmt" - "os" - "sync" - - "github.com/sirupsen/logrus" -) - -var ( - locationRusToEng map[string]string - once sync.Once - initErr error -) - -// Init loads the locations file once. -func Init(filePath string) error { - once.Do(func() { - log := logrus.WithFields(logrus.Fields{ - "component": "translator", - "path": filePath, - }) - log.Info("Loading translations...") - - file, err := os.ReadFile(filePath) - if err != nil { - initErr = fmt.Errorf("failed to read translations file: %w", err) - log.Error(initErr) - return - } - - if err = json.Unmarshal(file, &locationRusToEng); err != nil { - initErr = fmt.Errorf("failed to parse translations file: %w", err) - log.Error(initErr) - return - } - log.Info("Translations loaded successfully.") - }) - - return initErr -} - -// GetEngLocation returns Monitoring Point name in English -func GetEngLocation(rus string) string { - if initErr != nil { - return rus - } - - if val, ok := locationRusToEng[rus]; ok { - return val - } - - logrus.WithField("location", rus).Warn("Translation not found for location") - return rus -} diff --git a/locations.json b/locations.json deleted file mode 100644 index 9d9d2bf..0000000 --- a/locations.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "Россия, Москва, восток 1": "Russia, Moscow, East 1", - "Россия, Москва, восток 2": "Russia, Moscow, East 2", - "Россия, Москва, восток 3": "Russia, Moscow, East 3", - "Россия, Москва, восток 4": "Russia, Moscow, East 4", - "Россия, Москва, запад 1": "Russia, Moscow, West 1", - "Россия, Москва, запад 2": "Russia, Moscow, West 2", - "Россия, Москва, запад 3": "Russia, Moscow, West 3", - "Россия, Москва, запад 4": "Russia, Moscow, West 4", - "Россия, Москва, запад 5": "Russia, Moscow, West 5", - "Россия, Москва, северо-восток 1": "Russia, Moscow, North-East 1", - "Россия, Москва, северо-восток 2": "Russia, Moscow, North-East 2", - "Россия, Москва, северо-запад": "Russia, Moscow, North-West", - "Россия, Москва, север 1": "Russia, Moscow, North 1", - "Россия, Москва, север 2": "Russia, Moscow, North 2", - "Россия, Москва, север 3": "Russia, Moscow, North 3", - "Россия, Москва, центр 1": "Russia, Moscow, Center 1", - "Россия, Москва, центр 3": "Russia, Moscow, Center 3", - "Россия, Москва, центр 4": "Russia, Moscow, Center 4", - "Россия, Москва, юго-восток 1": "Russia, Moscow, South-East 1", - "Россия, Москва, юго-восток 2": "Russia, Moscow, South-East 2", - "Россия, Москва, юго-восток 3": "Russia, Moscow, South-East 3", - "Россия, Москва, юго-запад 1": "Russia, Moscow, South-West 1", - "Россия, Москва, юго-запад 2": "Russia, Moscow, South-West 2", - "Россия, Москва, юго-запад 3": "Russia, Moscow, South-West 3", - "Россия, Москва, юг 1": "Russia, Moscow, South 1", - "Россия, Москва, юг 2": "Russia, Moscow, South 2", - "Россия, Москва, юг 3": "Russia, Moscow, South 3", - "Россия, Владивосток": "Russia, Vladivostok", - "Россия, Владимир": "Russia, Vladimir", - "Россия, Вольно-Надеждинское": "Russia, Volno-Nadezhdinskoye", - "Россия, Воронеж, восток": "Russia, Voronezh, East", - "Россия, Воронеж, запад": "Russia, Voronezh, West", - "Россия, Дубровка": "Russia, Dubrovka", - "Россия, Евпатория": "Russia, Yevpatoria", - "Россия, Екатеринбург, восток": "Russia, Yekaterinburg, East", - "Россия, Екатеринбург, север": "Russia, Yekaterinburg, North", - "Россия, Екатеринбург, центр": "Russia, Yekaterinburg, Center", - "Россия, Иркутск": "Russia, Irkutsk", - "Россия, Казань": "Russia, Kazan", - "Россия, Калининград 1": "Russia, Kaliningrad 1", - "Россия, Калининград 2": "Russia, Kaliningrad 2", - "Россия, Кемерово": "Russia, Kemerovo", - "Россия, Королёв": "Russia, Korolyov", - "Россия, Краснодар, север": "Russia, Krasnodar, North", - "Россия, Краснодар, юг": "Russia, Krasnodar, South", - "Россия, Красноярск": "Russia, Krasnoyarsk", - "Россия, Нижний Новгород": "Russia, Nizhny Novgorod", - "Россия, Новокузнецк": "Russia, Novokuznetsk", - "Россия, Новосибирск, север": "Russia, Novosibirsk, North", - "Россия, Новосибирск, юг": "Russia, Novosibirsk, South", - "Россия, Омск": "Russia, Omsk", - "Россия, Пермь": "Russia, Perm", - "Россия, Петрозаводск": "Russia, Petrozavodsk", - "Россия, Ростов-на-Дону": "Russia, Rostov-on-Don", - "Россия, Санкт-Петербург, восток": "Russia, Saint Petersburg, East", - "Россия, Санкт-Петербург, север": "Russia, Saint Petersburg, North", - "Россия, Санкт-Петербург, центр 1": "Russia, Saint Petersburg, Center 1", - "Россия, Санкт-Петербург, центр 2": "Russia, Saint Petersburg, Center 2", - "Россия, Санкт-Петербург, центр 3": "Russia, Saint Petersburg, Center 3", - "Россия, Санкт-Петербург, юг": "Russia, Saint Petersburg, South", - "Россия, Саратов": "Russia, Saratov", - "Россия, Северск": "Russia, Seversk", - "Россия, Симферополь": "Russia, Simferopol", - "Россия, Тамбов": "Russia, Tambov", - "Россия, Томск, восток": "Russia, Tomsk, East", - "Россия, Томск, центр": "Russia, Tomsk, Center", - "Россия, Уфа": "Russia, Ufa", - "Россия, Хабаровск": "Russia, Khabarovsk", - "Россия, Химки": "Russia, Khimki", - "Россия, Челябинск": "Russia, Chelyabinsk", - "Россия, Челябинск 1": "Russia, Chelyabinsk 1", - "Россия, Челябинск 2": "Russia, Chelyabinsk 2", - "Россия, Южно-Сахалинск": "Russia, Yuzhno-Sakhalinsk", - "Россия, Ярославль": "Russia, Yaroslavl", - "Австралия, Сидней": "Australia, Sydney", - "Австрия, Вена": "Austria, Vienna", - "Азербайджан, Баку": "Azerbaijan, Baku", - "Армения, Абовян": "Armenia, Abovyan", - "Белоруссия, Гомель": "Belarus, Gomel", - "Белоруссия, Минск": "Belarus, Minsk", - "Болгария, София": "Bulgaria, Sofia", - "Бразилия, Сан-Паулу": "Brazil, Sao Paulo", - "Великобритания, Лондон": "United Kingdom, London", - "Великобритания, Хэмпшир": "United Kingdom, Hampshire", - "Вьетнам, Ханой": "Vietnam, Hanoi", - "Германия, Дюссельдорф": "Germany, Dusseldorf", - "Германия, Мюнхен": "Germany, Munich", - "Германия, Нюрнберг": "Germany, Nuremberg", - "Германия, Фалькенштайн": "Germany, Falkenstein", - "Германия, Франкфурт-на-Майне": "Germany, Frankfurt am Main", - "Германия, Эрфурт": "Germany, Erfurt", - "Гонконг": "Hong Kong", - "Греция, Салоники": "Greece, Thessaloniki", - "Грузия, Тбилиси": "Georgia, Tbilisi", - "Дания, Копенгаген": "Denmark, Copenhagen", - "Египет, Каир": "Egypt, Cairo", - "Израиль, Тель-Авив": "Israel, Tel Aviv", - "Индия, Бангалор": "India, Bangalore", - "Иран, Тегеран": "Iran, Tehran", - "Ирландия, Дублин": "Ireland, Dublin", - "Испания, Мадрид": "Spain, Madrid", - "Италия, Ареццо": "Italy, Arezzo", - "Италия, Милан": "Italy, Milan", - "Казахстан, Актау": "Kazakhstan, Aktau", - "Казахстан, Алатау": "Kazakhstan, Alatau", - "Казахстан, Алматы, восток 1": "Kazakhstan, Almaty, East 1", - "Казахстан, Алматы, восток 2": "Kazakhstan, Almaty, East 2", - "Казахстан, Астана": "Kazakhstan, Astana", - "Казахстан, Караганда": "Kazakhstan, Karaganda", - "Казахстан, Павлодар": "Kazakhstan, Pavlodar", - "Канада, Боарнуа": "Canada, Beauharnois", - "Канада, Ванкувер": "Canada, Vancouver", - "Канада, Монреаль": "Canada, Montreal", - "Канада, Торонто": "Canada, Toronto", - "Кипр, Лимассол": "Cyprus, Limassol", - "Киргизия, Бишкек 1": "Kyrgyzstan, Bishkek 1", - "Киргизия, Бишкек 2": "Kyrgyzstan, Bishkek 2", - "Китай, Нанкин": "China, Nanjing", - "Колумбия, Богота": "Colombia, Bogota", - "Латвия, Рига": "Latvia, Riga", - "Литва, Вильнюс": "Lithuania, Vilnius", - "Люксембург, Штейнсель": "Luxembourg, Steinsel", - "Малайзия, Куала-Лумпур": "Malaysia, Kuala Lumpur", - "Мексика, Пуэбла": "Mexico, Puebla", - "Молдавия, Кишинёв": "Moldova, Chisinau", - "Нигерия, Лагос": "Nigeria, Lagos", - "Нидерланды, Mеппел": "Netherlands, Meppel", - "Нидерланды, Амстердам, юго-запад": "Netherlands, Amsterdam, Southwest", - "Нидерланды, Амстердам, юг 1": "Netherlands, Amsterdam, South 1", - "Нидерланды, Дутинхем": "Netherlands, Doetinchem", - "Нидерланды, Налдвейк": "Netherlands, Naaldwijk", - "Новая Зеландия, Окленд": "New Zealand, Auckland", - "Норвегия, Сандефьорд": "Norway, Sandefjord", - "ОАЭ, Фуджайра": "UAE, Fujairah", - "Польша, Варшава": "Poland, Warsaw", - "Польша, Гданьск": "Poland, Gdansk", - "Португалия, Порту": "Portugal, Porto", - "Румыния, Бухарест": "Romania, Bucharest", - "Сербия, Белград": "Serbia, Belgrade", - "Сингапур": "Singapore", - "Словакия, Братислава": "Slovakia, Bratislava", - "США, Аризона, Финикс": "USA, Arizona, Phoenix", - "США, Вашингтон, Сиэтл": "USA, Washington, Seattle", - "США, Виргиния, Ашберн": "USA, Virginia, Ashburn", - "США, Джорджия, Атланта, север": "USA, Georgia, Atlanta, North", - "США, Джорджия, Атланта, юг": "USA, Georgia, Atlanta, South", - "США, Иллинойс, Чикаго": "USA, Illinois, Chicago", - "США, Калифония, Санта Клара": "USA, California, Santa Clara", - "США, Калифорния, Лос-Анджелес 1": "USA, California, Los Angeles 1", - "США, Калифорния, Лос-Анджелес 2": "USA, California, Los Angeles 2", - "США, Миссури, Канзас-Сити": "USA, Missouri, Kansas City", - "США, Невада, Лас-Вегас": "USA, Nevada, Las Vegas", - "США, Нью-Джерси, Клифтон": "USA, New Jersey, Clifton", - "США, Нью-Йорк, Гарден Сити": "USA, New York, Garden City", - "США, Нью-Йорк, Статен-Айленд": "USA, New York, Staten Island", - "США, Орегон, Бенд": "USA, Oregon, Bend", - "США, Техас, Даллас": "USA, Texas, Dallas", - "США, Флорида, Майами": "USA, Florida, Miami", - "США, Флорида, Тампа": "USA, Florida, Tampa", - "Тайвань, Тайбэй": "Taiwan, Taipei", - "Турция, Измир": "Turkey, Izmir", - "Турция, Стамбул": "Turkey, Istanbul", - "Узбекистан, Ташкент": "Uzbekistan, Tashkent", - "Украина, Винница, запад": "Ukraine, Vinnytsia, West", - "Украина, Винница, центр": "Ukraine, Vinnytsia, Center", - "Украина, Днепр": "Ukraine, Dnipro", - "Украина, Киев, запад": "Ukraine, Kyiv, West", - "Украина, Киев, центр": "Ukraine, Kyiv, Center", - "Украина, Киев, центр 2": "Ukraine, Kyiv, Center 2", - "Украина, Киев, юг": "Ukraine, Kyiv, South", - "Украина, Киев, юго-восток": "Ukraine, Kyiv, Southeast", - "Украина, Николаев": "Ukraine, Mykolaiv", - "Украина, Одесса, восток": "Ukraine, Odesa, East", - "Украина, Харьков, север": "Ukraine, Kharkiv, North", - "Украина, Харьков, юг": "Ukraine, Kharkiv, South", - "Украина, Хмельницкий": "Ukraine, Khmelnytskyi", - "Финляндия, Хельсинки": "Finland, Helsinki", - "Франция, Гравлин": "France, Gravelines", - "Франция, Париж": "France, Paris", - "Франция, Рубе": "France, Roubaix", - "Франция, Страсбург, север": "France, Strasbourg, North", - "Франция, Страсбург, юг": "France, Strasbourg, South", - "Чехия, Прага": "Czech Republic, Prague", - "Чили, Курико": "Chile, Curico", - "Швейцария, Хюненберг": "Switzerland, Hunenberg", - "Швеция, Стокгольм": "Sweden, Stockholm", - "Эстония, Нарва": "Estonia, Narva", - "ЮАР, Йоханнесбург": "South Africa, Johannesburg", - "Южная Корея, Сеул": "South Korea, Seoul", - "Япония, Токио": "Japan, Tokyo" -} \ No newline at end of file