Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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` |
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
```

Expand Down
7 changes: 0 additions & 7 deletions cmd/apatit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
}
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion deploy/grafana/monitoring-dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -2170,7 +2170,7 @@
"showLegend": false
},
"mergeValues": true,
"perPage": 20,
"perPage": 10,
"rowHeight": 0.9,
"showValue": "never",
"tooltip": {
Expand Down
94 changes: 14 additions & 80 deletions internal/client/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"`
}
Expand All @@ -140,6 +144,7 @@ type TaskInfo struct {
type MonitoringPointInfo struct {
ID string
Name string
NameEn string
IP string
GPS string
Status int64
Expand All @@ -150,6 +155,7 @@ type MonitoringPointInfo struct {
type MonitoringPointEntry struct {
ID string
Name string
NameEn string
Status int
Result []*MonitoringPointConnectionResult
}
Expand Down Expand Up @@ -180,6 +186,7 @@ type TaskLog struct {
Data string
Description string
Status int64
MPNameRu string
MPName string
MPID string
Traceroute string
Expand All @@ -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"),
Expand Down Expand Up @@ -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)),
}

Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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
//}
12 changes: 0 additions & 12 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
type Config struct {
APIKey string
TaskIDs []int
EngMPNames bool
ApiUpdateDelay time.Duration
ApiDataTimeStep time.Duration
RefreshInterval time.Duration
Expand All @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
34 changes: 13 additions & 21 deletions internal/exporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/sirupsen/logrus"

"apatit/internal/client"
"apatit/internal/translator"
)

// Exporter collects metrics for a single task.
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}

Expand All @@ -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 {
Expand All @@ -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,
}
Expand Down
1 change: 1 addition & 0 deletions internal/exporter/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
LabelTaskName = "task_name"
LabelMPID = "mp_id"
LabelMPName = "mp_name"
LabelMPNameRu = "mp_name_ru"
LabelMPIP = "mp_ip"
LabelMPGPS = "mp_gps"
)
Loading