mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
feat(annotations): working on alert annotations, #5694
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
"github.com/kr/s3/s3util"
|
"github.com/kr/s3/s3util"
|
||||||
)
|
)
|
||||||
@@ -31,8 +30,6 @@ func (u *S3Uploader) Upload(path string) (string, error) {
|
|||||||
|
|
||||||
s3util.DefaultConfig.AccessKey = u.accessKey
|
s3util.DefaultConfig.AccessKey = u.accessKey
|
||||||
s3util.DefaultConfig.SecretKey = u.secretKey
|
s3util.DefaultConfig.SecretKey = u.secretKey
|
||||||
log.Info("AccessKey: %s", u.accessKey)
|
|
||||||
log.Info("SecretKey: %s", u.secretKey)
|
|
||||||
|
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
header.Add("x-amz-acl", "public-read")
|
header.Add("x-amz-acl", "public-read")
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const (
|
|||||||
AlertSeverityCritical AlertSeverityType = "critical"
|
AlertSeverityCritical AlertSeverityType = "critical"
|
||||||
AlertSeverityWarning AlertSeverityType = "warning"
|
AlertSeverityWarning AlertSeverityType = "warning"
|
||||||
AlertSeverityInfo AlertSeverityType = "info"
|
AlertSeverityInfo AlertSeverityType = "info"
|
||||||
|
AlertSeverityOK AlertSeverityType = "ok"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s AlertSeverityType) IsValid() bool {
|
func (s AlertSeverityType) IsValid() bool {
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AnnotationType string
|
|
||||||
|
|
||||||
type Annotation struct {
|
|
||||||
Id int64
|
|
||||||
OrgId int64
|
|
||||||
Type AnnotationType
|
|
||||||
Title string
|
|
||||||
Text string
|
|
||||||
AlertId int64
|
|
||||||
UserId int64
|
|
||||||
PreviousState string
|
|
||||||
NewState string
|
|
||||||
Timestamp time.Time
|
|
||||||
|
|
||||||
Data *simplejson.Json
|
|
||||||
}
|
|
||||||
@@ -38,8 +38,8 @@ func (e *Engine) Start() {
|
|||||||
e.log.Info("Starting Alerting Engine")
|
e.log.Info("Starting Alerting Engine")
|
||||||
|
|
||||||
go e.alertingTicker()
|
go e.alertingTicker()
|
||||||
go e.execDispatch()
|
go e.execDispatcher()
|
||||||
go e.resultDispatch()
|
go e.resultDispatcher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) Stop() {
|
func (e *Engine) Stop() {
|
||||||
@@ -70,7 +70,7 @@ func (e *Engine) alertingTicker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) execDispatch() {
|
func (e *Engine) execDispatcher() {
|
||||||
for job := range e.execQueue {
|
for job := range e.execQueue {
|
||||||
e.log.Debug("Starting executing alert rule %s", job.Rule.Name)
|
e.log.Debug("Starting executing alert rule %s", job.Rule.Name)
|
||||||
go e.executeJob(job)
|
go e.executeJob(job)
|
||||||
@@ -92,10 +92,10 @@ func (e *Engine) executeJob(job *Job) {
|
|||||||
e.resultQueue <- context
|
e.resultQueue <- context
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) resultDispatch() {
|
func (e *Engine) resultDispatcher() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
e.log.Error("Engine Panic, stopping resultHandler", "error", err, "stack", log.Stack(1))
|
e.log.Error("Panic in resultDispatcher", "error", err, "stack", log.Stack(1))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ func (c *EvalContext) GetStateText() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *EvalContext) GetNotificationTitle() string {
|
||||||
|
return "[" + c.GetStateText() + "] " + c.Rule.Name
|
||||||
|
}
|
||||||
|
|
||||||
func (c *EvalContext) getDashboardSlug() (string, error) {
|
func (c *EvalContext) getDashboardSlug() (string, error) {
|
||||||
if c.dashboardSlug != "" {
|
if c.dashboardSlug != "" {
|
||||||
return c.dashboardSlug, nil
|
return c.dashboardSlug, nil
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
|
|||||||
|
|
||||||
cmd := &m.SendEmailCommand{
|
cmd := &m.SendEmailCommand{
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
|
"Title": context.GetNotificationTitle(),
|
||||||
"RuleState": context.Rule.State,
|
"RuleState": context.Rule.State,
|
||||||
"RuleName": context.Rule.Name,
|
"RuleName": context.Rule.Name,
|
||||||
"Severity": context.Rule.Severity,
|
"Severity": context.Rule.Severity,
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ type SlackNotifier struct {
|
|||||||
func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
|
func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
|
||||||
this.log.Info("Executing slack notification", "ruleId", context.Rule.Id, "notification", this.Name)
|
this.log.Info("Executing slack notification", "ruleId", context.Rule.Id, "notification", this.Name)
|
||||||
|
|
||||||
rule := context.Rule
|
|
||||||
|
|
||||||
ruleUrl, err := context.GetRuleUrl()
|
ruleUrl, err := context.GetRuleUrl()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.log.Error("Failed get rule link", "error", err)
|
this.log.Error("Failed get rule link", "error", err)
|
||||||
@@ -68,7 +66,7 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
|
|||||||
// "author_name": "Bobby Tables",
|
// "author_name": "Bobby Tables",
|
||||||
// "author_link": "http://flickr.com/bobby/",
|
// "author_link": "http://flickr.com/bobby/",
|
||||||
// "author_icon": "http://flickr.com/icons/bobby.jpg",
|
// "author_icon": "http://flickr.com/icons/bobby.jpg",
|
||||||
"title": "[" + context.GetStateText() + "] " + rule.Name,
|
"title": context.GetNotificationTitle(),
|
||||||
"title_link": ruleUrl,
|
"title_link": ruleUrl,
|
||||||
// "text": "Optional text that appears within the attachment",
|
// "text": "Optional text that appears within the attachment",
|
||||||
"fields": fields,
|
"fields": fields,
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ func (this *WebhookNotifier) Notify(context *alerting.EvalContext) {
|
|||||||
this.log.Info("Sending webhook")
|
this.log.Info("Sending webhook")
|
||||||
|
|
||||||
bodyJSON := simplejson.New()
|
bodyJSON := simplejson.New()
|
||||||
bodyJSON.Set("name", context.Rule.Name)
|
bodyJSON.Set("title", context.GetNotificationTitle())
|
||||||
|
bodyJSON.Set("ruleId", context.Rule.Id)
|
||||||
|
bodyJSON.Set("ruleName", context.Rule.Name)
|
||||||
bodyJSON.Set("firing", context.Firing)
|
bodyJSON.Set("firing", context.Firing)
|
||||||
bodyJSON.Set("severity", context.Rule.Severity)
|
bodyJSON.Set("severity", context.Rule.Severity)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResultHandler interface {
|
type ResultHandler interface {
|
||||||
Handle(result *EvalContext)
|
Handle(ctx *EvalContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultResultHandler struct {
|
type DefaultResultHandler struct {
|
||||||
@@ -22,32 +25,47 @@ func NewResultHandler() *DefaultResultHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *DefaultResultHandler) Handle(result *EvalContext) {
|
func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
|
||||||
var newState m.AlertStateType
|
oldState := ctx.Rule.State
|
||||||
|
|
||||||
if result.Error != nil {
|
if ctx.Error != nil {
|
||||||
handler.log.Error("Alert Rule Result Error", "ruleId", result.Rule.Id, "error", result.Error)
|
handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error)
|
||||||
newState = m.AlertStatePending
|
ctx.Rule.State = m.AlertStatePending
|
||||||
} else if result.Firing {
|
} else if ctx.Firing {
|
||||||
newState = m.AlertStateFiring
|
ctx.Rule.State = m.AlertStateFiring
|
||||||
} else {
|
} else {
|
||||||
newState = m.AlertStateOK
|
ctx.Rule.State = m.AlertStateOK
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Rule.State != newState {
|
if ctx.Rule.State != oldState {
|
||||||
handler.log.Info("New state change", "alertId", result.Rule.Id, "newState", newState, "oldState", result.Rule.State)
|
handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState)
|
||||||
|
|
||||||
cmd := &m.SetAlertStateCommand{
|
cmd := &m.SetAlertStateCommand{
|
||||||
AlertId: result.Rule.Id,
|
AlertId: ctx.Rule.Id,
|
||||||
OrgId: result.Rule.OrgId,
|
OrgId: ctx.Rule.OrgId,
|
||||||
State: newState,
|
State: ctx.Rule.State,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(cmd); err != nil {
|
if err := bus.Dispatch(cmd); err != nil {
|
||||||
handler.log.Error("Failed to save state", "error", err)
|
handler.log.Error("Failed to save state", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Rule.State = newState
|
// save annotation
|
||||||
handler.notifier.Notify(result)
|
item := annotations.Item{
|
||||||
|
OrgId: ctx.Rule.OrgId,
|
||||||
|
Type: annotations.AlertType,
|
||||||
|
AlertId: ctx.Rule.Id,
|
||||||
|
Title: ctx.Rule.Name,
|
||||||
|
Text: ctx.GetStateText(),
|
||||||
|
NewState: string(ctx.Rule.State),
|
||||||
|
PrevState: string(oldState),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
annotationRepo := annotations.GetRepository()
|
||||||
|
if err := annotationRepo.Save(&item); err != nil {
|
||||||
|
handler.log.Error("Failed to save annotation for new alert state", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.notifier.Notify(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func NewScheduler() Scheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SchedulerImpl) Update(rules []*Rule) {
|
func (s *SchedulerImpl) Update(rules []*Rule) {
|
||||||
s.log.Debug("Scheduling update", "rules.count", len(rules))
|
s.log.Debug("Scheduling update", "ruleCount", len(rules))
|
||||||
|
|
||||||
jobs := make(map[int64]*Job, 0)
|
jobs := make(map[int64]*Job, 0)
|
||||||
|
|
||||||
|
|||||||
44
pkg/services/annotations/annotations.go
Normal file
44
pkg/services/annotations/annotations.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package annotations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
Save(item *Item) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var repositoryInstance Repository
|
||||||
|
|
||||||
|
func GetRepository() Repository {
|
||||||
|
return repositoryInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetRepository(rep Repository) {
|
||||||
|
repositoryInstance = rep
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AlertType ItemType = "alert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
OrgId int64 `json:"orgId"`
|
||||||
|
PanelLinkId string `json:"panelLinkId"`
|
||||||
|
Type ItemType `json:"type"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Metric string `json:"metric"`
|
||||||
|
AlertId int64 `json:"alertId"`
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
PrevState string `json:"prevState"`
|
||||||
|
NewState string `json:"newState"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
|
||||||
|
Data *simplejson.Json `json:"data"`
|
||||||
|
}
|
||||||
21
pkg/services/sqlstore/annotation.go
Normal file
21
pkg/services/sqlstore/annotation.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package sqlstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SqlAnnotationRepo struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
|
||||||
|
return inTransaction(func(sess *xorm.Session) error {
|
||||||
|
|
||||||
|
if _, err := sess.Table("annotation").Insert(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -31,21 +31,6 @@ func addAlertMigrations(mg *Migrator) {
|
|||||||
// create table
|
// create table
|
||||||
mg.AddMigration("create alert table v1", NewAddTableMigration(alertV1))
|
mg.AddMigration("create alert table v1", NewAddTableMigration(alertV1))
|
||||||
|
|
||||||
alert_state_log := Table{
|
|
||||||
Name: "alert_state",
|
|
||||||
Columns: []*Column{
|
|
||||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
|
||||||
{Name: "alert_id", Type: DB_BigInt, Nullable: false},
|
|
||||||
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
|
||||||
{Name: "state", Type: DB_NVarchar, Length: 50, Nullable: false},
|
|
||||||
{Name: "info", Type: DB_Text, Nullable: true},
|
|
||||||
{Name: "triggered_alerts", Type: DB_Text, Nullable: true},
|
|
||||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mg.AddMigration("create alert_state_log table v1", NewAddTableMigration(alert_state_log))
|
|
||||||
|
|
||||||
alert_heartbeat := Table{
|
alert_heartbeat := Table{
|
||||||
Name: "alert_heartbeat",
|
Name: "alert_heartbeat",
|
||||||
Columns: []*Column{
|
Columns: []*Column{
|
||||||
|
|||||||
40
pkg/services/sqlstore/migrations/annotation_mig.go
Normal file
40
pkg/services/sqlstore/migrations/annotation_mig.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addAnnotationMig(mg *Migrator) {
|
||||||
|
table := Table{
|
||||||
|
Name: "annotation",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||||
|
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
||||||
|
{Name: "alert_id", Type: DB_BigInt, Nullable: true},
|
||||||
|
{Name: "user_id", Type: DB_BigInt, Nullable: true},
|
||||||
|
{Name: "panel_link_id", Type: DB_NVarchar, Length: 32, Nullable: false},
|
||||||
|
{Name: "type", Type: DB_NVarchar, Length: 25, Nullable: false},
|
||||||
|
{Name: "title", Type: DB_Text, Nullable: false},
|
||||||
|
{Name: "text", Type: DB_Text, Nullable: false},
|
||||||
|
{Name: "metric", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||||
|
{Name: "prev_state", Type: DB_NVarchar, Length: 25, Nullable: false},
|
||||||
|
{Name: "new_state", Type: DB_NVarchar, Length: 25, Nullable: false},
|
||||||
|
{Name: "data", Type: DB_Text, Nullable: false},
|
||||||
|
{Name: "timestamp", Type: DB_DateTime, Nullable: false},
|
||||||
|
},
|
||||||
|
Indices: []*Index{
|
||||||
|
{Cols: []string{"org_id", "alert_id"}, Type: IndexType},
|
||||||
|
{Cols: []string{"org_id", "type"}, Type: IndexType},
|
||||||
|
{Cols: []string{"org_id", "panel_link_id"}, Type: IndexType},
|
||||||
|
{Cols: []string{"timestamp"}, Type: IndexType},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.AddMigration("create annotation table v1", NewAddTableMigration(table))
|
||||||
|
|
||||||
|
// create indices
|
||||||
|
mg.AddMigration("add index annotation org_id & alert_id ", NewAddIndexMigration(table, table.Indices[0]))
|
||||||
|
mg.AddMigration("add index annotation org_id & type", NewAddIndexMigration(table, table.Indices[1]))
|
||||||
|
mg.AddMigration("add index annotation org_id & panel_link_id ", NewAddIndexMigration(table, table.Indices[2]))
|
||||||
|
mg.AddMigration("add index annotation timestamp", NewAddIndexMigration(table, table.Indices[3]))
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ func AddMigrations(mg *Migrator) {
|
|||||||
addPlaylistMigrations(mg)
|
addPlaylistMigrations(mg)
|
||||||
addPreferencesMigrations(mg)
|
addPreferencesMigrations(mg)
|
||||||
addAlertMigrations(mg)
|
addAlertMigrations(mg)
|
||||||
|
addAnnotationMig(mg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addMigrationLogMigrations(mg *Migrator) {
|
func addMigrationLogMigrations(mg *Migrator) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@@ -97,6 +98,8 @@ func SetEngine(engine *xorm.Engine) (err error) {
|
|||||||
return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err)
|
return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
annotations.SetRepository(&SqlAnnotationRepo{})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
var alertQueryDef = new QueryPartDef({
|
var alertQueryDef = new QueryPartDef({
|
||||||
type: 'query',
|
type: 'query',
|
||||||
params: [
|
params: [
|
||||||
{name: "queryRefId", type: 'string', options: ['#A', '#B', '#C', '#D']},
|
{name: "queryRefId", type: 'string', options: ['A', 'B', 'C', 'D', 'E', 'F']},
|
||||||
{name: "from", type: "string", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']},
|
{name: "from", type: "string", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']},
|
||||||
{name: "to", type: "string", options: ['now']},
|
{name: "to", type: "string", options: ['now']},
|
||||||
],
|
],
|
||||||
@@ -142,7 +142,7 @@ export class AlertTabCtrl {
|
|||||||
return memo;
|
return memo;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
///this.panelCtrl.editingAlert = true;
|
this.panelCtrl.editingAlert = true;
|
||||||
this.syncThresholds();
|
this.syncThresholds();
|
||||||
this.panelCtrl.render();
|
this.panelCtrl.render();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user