From ecf248d0fb0d64a99179cc40184e6cde0b779f9c Mon Sep 17 00:00:00 2001 From: SlashNephy Date: Tue, 31 Jan 2023 12:31:30 +0900 Subject: [PATCH] Publish --- .editorconfig | 13 ++ .github/workflows/check-go.yml | 28 +++ .github/workflows/release.yml | 32 ++++ .gitignore | 2 + .goreleaser.yml | 16 ++ Makefile | 5 + epgstation.go | 302 +++++++++++++++++++++++++++++++++ go.mod | 15 ++ go.sum | 12 ++ main.go | 285 +++++++++++++++++++++++++++++++ renovate.json | 4 + 11 files changed, 714 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/check-go.yml create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yml create mode 100644 Makefile create mode 100644 epgstation.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 renovate.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f9912c4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 120 +trim_trailing_whitespace = true + +[*.go] +indent_style = tab diff --git a/.github/workflows/check-go.yml b/.github/workflows/check-go.yml new file mode 100644 index 0000000..e198542 --- /dev/null +++ b/.github/workflows/check-go.yml @@ -0,0 +1,28 @@ +name: 'Check' + +on: + push: + branches: + - 'master' + + pull_request: + types: + - opened + - synchronize + + workflow_dispatch: + +jobs: + build: + if: github.event_name != 'push' + uses: SlashNephy/.github/.github/workflows/go-run.yml@master + permissions: + contents: 'read' + with: + task: 'build' + + lint: + uses: SlashNephy/.github/.github/workflows/go-lint.yml@master + permissions: + contents: 'read' + pull-requests: 'write' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..40deab9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version-file: 'go.mod' + cache: true + cache-dependency-path: 'go.sum' + + - name: Release + uses: goreleaser/goreleaser-action@v3 + with: + args: release --rm-dist + env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.gitignore b/.gitignore index 66fd13c..aac12f2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +mackerel-plugin-epgstation diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..d7623c0 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,16 @@ +builds: + - binary: mackerel-plugin-epgstation + goos: + - linux + goarch: + - amd64 + - arm64 + +archives: + - format: zip + name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" + +release: + github: + owner: SlashNephy + name: mackerel-plugin-epgstation diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6a5791a --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +build: + go build -o mackerel-plugin-epgstation ./*.go + +run: + go run ./*.go diff --git a/epgstation.go b/epgstation.go new file mode 100644 index 0000000..2147b72 --- /dev/null +++ b/epgstation.go @@ -0,0 +1,302 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "net/url" + + "github.com/goccy/go-json" +) + +type EPGStationAPI struct { + baseUrl *url.URL + client *http.Client +} + +func NewEPGStationAPI(host string, port int) *EPGStationAPI { + return &EPGStationAPI{ + baseUrl: &url.URL{ + Scheme: "http", + Host: fmt.Sprintf("%s:%d", host, port), + }, + client: &http.Client{}, + } +} + +type EPGStationError struct { + Code int `json:"code"` + Message string `json:"message"` + Errors string `json:"errors"` +} + +type EPGStationStreams struct { + Items []struct { + StreamId int `json:"streamId"` + Type string `json:"type"` + Mode int `json:"mode"` + IsEnable bool `json:"isEnable"` + ChannelId int64 `json:"channelId"` + VideoFileId int `json:"videoFileId"` + RecordedId int `json:"recordedId"` + Name string `json:"name"` + StartAt int `json:"startAt"` + EndAt int `json:"endAt"` + Description string `json:"description"` + Extended string `json:"extended"` + } `json:"items"` + EPGStationError +} + +func (e *EPGStationAPI) GetStreams() (*EPGStationStreams, error) { + var result EPGStationStreams + if err := e.get(&url.URL{Path: "/api/streams", RawQuery: "isHalfWidth=false"}, &result); err != nil { + return nil, err + } + + return &result, nil +} + +type EPGStationReserveCounts struct { + Normal int `json:"normal"` + Conflicts int `json:"conflicts"` + Skips int `json:"skips"` + Overlaps int `json:"overlaps"` + EPGStationError +} + +func (e *EPGStationAPI) GetReserveCounts() (*EPGStationReserveCounts, error) { + var result EPGStationReserveCounts + if err := e.get(&url.URL{Path: "/api/reserves/cnts"}, &result); err != nil { + return nil, err + } + + return &result, nil +} + +type EPGStationRecording struct { + Records []struct { + Id int `json:"id"` + RuleId int `json:"ruleId"` + ProgramId int64 `json:"programId"` + ChannelId int64 `json:"channelId"` + StartAt int `json:"startAt"` + EndAt int `json:"endAt"` + Name string `json:"name"` + Description string `json:"description"` + Extended string `json:"extended"` + RawExtended struct { + } `json:"rawExtended"` + Genre1 int `json:"genre1"` + SubGenre1 int `json:"subGenre1"` + Genre2 int `json:"genre2"` + SubGenre2 int `json:"subGenre2"` + Genre3 int `json:"genre3"` + SubGenre3 int `json:"subGenre3"` + VideoType string `json:"videoType"` + VideoResolution string `json:"videoResolution"` + VideoStreamContent int `json:"videoStreamContent"` + VideoComponentType int `json:"videoComponentType"` + AudioSamplingRate int `json:"audioSamplingRate"` + AudioComponentType int `json:"audioComponentType"` + IsRecording bool `json:"isRecording"` + Thumbnails []int `json:"thumbnails"` + VideoFiles []struct { + Id int `json:"id"` + Name string `json:"name"` + Filename string `json:"filename"` + Type string `json:"type"` + Size int `json:"size"` + } `json:"videoFiles"` + DropLog struct { + Id int `json:"id"` + ErrorCnt int `json:"errorCnt"` + DropCnt int `json:"dropCnt"` + ScramblingCnt int `json:"scramblingCnt"` + } `json:"dropLog"` + Tags []struct { + Id int `json:"id"` + Name string `json:"name"` + Color string `json:"color"` + } `json:"tags"` + IsEncoding bool `json:"isEncoding"` + IsProtected bool `json:"isProtected"` + } `json:"records"` + Total int `json:"total"` + EPGStationError +} + +func (e *EPGStationAPI) GetRecording() (*EPGStationRecording, error) { + var result EPGStationRecording + if err := e.get(&url.URL{Path: "/api/recording", RawQuery: "isHalfWidth=false"}, &result); err != nil { + return nil, err + } + + return &result, nil +} + +type EPGStationEncode struct { + RunningItems []struct { + Id int `json:"id"` + Mode string `json:"mode"` + Recorded struct { + Id int `json:"id"` + RuleId int `json:"ruleId"` + ProgramId int64 `json:"programId"` + ChannelId int64 `json:"channelId"` + StartAt int `json:"startAt"` + EndAt int `json:"endAt"` + Name string `json:"name"` + Description string `json:"description"` + Extended string `json:"extended"` + RawExtended struct { + } `json:"rawExtended"` + Genre1 int `json:"genre1"` + SubGenre1 int `json:"subGenre1"` + Genre2 int `json:"genre2"` + SubGenre2 int `json:"subGenre2"` + Genre3 int `json:"genre3"` + SubGenre3 int `json:"subGenre3"` + VideoType string `json:"videoType"` + VideoResolution string `json:"videoResolution"` + VideoStreamContent int `json:"videoStreamContent"` + VideoComponentType int `json:"videoComponentType"` + AudioSamplingRate int `json:"audioSamplingRate"` + AudioComponentType int `json:"audioComponentType"` + IsRecording bool `json:"isRecording"` + Thumbnails []int `json:"thumbnails"` + VideoFiles []struct { + Id int `json:"id"` + Name string `json:"name"` + Filename string `json:"filename"` + Type string `json:"type"` + Size int `json:"size"` + } `json:"videoFiles"` + DropLog struct { + Id int `json:"id"` + ErrorCnt int `json:"errorCnt"` + DropCnt int `json:"dropCnt"` + ScramblingCnt int `json:"scramblingCnt"` + } `json:"dropLog"` + Tags []struct { + Id int `json:"id"` + Name string `json:"name"` + Color string `json:"color"` + } `json:"tags"` + IsEncoding bool `json:"isEncoding"` + IsProtected bool `json:"isProtected"` + } `json:"recorded"` + Percent int `json:"percent"` + Log string `json:"log"` + } `json:"runningItems"` + WaitItems []struct { + Id int `json:"id"` + Mode string `json:"mode"` + Recorded struct { + Id int `json:"id"` + RuleId int `json:"ruleId"` + ProgramId int64 `json:"programId"` + ChannelId int64 `json:"channelId"` + StartAt int `json:"startAt"` + EndAt int `json:"endAt"` + Name string `json:"name"` + Description string `json:"description"` + Extended string `json:"extended"` + RawExtended struct { + } `json:"rawExtended"` + Genre1 int `json:"genre1"` + SubGenre1 int `json:"subGenre1"` + Genre2 int `json:"genre2"` + SubGenre2 int `json:"subGenre2"` + Genre3 int `json:"genre3"` + SubGenre3 int `json:"subGenre3"` + VideoType string `json:"videoType"` + VideoResolution string `json:"videoResolution"` + VideoStreamContent int `json:"videoStreamContent"` + VideoComponentType int `json:"videoComponentType"` + AudioSamplingRate int `json:"audioSamplingRate"` + AudioComponentType int `json:"audioComponentType"` + IsRecording bool `json:"isRecording"` + Thumbnails []int `json:"thumbnails"` + VideoFiles []struct { + Id int `json:"id"` + Name string `json:"name"` + Filename string `json:"filename"` + Type string `json:"type"` + Size int `json:"size"` + } `json:"videoFiles"` + DropLog struct { + Id int `json:"id"` + ErrorCnt int `json:"errorCnt"` + DropCnt int `json:"dropCnt"` + ScramblingCnt int `json:"scramblingCnt"` + } `json:"dropLog"` + Tags []struct { + Id int `json:"id"` + Name string `json:"name"` + Color string `json:"color"` + } `json:"tags"` + IsEncoding bool `json:"isEncoding"` + IsProtected bool `json:"isProtected"` + } `json:"recorded"` + Percent int `json:"percent"` + Log string `json:"log"` + } `json:"waitItems"` + EPGStationError +} + +func (e *EPGStationAPI) GetEncode() (*EPGStationEncode, error) { + var result EPGStationEncode + if err := e.get(&url.URL{Path: "/api/encode", RawQuery: "isHalfWidth=false"}, &result); err != nil { + return nil, err + } + + return &result, nil +} + +type EPGStationStorages struct { + Items []struct { + Name string `json:"name"` + Available int `json:"available"` + Used int `json:"used"` + Total int `json:"total"` + } `json:"items"` + EPGStationError +} + +func (e *EPGStationAPI) GetStorages() (*EPGStationStorages, error) { + var result EPGStationStorages + if err := e.get(&url.URL{Path: "/api/storages"}, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func (e *EPGStationAPI) get(path *url.URL, result any) error { + requestUrl := e.baseUrl.ResolveReference(path) + request, err := http.NewRequest(http.MethodGet, requestUrl.String(), nil) + if err != nil { + return err + } + + request.Header.Set("User-Agent", "mackerel-plugin-epgstation (+https://github.com/SlashNephy/mackerel-plugin-epgstation)") + + response, err := http.DefaultClient.Do(request) + if err != nil { + return err + } + + defer response.Body.Close() + + body, err := io.ReadAll(response.Body) + if err != nil { + return err + } + + if err = json.Unmarshal(body, &result); err != nil { + return err + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b1bcb66 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/SlashNephy/mackerel-plugin-epgstation + +go 1.19 + +require ( + github.com/goccy/go-json v0.10.0 + github.com/jessevdk/go-flags v1.5.0 + github.com/mackerelio/go-mackerel-plugin v0.1.4 +) + +require ( + github.com/mackerelio/golib v1.2.1 // indirect + golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect + golang.org/x/text v0.3.7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4dcc5f4 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= +github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/mackerelio/go-mackerel-plugin v0.1.4 h1:+kyatPMFSoghKd7o9oge1FnDrVknFbiwz5VWdFIgsqY= +github.com/mackerelio/go-mackerel-plugin v0.1.4/go.mod h1:bau0bZbR1JXiCwDIg880djjttZ/0j885v5k0n+jAS/I= +github.com/mackerelio/golib v1.2.1 h1:SDcDn6Jw3p9bi1N0bg1Z/ilG5qcBB23qL8xNwrU0gg4= +github.com/mackerelio/golib v1.2.1/go.mod h1:b8ZaapsHGH1FlEJlCqfD98CqafLeyMevyATDlID2BsM= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..80cf984 --- /dev/null +++ b/main.go @@ -0,0 +1,285 @@ +package main + +import ( + "fmt" + "log" + + "github.com/jessevdk/go-flags" + mp "github.com/mackerelio/go-mackerel-plugin" +) + +type commandLine struct { + EPGStationHost string `long:"host" description:"EPGStation host" default:"localhost"` + EPGStationPort int `long:"port" description:"EPGStation port" default:"8888"` + Prefix string `long:"prefix" description:"Metric key prefix" default:"epgstation"` + Tempfile string `long:"tempfile" description:"Temp filename"` +} + +type plugin struct { + api *EPGStationAPI + options *commandLine +} + +func (u plugin) MetricKeyPrefix() string { + return u.options.Prefix +} + +const ( + keyStreamsLiveStream = "live_stream" + keyStreamsLiveHLS = "live_hls" + keyStreamsRecordedStream = "recorded_stream" + keyStreamsRecordedHLS = "recorded_hls" + keyReserveNormal = "normal" + keyReserveSkips = "skips" + keyReserveOverlaps = "overlaps" + keyReserveConflicts = "conflicts" + keyRecordingCount = "recording" + keyEncodeRunning = "running" + keyEncodeWaiting = "waiting" + keyStoragesAvailable = "available" + keyStoragesUsed = "used" + keyStoragesTotal = "total" +) + +func (u plugin) GraphDefinition() map[string]mp.Graphs { + return map[string]mp.Graphs{ + "streams": { + Label: "EPGStation Streams", + Unit: mp.UnitInteger, + Metrics: []mp.Metrics{ + { + Name: keyStreamsLiveStream, + Label: "Live Stream", + }, + { + Name: keyStreamsLiveHLS, + Label: "Live HLS", + }, + { + Name: keyStreamsRecordedStream, + Label: "Recorded Stream", + }, + { + Name: keyStreamsRecordedHLS, + Label: "Recorded HLS", + }, + }, + }, + "reservation": { + Label: "EPGStation Reservation", + Unit: mp.UnitInteger, + Metrics: []mp.Metrics{ + { + Name: keyReserveNormal, + Label: "Normal", + }, + { + Name: keyReserveSkips, + Label: "Skips", + }, + { + Name: keyReserveOverlaps, + Label: "Overlaps", + }, + { + Name: keyReserveConflicts, + Label: "Conflicts", + }, + }, + }, + "recording": { + Label: "EPGStation Recording", + Unit: mp.UnitInteger, + Metrics: []mp.Metrics{ + { + Name: keyRecordingCount, + Label: "Count", + }, + }, + }, + "encode": { + Label: "EPGStation Encoding", + Unit: mp.UnitInteger, + Metrics: []mp.Metrics{ + { + Name: keyEncodeRunning, + Label: "Running", + }, + { + Name: keyEncodeWaiting, + Label: "Waiting", + }, + }, + }, + "storages": { + Label: "EPGStation Storages", + Unit: mp.UnitBytes, + Metrics: []mp.Metrics{ + { + Name: keyStoragesAvailable, + Label: "Available", + }, + { + Name: keyStoragesUsed, + Label: "Used", + }, + { + Name: keyStoragesTotal, + Label: "Total", + }, + }, + }, + } +} + +func (u plugin) FetchMetrics() (map[string]float64, error) { + metrics := map[string]float64{} + + if err := u.appendStreamsMetrics(metrics); err != nil { + return nil, err + } + + if err := u.appendReserveCountsMetrics(metrics); err != nil { + return nil, err + } + + if err := u.appendRecordingMetrics(metrics); err != nil { + return nil, err + } + + if err := u.appendEncodeMetrics(metrics); err != nil { + return nil, err + } + + if err := u.appendStoragesMetrics(metrics); err != nil { + return nil, err + } + + return metrics, nil +} + +func (u plugin) appendStreamsMetrics(metrics map[string]float64) error { + streams, err := u.api.GetStreams() + if err != nil { + return err + } + + if streams.Code != 0 { + return fmt.Errorf("failed to get streams: %d: %s, %s", streams.Code, streams.Message, streams.Errors) + } + + var ( + ls float64 + lh float64 + rs float64 + rh float64 + ) + for _, stream := range streams.Items { + switch stream.Type { + case "LiveStream": + ls++ + case "LiveHLS": + lh++ + case "RecordedStream": + rs++ + case "RecordedHLS": + rh++ + } + } + + metrics[keyStreamsLiveStream] = ls + metrics[keyStreamsLiveHLS] = lh + metrics[keyStreamsRecordedStream] = rs + metrics[keyStreamsRecordedHLS] = rh + return nil +} + +func (u plugin) appendReserveCountsMetrics(metrics map[string]float64) error { + counts, err := u.api.GetReserveCounts() + if err != nil { + return err + } + + if counts.Code != 0 { + return fmt.Errorf("failed to get reserve counts: %d: %s, %s", counts.Code, counts.Message, counts.Errors) + } + + metrics[keyReserveNormal] = float64(counts.Normal) + metrics[keyReserveSkips] = float64(counts.Skips) + metrics[keyReserveOverlaps] = float64(counts.Overlaps) + metrics[keyReserveConflicts] = float64(counts.Conflicts) + return nil +} + +func (u plugin) appendRecordingMetrics(metrics map[string]float64) error { + recording, err := u.api.GetRecording() + if err != nil { + return err + } + + if recording.Code != 0 { + return fmt.Errorf("failed to get recording: %d: %s, %s", recording.Code, recording.Message, recording.Errors) + } + + metrics[keyRecordingCount] = float64(len(recording.Records)) + return nil +} + +func (u plugin) appendEncodeMetrics(metrics map[string]float64) error { + encode, err := u.api.GetEncode() + if err != nil { + return err + } + + if encode.Code != 0 { + return fmt.Errorf("failed to get encode: %d: %s, %s", encode.Code, encode.Message, encode.Errors) + } + + metrics[keyEncodeRunning] = float64(len(encode.RunningItems)) + metrics[keyEncodeWaiting] = float64(len(encode.WaitItems)) + return nil +} + +func (u plugin) appendStoragesMetrics(metrics map[string]float64) error { + storages, err := u.api.GetStorages() + if err != nil { + return err + } + + if storages.Code != 0 { + return fmt.Errorf("failed to get storages: %d: %s, %s", storages.Code, storages.Message, storages.Errors) + } + + var ( + available int + used int + total int + ) + for _, storage := range storages.Items { + available += storage.Available + used += storage.Used + total += storage.Total + } + + metrics[keyStoragesAvailable] = float64(available) + metrics[keyStoragesUsed] = float64(used) + metrics[keyStoragesTotal] = float64(total) + return nil +} + +func main() { + options := commandLine{} + parser := flags.NewParser(&options, flags.Default) + + _, err := parser.Parse() + if err != nil { + log.Fatalln(err) + } + + helper := mp.NewMackerelPlugin(plugin{ + api: NewEPGStationAPI(options.EPGStationHost, options.EPGStationPort), + options: &options, + }) + helper.Tempfile = options.Tempfile + helper.Run() +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..ac2c162 --- /dev/null +++ b/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>SlashNephy/.github:renovate-config"] +}