Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: grafana/amixr-api-go-client
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.0.15
Choose a base ref
...
head repository: grafana/amixr-api-go-client
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref

Commits on Oct 7, 2024

  1. Copy the full SHA
    7a86c28 View commit details

Commits on Oct 8, 2024

  1. Merge pull request #23 from grafana/matiasb/escalation-policy-severity

    Add `severity` escalation option
    matiasb authored Oct 8, 2024
    Copy the full SHA
    a0f8b48 View commit details

Commits on Nov 6, 2024

  1. update CODEOWNERS

    joeyorlando committed Nov 6, 2024
    Copy the full SHA
    8ca93e5 View commit details
  2. Copy the full SHA
    eb61b49 View commit details

Commits on Nov 14, 2024

  1. Copy the full SHA
    60304e7 View commit details

Commits on Nov 26, 2024

  1. Bump github.com/hashicorp/go-retryablehttp from 0.6.6 to 0.7.7 (#26)

    Bumps [github.com/hashicorp/go-retryablehttp](https://github.com/hashicorp/go-retryablehttp) from 0.6.6 to 0.7.7.
    - [Changelog](https://github.com/hashicorp/go-retryablehttp/blob/main/CHANGELOG.md)
    - [Commits](hashicorp/go-retryablehttp@v0.6.6...v0.7.7)
    
    ---
    updated-dependencies:
    - dependency-name: github.com/hashicorp/go-retryablehttp
      dependency-type: direct:production
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Nov 26, 2024
    Copy the full SHA
    2aeb52c View commit details

Commits on Nov 28, 2024

  1. Merge pull request #24 from grafana/matiasb/client-passing-grafana-url

    Add option to instantiate client passing grafana URL
    matiasb authored Nov 28, 2024
    Copy the full SHA
    63aded6 View commit details

Commits on Dec 16, 2024

  1. Copy the full SHA
    c4dc183 View commit details
  2. Merge pull request #27 from grafana/matiasb/allow-empty-string-token

    Allow instantiating the client with an empty string token
    matiasb authored Dec 16, 2024
    Copy the full SHA
    1e17613 View commit details

Commits on Jan 28, 2025

  1. Copy the full SHA
    0ff7960 View commit details

Commits on Mar 17, 2025

  1. Fix ratelimits (#28)

    vstpme authored Mar 17, 2025
    Copy the full SHA
    477f370 View commit details

Commits on Mar 20, 2025

  1. Copy the full SHA
    5c7bc23 View commit details

Commits on Mar 26, 2025

  1. Copy the full SHA
    55a8d93 View commit details
  2. Copy the full SHA
    f781a73 View commit details

Commits on Mar 27, 2025

  1. Merge pull request #30 from grafana/add-teamid-in-schedules

    Add teamID to the schedule list options
    ioanarm authored Mar 27, 2025
    Copy the full SHA
    ab28268 View commit details
  2. Merge pull request #29 from grafana/ese-rebase

    Add alertGroup Service to implement alert_groups API endpoints
    ioanarm authored Mar 27, 2025
    Copy the full SHA
    034f790 View commit details

Commits on Apr 7, 2025

  1. Copy the full SHA
    ee19829 View commit details

Commits on Apr 11, 2025

  1. Merge pull request #31 from grafana/get-alert-groups-by-teamID

    alert_groups: add additional options as query parameters
    ioanarm authored Apr 11, 2025
    Copy the full SHA
    9f05d31 View commit details

Commits on Apr 13, 2025

  1. Copy the full SHA
    743639d View commit details

Commits on Apr 14, 2025

  1. Merge pull request #32 from grafana/oncallIntegrationLabels

    feat: Oncall integration labels
    willgallego-grafana authored Apr 14, 2025
    Copy the full SHA
    6977163 View commit details

Commits on May 29, 2025

  1. Copy the full SHA
    0ce7257 View commit details

Commits on May 30, 2025

  1. Copy the full SHA
    4794671 View commit details

Commits on Jun 2, 2025

  1. Merge pull request #33 from grafana/oncallIntegrationsDynamicLabels

    adding Dynamic Labels to OnCall Integration
    willgallego-grafana authored Jun 2, 2025
    Copy the full SHA
    5233423 View commit details
Showing with 606 additions and 99 deletions.
  1. +1 −1 .github/CODEOWNERS
  2. +139 −0 alert_group.go
  3. +210 −0 alert_group_test.go
  4. +50 −71 client.go
  5. +106 −0 client_test.go
  6. +3 −0 escalation_policy.go
  7. +1 −2 go.mod
  8. +33 −12 go.sum
  9. +24 −9 integration.go
  10. +32 −2 integration_test.go
  11. +3 −1 on_call_shift_test.go
  12. +4 −1 schedule.go
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @grafana/grafana-oncall-backend
* @grafana/grafana-irm-backend
139 changes: 139 additions & 0 deletions alert_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package aapi

import (
"fmt"
"net/http"
"regexp"
"time"
)

// AlertGroupService handles requests to the on-call alert_groups endpoint.
//
// https://grafana.com/docs/oncall/latest/oncall-api-reference/alertgroups/
type AlertGroupService struct {
client *Client
url string
}

// NewAlertGroupService creates an AlertGroupService with the defined URL.
func NewAlertGroupService(client *Client) *AlertGroupService {
alertGroupService := AlertGroupService{}
alertGroupService.client = client
alertGroupService.url = "alert_groups"
return &alertGroupService
}

// PaginatedAlertGroupsResponse represents a paginated response from the on-call alerts API.
type PaginatedAlertGroupsResponse struct {
PaginatedResponse
AlertGroups []*AlertGroup `json:"results"`
}

// AlertGroup represents an on-call alert group.
type AlertGroup struct {
ID string `json:"id"`
IntegrationID string `json:"integration_id"`
RouteID string `json:"route_id"`
AlertsCount int `json:"alerts_count"`
State string `json:"state"`
CreatedAt string `json:"created_at"`
ResolvedAt string `json:"resolved_at"`
AcknowledgedAt string `json:"acknowledged_at"`
Title string `json:"title"`
Permalinks map[string]string `json:"permalinks"`
}

// validateTimeRange validates if the time range string matches the expected format
// Expected format: %Y-%m-%dT%H:%M:%S_%Y-%m-%dT%H:%M:%S
func validateTimeRange(timeRange string) error {
if timeRange == "" {
return nil
}

// Check if the string matches the expected format
pattern := `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}_\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$`
matched, err := regexp.MatchString(pattern, timeRange)
if err != nil {
return fmt.Errorf("error validating time range format: %v", err)
}
if !matched {
return fmt.Errorf("invalid time range format. Expected format: YYYY-MM-DDThh:mm:ss_YYYY-MM-DDThh:mm:ss")
}

// Split the time range into start and end times
times := regexp.MustCompile(`_`).Split(timeRange, 2)
if len(times) != 2 {
return fmt.Errorf("invalid time range format: missing separator '_'")
}

// Parse both times to ensure they are valid
startTime, err := time.Parse("2006-01-02T15:04:05", times[0])
if err != nil {
return fmt.Errorf("invalid start time format: %v", err)
}

endTime, err := time.Parse("2006-01-02T15:04:05", times[1])
if err != nil {
return fmt.Errorf("invalid end time format: %v", err)
}

// Validate that end time is after start time
if endTime.Before(startTime) {
return fmt.Errorf("end time must be after start time")
}

return nil
}

// ListAlertGroupOptions represent filter options supported by the on-call alert_groups API.
type ListAlertGroupOptions struct {
ListOptions
AlertGroupID string `url:"alert_group_id,omitempty" json:"alert_group_id,omitempty"`
RouteID string `url:"route_id,omitempty" json:"route_id,omitempty"`
IntegrationID string `url:"integration_id,omitempty" json:"integration_id,omitempty" `
State string `url:"state,omitempty" json:"state,omitempty" `
TeamID string `url:"team_id,omitempty" json:"team_id,omitempty"`
// StartedAt is a time range in ISO 8601 format with start and end timestamps separated by underscore.
// Expected format: %Y-%m-%dT%H:%M:%S_%Y-%m-%dT%H:%M:%S
// Example: "2024-03-20T10:00:00_2024-03-21T10:00:00"
StartedAt string `url:"started_at,omitempty" json:"started_at,omitempty"`
// Labels are matching labels that can be passed multiple times.
// Expected format: key1:value1
// Example: ["env:prod", "severity:high"]
Labels []string `url:"label,omitempty" json:"label,omitempty"`
Name string `url:"name,omitempty" json:"name,omitempty"`
}

// Validate checks if the options are valid
func (o *ListAlertGroupOptions) Validate() error {
if err := validateTimeRange(o.StartedAt); err != nil {
return err
}
return nil
}

// ListAlertGroups fetches all on-call alerts for authorized organization.
//
// https://grafana.com/docs/oncall/latest/oncall-api-reference/alertgroups/
func (service *AlertGroupService) ListAlertGroups(opt *ListAlertGroupOptions) (*PaginatedAlertGroupsResponse, *http.Response, error) {
if opt != nil {
if err := opt.Validate(); err != nil {
return nil, nil, err
}
}

u := fmt.Sprintf("%s/", service.url)

req, err := service.client.NewRequest("GET", u, opt)
if err != nil {
return nil, nil, err
}

var alertGroups *PaginatedAlertGroupsResponse
resp, err := service.client.Do(req, &alertGroups)
if err != nil {
return nil, resp, err
}

return alertGroups, resp, err
}
210 changes: 210 additions & 0 deletions alert_group_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package aapi

import (
"fmt"
"net/http"
"reflect"
"testing"
)

var testAlertGroup = &AlertGroup{
ID: "I68T24C13IFW1",
IntegrationID: "CFRPV98RPR1U8",
RouteID: "RIYGUJXCPFHXY",
AlertsCount: 3,
State: "resolved",
CreatedAt: "2020-05-19T12:37:01.430444Z",
ResolvedAt: "2020-05-19T13:37:01.429805Z",
Title: "Memory above 90% threshold",
Permalinks: map[string]string{
"slack": "https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500008",
"telegram": "https://t.me/c/5354/1234?thread=1234",
},
}

var testAlertGroupBody = `{
"id": "I68T24C13IFW1",
"integration_id": "CFRPV98RPR1U8",
"route_id": "RIYGUJXCPFHXY",
"alerts_count": 3,
"state": "resolved",
"created_at": "2020-05-19T12:37:01.430444Z",
"resolved_at": "2020-05-19T13:37:01.429805Z",
"acknowledged_at": null,
"title": "Memory above 90% threshold",
"permalinks": {
"slack": "https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500008",
"telegram": "https://t.me/c/5354/1234?thread=1234"
}
}`

func TestListAlertGroup(t *testing.T) {
mux, server, client := setup(t)
defer teardown(server)

mux.HandleFunc("/api/v1/alert_groups/", func(w http.ResponseWriter, r *http.Request) {
testRequestMethod(t, r, "GET")
fmt.Fprint(w, fmt.Sprintf(`{"count": 1, "next": null, "previous": null, "results": [%s]}`, testAlertGroupBody))
})

options := &ListAlertGroupOptions{
AlertGroupID: "I68T24C13IFW1",
}

alerts, _, err := client.AlertGroups.ListAlertGroups(options)
if err != nil {
t.Fatal(err)
}

want := &PaginatedAlertGroupsResponse{
PaginatedResponse: PaginatedResponse{
Count: 1,
Next: nil,
Previous: nil,
},
AlertGroups: []*AlertGroup{
testAlertGroup,
},
}

if !reflect.DeepEqual(want, alerts) {
fmt.Println(alerts.AlertGroups[0])
fmt.Println(want.AlertGroups[0])
t.Errorf(" returned\n %+v, \nwant\n %+v", alerts, want)
}
}

func TestValidateTimeRange(t *testing.T) {
tests := []struct {
name string
timeRange string
wantErr bool
}{
{
name: "valid time range",
timeRange: "2024-03-20T10:00:00_2024-03-21T10:00:00",
wantErr: false,
},
{
name: "empty time range",
timeRange: "",
wantErr: false,
},
{
name: "invalid format - missing separator",
timeRange: "2024-03-20T10:00:002024-03-21T10:00:00",
wantErr: true,
},
{
name: "invalid format - wrong date format",
timeRange: "2024/03/20T10:00:00_2024/03/21T10:00:00",
wantErr: true,
},
{
name: "invalid time - end before start",
timeRange: "2024-03-21T10:00:00_2024-03-20T10:00:00",
wantErr: true,
},
{
name: "invalid time - invalid hour",
timeRange: "2024-03-20T25:00:00_2024-03-21T10:00:00",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateTimeRange(tt.timeRange)
if (err != nil) != tt.wantErr {
t.Errorf("validateTimeRange() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestListAlertGroupsValidation(t *testing.T) {
tests := []struct {
name string
options *ListAlertGroupOptions
wantErr bool
}{
{
name: "valid options",
options: &ListAlertGroupOptions{
StartedAt: "2024-03-20T10:00:00_2024-03-21T10:00:00",
},
wantErr: false,
},
{
name: "invalid time range",
options: &ListAlertGroupOptions{
StartedAt: "invalid-time-range",
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.options.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("ListAlertGroupOptions.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestListAlertGroupQueryURL(t *testing.T) {
tests := []struct {
name string
options *ListAlertGroupOptions
expectedURL string
}{
{
name: "single label",
options: &ListAlertGroupOptions{
Labels: []string{"env:prod"},
},
expectedURL: "/api/v1/alert_groups/?label=env%3Aprod",
},
{
name: "multiple labels",
options: &ListAlertGroupOptions{
Labels: []string{"env:prod", "severity:high", "team:backend"},
},
expectedURL: "/api/v1/alert_groups/?label=env%3Aprod&label=severity%3Ahigh&label=team%3Abackend",
},
{
name: "empty labels",
options: &ListAlertGroupOptions{
Labels: []string{},
},
expectedURL: "/api/v1/alert_groups/",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mux, server, client := setup(t)
defer teardown(server)

var capturedURL string
mux.HandleFunc("/api/v1/alert_groups/", func(w http.ResponseWriter, r *http.Request) {
capturedURL = r.URL.String()
t.Logf("Request URL: %s", capturedURL)
w.WriteHeader(http.StatusOK)
// Add minimal response body
fmt.Fprint(w, `{"count": 0, "next": null, "previous": null, "results": []}`)
})

_, _, err := client.AlertGroups.ListAlertGroups(tt.options)
if err != nil {
t.Fatal(err)
}

if capturedURL != tt.expectedURL {
t.Errorf("Request URL = %v, want %v", capturedURL, tt.expectedURL)
}
})
}
}
Loading