Added logs for twitch
This commit is contained in:
parent
11c4dbe925
commit
42e1ed95fb
|
@ -0,0 +1,52 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||
"github.com/jackc/pgx/v4"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerMigration(AddTwitchLog{})
|
||||
}
|
||||
|
||||
type AddTwitchLog struct{}
|
||||
|
||||
func (m AddTwitchLog) Version() types.MigrationVersion {
|
||||
return types.MigrationVersion(time.Date(2022, 8, 28, 20, 39, 35, 0, time.UTC))
|
||||
}
|
||||
|
||||
func (m AddTwitchLog) Name() string {
|
||||
return "AddTwitchLog"
|
||||
}
|
||||
|
||||
func (m AddTwitchLog) Description() string {
|
||||
return "Add twitch logging"
|
||||
}
|
||||
|
||||
func (m AddTwitchLog) Up(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx,
|
||||
`
|
||||
CREATE TABLE twitch_log (
|
||||
id SERIAL NOT NULL PRIMARY KEY,
|
||||
logged_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
twitch_login VARCHAR(256) NOT NULL DEFAULT '',
|
||||
type INT NOT NULL DEFAULT 0,
|
||||
message TEXT NOT NULL DEFAULT '',
|
||||
payload TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
`,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m AddTwitchLog) Down(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx,
|
||||
`
|
||||
DROP TABLE twitch_log;
|
||||
`,
|
||||
)
|
||||
return err
|
||||
}
|
|
@ -13,3 +13,20 @@ type TwitchStream struct {
|
|||
Title string `db:"title"`
|
||||
StartedAt time.Time `db:"started_at"`
|
||||
}
|
||||
|
||||
type TwitchLogType int
|
||||
|
||||
const (
|
||||
TwitchLogTypeOther TwitchLogType = iota + 1
|
||||
TwitchLogTypeHook
|
||||
TwitchLogTypeREST
|
||||
)
|
||||
|
||||
type TwitchLog struct {
|
||||
ID int `db:"id"`
|
||||
LoggedAt time.Time `db:"logged_at"`
|
||||
Login string `db:"twitch_login"`
|
||||
Type TwitchLogType `db:"type"`
|
||||
Message string `db:"message"`
|
||||
Payload string `db:"payload"`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
{{ template "base.html" . }}
|
||||
|
||||
{{ define "extrahead" }}
|
||||
<style>
|
||||
.twitchdebug a {
|
||||
display: block;
|
||||
border: 1px solid grey;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.twitchdebug > * {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.twitchdebug .selected {
|
||||
background: rgba(128, 255, 128, 0.2);
|
||||
}
|
||||
|
||||
.twitchdebug a.live:after {
|
||||
content: '(LIVE)';
|
||||
}
|
||||
|
||||
.twitchloglist .logline {
|
||||
cursor: pointer;
|
||||
}
|
||||
.twitchuserlist {
|
||||
flex-basis: 10%;
|
||||
}
|
||||
table {
|
||||
flex-basis: 40%;
|
||||
}
|
||||
.twitchdetails {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
td {
|
||||
border: 1px solid grey;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
</style>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="twitchdebug flex flex-row items-start">
|
||||
<div class="twitchuserlist flex-grow-0">
|
||||
<a href="javascript:;" data-login="all">All</a>
|
||||
<a href="javascript:;" data-login="none">No login</a>
|
||||
</div>
|
||||
<table class="flex-grow-1" cellpadding="0" cellspacing="0">
|
||||
<thead>
|
||||
<td>Time</td>
|
||||
<td>Type</td>
|
||||
<td>Login</td>
|
||||
<td>Message</td>
|
||||
</thead>
|
||||
<tbody class="twitchloglist">
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="twitchdetails flex-grow-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let userlist = document.querySelector(".twitchuserlist");
|
||||
let loglist = document.querySelector(".twitchloglist");
|
||||
let details = document.querySelector(".twitchdetails");
|
||||
|
||||
const fmt = new Intl.DateTimeFormat([], {
|
||||
hour12: false,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
|
||||
const data = JSON.parse("{{ .DataJson }}");
|
||||
|
||||
for (let i = 0; i < data.users.length; ++i) {
|
||||
let u = data.users[i];
|
||||
let el = document.createElement("A");
|
||||
el.href = "javascript:;"
|
||||
el.textContent = u.login;
|
||||
el.setAttribute("data-login", u.login);
|
||||
el.classList.toggle("live", u.live);
|
||||
userlist.appendChild(el);
|
||||
}
|
||||
|
||||
function showLogs(login) {
|
||||
loglist.innerHTML = "";
|
||||
details.innerHTML = "";
|
||||
let userEls = userlist.querySelectorAll("A");
|
||||
for (let i = 0; i < userEls.length; ++i) {
|
||||
let el = userEls[i];
|
||||
el.classList.toggle("selected", el.getAttribute("data-login") == login);
|
||||
}
|
||||
|
||||
for (let i = 0; i < data.logs.length; ++i) {
|
||||
let log = data.logs[i];
|
||||
if (login == "all" || log.login == login || (login == "none" && log.login == "")) {
|
||||
let el = document.createElement("tr");
|
||||
el.classList.add("logline");
|
||||
el.setAttribute("data-logid", log.id);
|
||||
let timeEl = document.createElement("td");
|
||||
timeEl.textContent = fmt.format(new Date(log.loggedAt));
|
||||
el.appendChild(timeEl);
|
||||
let typeEl = document.createElement("td");
|
||||
typeEl.textContent = log.type;
|
||||
el.appendChild(typeEl);
|
||||
let loginEl = document.createElement("td");
|
||||
loginEl.textContent = log.login;
|
||||
el.appendChild(loginEl);
|
||||
let messageEl = document.createElement("td");
|
||||
messageEl.textContent = log.message;
|
||||
el.appendChild(messageEl);
|
||||
loglist.appendChild(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showDetails(logId) {
|
||||
details.innerHTML = "";
|
||||
for (let i = 0; i < data.logs.length; ++i) {
|
||||
let log = data.logs[i];
|
||||
if (log.id == logId) {
|
||||
details.textContent = log.payload;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let logEls = loglist.querySelectorAll("tr");
|
||||
for (let i = 0; i < logEls.length; ++i) {
|
||||
logEls[i].classList.toggle("selected", parseInt(logEls[i].getAttribute("data-logid"), 10) == logId);
|
||||
}
|
||||
}
|
||||
|
||||
userlist.addEventListener("click", function(ev) {
|
||||
let el = ev.target;
|
||||
while (el && el.tagName != "A") {
|
||||
el = el.parentElement;
|
||||
}
|
||||
if (el && el.tagName == "A") {
|
||||
let login = el.getAttribute("data-login");
|
||||
showLogs(login);
|
||||
}
|
||||
});
|
||||
|
||||
loglist.addEventListener("click", function(ev) {
|
||||
let el = ev.target;
|
||||
while (el && el.tagName != "TR") {
|
||||
el = el.parentElement;
|
||||
}
|
||||
if (el && el.tagName == "TR") {
|
||||
let logId = el.getAttribute("data-logid");
|
||||
showDetails(logId);
|
||||
}
|
||||
});
|
||||
|
||||
showLogs("all");
|
||||
|
||||
</script>
|
||||
{{ end }}
|
|
@ -3,6 +3,7 @@ package twitch
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
|
@ -76,6 +77,7 @@ func MonitorTwitchSubscriptions(ctx context.Context, dbConn *pgxpool.Pool) jobs.
|
|||
}
|
||||
syncWithTwitch(ctx, dbConn, true)
|
||||
case <-monitorTicker.C:
|
||||
twitchLogClear(ctx, dbConn)
|
||||
syncWithTwitch(ctx, dbConn, true)
|
||||
case <-linksChangedChannel:
|
||||
// NOTE(asaf): Since we update links inside transactions for users/projects
|
||||
|
@ -132,7 +134,7 @@ const (
|
|||
notificationTypeRevocation = 4
|
||||
)
|
||||
|
||||
func QueueTwitchNotification(messageType string, body []byte) error {
|
||||
func QueueTwitchNotification(ctx context.Context, conn db.ConnOrTx, messageType string, body []byte) error {
|
||||
var notification twitchNotification
|
||||
if messageType == "notification" {
|
||||
type notificationJson struct {
|
||||
|
@ -152,6 +154,8 @@ func QueueTwitchNotification(messageType string, body []byte) error {
|
|||
return oops.New(err, "failed to parse notification body")
|
||||
}
|
||||
|
||||
twitchLog(ctx, conn, models.TwitchLogTypeHook, incoming.Event.BroadcasterUserLogin, "Got hook: "+incoming.Subscription.Type, string(body))
|
||||
|
||||
notification.Status.TwitchID = incoming.Event.BroadcasterUserID
|
||||
notification.Status.TwitchLogin = incoming.Event.BroadcasterUserLogin
|
||||
notification.Status.Title = incoming.Event.Title
|
||||
|
@ -170,6 +174,7 @@ func QueueTwitchNotification(messageType string, body []byte) error {
|
|||
return oops.New(nil, "unknown subscription type received")
|
||||
}
|
||||
} else if messageType == "revocation" {
|
||||
twitchLog(ctx, conn, models.TwitchLogTypeHook, "", "Got hook: Revocation", string(body))
|
||||
notification.Type = notificationTypeRevocation
|
||||
}
|
||||
|
||||
|
@ -371,10 +376,12 @@ func syncWithTwitch(ctx context.Context, dbConn *pgxpool.Pool, updateAll bool) {
|
|||
log.Error().Err(err).Msg("failed to fetch stream statuses")
|
||||
return
|
||||
}
|
||||
twitchLog(ctx, tx, models.TwitchLogTypeOther, "", "Batch resync", fmt.Sprintf("%#v", statuses))
|
||||
p.EndBlock()
|
||||
p.StartBlock("SQL", "Update stream statuses in db")
|
||||
for _, status := range statuses {
|
||||
log.Debug().Interface("Status", status).Msg("Got streamer")
|
||||
twitchLog(ctx, tx, models.TwitchLogTypeREST, status.TwitchLogin, "Resync", fmt.Sprintf("%#v", status))
|
||||
err = updateStreamStatusInDB(ctx, tx, &status)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to update twitch stream status")
|
||||
|
@ -456,6 +463,7 @@ func updateStreamStatus(ctx context.Context, dbConn db.ConnOrTx, twitchID string
|
|||
log.Error().Str("TwitchID", twitchID).Err(err).Msg("failed to fetch stream status")
|
||||
return
|
||||
}
|
||||
twitchLog(ctx, dbConn, models.TwitchLogTypeREST, twitchLogin, "Fetched status", fmt.Sprintf("%#v", result))
|
||||
if len(result) > 0 {
|
||||
log.Debug().Interface("Got status", result[0]).Msg("Got streamer status from twitch")
|
||||
status = result[0]
|
||||
|
@ -497,6 +505,7 @@ func processEventSubNotification(ctx context.Context, dbConn db.ConnOrTx, notifi
|
|||
return
|
||||
}
|
||||
|
||||
twitchLog(ctx, dbConn, models.TwitchLogTypeHook, notification.Status.TwitchLogin, "Processing hook", fmt.Sprintf("%#v", notification))
|
||||
if notification.Type == notificationTypeOnline || notification.Type == notificationTypeOffline {
|
||||
log.Debug().Interface("Status", notification.Status).Msg("Updating status")
|
||||
err = updateStreamStatusInDB(ctx, dbConn, ¬ification.Status)
|
||||
|
@ -533,6 +542,7 @@ func processEventSubNotification(ctx context.Context, dbConn db.ConnOrTx, notifi
|
|||
func updateStreamStatusInDB(ctx context.Context, conn db.ConnOrTx, status *streamStatus) error {
|
||||
log := logging.ExtractLogger(ctx)
|
||||
if isStatusRelevant(status) {
|
||||
twitchLog(ctx, conn, models.TwitchLogTypeOther, status.TwitchLogin, "Marking online", fmt.Sprintf("%#v", status))
|
||||
log.Debug().Msg("Status relevant")
|
||||
_, err := conn.Exec(ctx,
|
||||
`
|
||||
|
@ -552,6 +562,7 @@ func updateStreamStatusInDB(ctx context.Context, conn db.ConnOrTx, status *strea
|
|||
}
|
||||
} else {
|
||||
log.Debug().Msg("Stream not relevant")
|
||||
twitchLog(ctx, conn, models.TwitchLogTypeOther, status.TwitchLogin, "Marking offline", fmt.Sprintf("%#v", status))
|
||||
_, err := conn.Exec(ctx,
|
||||
`
|
||||
DELETE FROM twitch_stream WHERE twitch_id = $1
|
||||
|
@ -592,3 +603,35 @@ func isStatusRelevant(status *streamStatus) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func twitchLog(ctx context.Context, conn db.ConnOrTx, logType models.TwitchLogType, login string, message string, payload string) {
|
||||
_, err := conn.Exec(ctx,
|
||||
`
|
||||
INSERT INTO twitch_log (logged_at, twitch_login, type, message, payload)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`,
|
||||
time.Now(),
|
||||
login,
|
||||
logType,
|
||||
message,
|
||||
payload,
|
||||
)
|
||||
if err != nil {
|
||||
log := logging.ExtractLogger(ctx).With().Str("twitch goroutine", "twitch logger").Logger()
|
||||
log.Error().Err(err).Msg("Failed to log twitch event")
|
||||
}
|
||||
}
|
||||
|
||||
func twitchLogClear(ctx context.Context, conn db.ConnOrTx) {
|
||||
_, err := conn.Exec(ctx,
|
||||
`
|
||||
DELETE FROM twitch_log
|
||||
WHERE timestamp <= $1
|
||||
`,
|
||||
time.Now().Add(-(time.Hour * 24 * 4)),
|
||||
)
|
||||
if err != nil {
|
||||
log := logging.ExtractLogger(ctx).With().Str("twitch goroutine", "twitch logger").Logger()
|
||||
log.Error().Err(err).Msg("Failed to clear old twitch logs")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,10 @@ import (
|
|||
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
"git.handmade.network/hmn/hmn/src/hmndata"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
"git.handmade.network/hmn/hmn/src/twitch"
|
||||
)
|
||||
|
||||
|
@ -58,7 +60,7 @@ func TwitchEventSubCallback(c *RequestContext) ResponseData {
|
|||
res.Write([]byte(data.Challenge))
|
||||
return res
|
||||
} else {
|
||||
err := twitch.QueueTwitchNotification(messageType, body)
|
||||
err := twitch.QueueTwitchNotification(c, c.Conn, messageType, body)
|
||||
if err != nil {
|
||||
c.Logger.Error().Err(err).Msg("Failed to process twitch callback")
|
||||
// NOTE(asaf): Returning 200 either way here
|
||||
|
@ -69,25 +71,91 @@ func TwitchEventSubCallback(c *RequestContext) ResponseData {
|
|||
}
|
||||
}
|
||||
|
||||
type TwitchDebugData struct {
|
||||
templates.BaseData
|
||||
DataJson string
|
||||
}
|
||||
|
||||
func TwitchDebugPage(c *RequestContext) ResponseData {
|
||||
streams, err := db.Query[models.TwitchStream](c, c.Conn,
|
||||
type dataUser struct {
|
||||
Login string `json:"login"`
|
||||
Live bool `json:"live"`
|
||||
}
|
||||
type dataLog struct {
|
||||
ID int `json:"id"`
|
||||
LoggedAt int64 `json:"loggedAt"`
|
||||
Type string `json:"type"`
|
||||
Login string `json:"login"`
|
||||
Message string `json:"message"`
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
type dataJson struct {
|
||||
Users []dataUser `json:"users"`
|
||||
Logs []dataLog `json:"logs"`
|
||||
}
|
||||
streamers, err := hmndata.FetchTwitchStreamers(c, c.Conn)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch twitch streamers"))
|
||||
}
|
||||
live, err := db.Query[models.TwitchStream](c, c.Conn,
|
||||
`
|
||||
SELECT $columns
|
||||
FROM
|
||||
twitch_stream
|
||||
ORDER BY started_at DESC
|
||||
`,
|
||||
)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch twitch streams"))
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch live twitch streamers"))
|
||||
}
|
||||
logs, err := db.Query[models.TwitchLog](c, c.Conn,
|
||||
`
|
||||
SELECT $columns
|
||||
FROM twitch_log
|
||||
ORDER BY logged_at DESC, id DESC
|
||||
`,
|
||||
)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch twitch logs"))
|
||||
}
|
||||
|
||||
html := ""
|
||||
for _, s := range streams {
|
||||
html += fmt.Sprintf(`<a href="https://twitch.tv/%s">%s</a>%s<br />`, s.Login, s.Login, s.Title)
|
||||
var data dataJson
|
||||
for _, u := range streamers {
|
||||
var user dataUser
|
||||
user.Login = u.TwitchLogin
|
||||
user.Live = false
|
||||
for _, l := range live {
|
||||
if l.Login == u.TwitchLogin {
|
||||
user.Live = true
|
||||
break
|
||||
}
|
||||
}
|
||||
data.Users = append(data.Users, user)
|
||||
}
|
||||
messageTypes := []string{
|
||||
"",
|
||||
"Other",
|
||||
"Hook",
|
||||
"REST",
|
||||
}
|
||||
data.Logs = make([]dataLog, 0, 0)
|
||||
for _, l := range logs {
|
||||
var log dataLog
|
||||
log.ID = l.ID
|
||||
log.LoggedAt = l.LoggedAt.UnixMilli()
|
||||
log.Login = l.Login
|
||||
log.Type = messageTypes[l.Type]
|
||||
log.Message = l.Message
|
||||
log.Payload = l.Payload
|
||||
data.Logs = append(data.Logs, log)
|
||||
}
|
||||
jsonStr, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to stringify twitch logs"))
|
||||
}
|
||||
var res ResponseData
|
||||
res.StatusCode = 200
|
||||
res.Write([]byte(html))
|
||||
res.MustWriteTemplate("twitch_debug.html", TwitchDebugData{
|
||||
BaseData: getBaseDataAutocrumb(c, "Twitch Debug"),
|
||||
DataJson: string(jsonStr),
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue