pico
created pr with
ps-74
added ps-77
1: 77aaa29 ! 1: 8d56535 reactor(metric-drain): use caddy json format
-: ------- > 2: a336041 wip
-: ------- > 3: 7ae45b3 chore: wrap
-: ------- > 4: bfa5c4f done
added ps-78
1: 8d56535 < -: ------- reactor(metric-drain): use caddy json format
-: ------- > 1: c7eeb12 reactor(metric-drain): use caddy access logs
2: a336041 < -: ------- wip
3: 7ae45b3 < -: ------- chore: wrap
4: bfa5c4f < -: ------- done
added ps-79
1: c7eeb12 ! 1: 4e0839a reactor(metric-drain): use caddy access logs
changed status to
accepted
cmds
checkout latest patchset:
ssh pr.pico.sh print pr-35 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print ps-X | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 35set PR to open (enables RSS notifications):
ssh pr.pico.sh pr open 35set PR to draft (stops RSS notifications):
ssh pr.pico.sh pr draft 35
Patchset
ps-77
reactor(metric-drain): use caddy json format
Eric Bower
2024-11-15T15:02:24ZSemantic diff summary
5 added,
5 modified,
0 signature changed,
2 removed
across 4 analyzed files
(1 file skipped: unsupported file type)
+106
-26
auth/api.go
#
@@ -14,6 +14,7 @@ import (
"log/slog"
"net/http"
"net/url"
+ "strings"
"time"
"github.com/gorilla/feeds"
@@ -578,6 +579,89 @@ func checkoutHandler() http.HandlerFunc {
}
}
+type AccessLogReq struct {
+ RemoteIP string `json:"remote_ip"`
+ RemotePort string `json:"remote_port"`
+ ClientIP string `json:"client_ip"`
+ Method string `json:"method"`
+ Host string `json:"host"`
+ Uri string `json:"uri"`
+ Headers struct {
+ UserAgent string `json:"User-Agent"`
+ Referer string `json:"Referer"`
+ } `json:"headers"`
+ Tls struct {
+ ServerName string `json:"server_name"`
+ } `json:"tls"`
+}
+
+type CaddyAccessLog struct {
+ Request AccessLogReq `json:"request"`
+ Status int `json:"status"`
+}
+
+func deserializeCaddyAccessLog(dbpool db.DB, access *CaddyAccessLog) (*db.AnalyticsVisits, error) {
+ spaceRaw := strings.SplitN(access.Request.Tls.ServerName, ".", 2)
+ space := spaceRaw[0]
+ host := access.Request.Host
+ path := access.Request.Uri
+ subdomain := ""
+
+ // grab subdomain based on host
+ if strings.HasSuffix(host, "tuns.sh") {
+ subdomain = strings.TrimSuffix(host, ".tuns.sh")
+ } else if strings.HasSuffix(host, "pgs.sh") {
+ subdomain = strings.TrimSuffix(host, ".pgs.sh")
+ } else if strings.HasSuffix(host, "prose.sh") {
+ subdomain = strings.TrimSuffix(host, ".prose.sh")
+ } else {
+ subdomain = shared.GetCustomDomain(host, space)
+ }
+
+ // get user and namespace details from subdomain
+ props, err := shared.GetProjectFromSubdomain(subdomain)
+ if err != nil {
+ return nil, err
+ }
+ // get user ID
+ user, err := dbpool.FindUserForName(props.Username)
+ if err != nil {
+ return nil, err
+ }
+
+ projectID := ""
+ postID := ""
+ if space == "pgs" { // figure out project ID
+ project, err := dbpool.FindProjectByName(user.ID, props.ProjectName)
+ if err != nil {
+ return nil, err
+ }
+ projectID = project.ID
+ } else if space == "prose" { // figure out post ID
+ if path == "" || path == "/" {
+ } else {
+ post, err := dbpool.FindPostWithSlug(path, user.ID, space)
+ if err != nil {
+ return nil, err
+ }
+ postID = post.ID
+ }
+ }
+
+ return &db.AnalyticsVisits{
+ UserID: user.ID,
+ ProjectID: projectID,
+ PostID: postID,
+ Namespace: space,
+ Host: host,
+ Path: path,
+ IpAddress: access.Request.ClientIP,
+ UserAgent: access.Request.Headers.UserAgent,
+ Referer: access.Request.Headers.Referer, // TODO: I don't see referer in the access log
+ Status: access.Status,
+ }, nil
+}
+
func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secret string) {
drain := metrics.ReconnectReadMetrics(
ctx,
@@ -587,36 +671,32 @@ func metricDrainSub(ctx context.Context, dbpool db.DB, logger *slog.Logger, secr
-1,
)
- for {
- scanner := bufio.NewScanner(drain)
- for scanner.Scan() {
- line := scanner.Text()
- visit := db.AnalyticsVisits{}
- err := json.Unmarshal([]byte(line), &visit)
- if err != nil {
- logger.Error("json unmarshal", "err", err)
- continue
- }
-
- user := slog.Any("userId", visit.UserID)
-
- err = shared.AnalyticsVisitFromVisit(&visit, dbpool, secret)
- if err != nil {
- if !errors.Is(err, shared.ErrAnalyticsDisabled) {
- logger.Info("could not record analytics visit", "reason", err, "visit", visit, user)
- continue
- }
- }
+ scanner := bufio.NewScanner(drain)
+ for scanner.Scan() {
+ line := scanner.Text()
+ accessLog := CaddyAccessLog{}
+ err := json.Unmarshal([]byte(line), &accessLog)
+ if err != nil {
+ logger.Error("json unmarshal", "err", err)
+ continue
+ }
- logger.Info("inserting visit", "visit", visit, user)
- err = dbpool.InsertVisit(&visit)
- if err != nil {
- logger.Error("could not insert visit record", "err", err, "visit", visit, user)
+ visit, err := deserializeCaddyAccessLog(dbpool, &accessLog)
+ if err != nil {
+ logger.Error("cannot deserialize access log", "err", err)
+ continue
+ }
+ err = shared.AnalyticsVisitFromVisit(visit, dbpool, secret)
+ if err != nil {
+ if !errors.Is(err, shared.ErrAnalyticsDisabled) {
+ logger.Info("could not record analytics visit", "reason", err)
}
}
- if scanner.Err() != nil {
- logger.Error("scanner error", "err", scanner.Err())
+ logger.Info("inserting visit", "visit", visit)
+ err = dbpool.InsertVisit(visit)
+ if err != nil {
+ logger.Error("could not insert visit record", "err", err)
}
}
}
+40
-0
caddy.json
#
@@ -0,0 +1,40 @@
+{
+ "level": "info",
+ "ts": 1731644477.313701,
+ "logger": "http.log.access",
+ "msg": "handled request",
+ "request": {
+ "remote_ip": "127.0.0.1",
+ "remote_port": "40400",
+ "client_ip": "127.0.0.1",
+ "proto": "HTTP/2.0",
+ "method": "GET",
+ "host": "pgs.sh",
+ "uri": "/",
+ "headers": { "User-Agent": ["Blackbox Exporter/0.24.0"] },
+ "tls": {
+ "resumed": false,
+ "version": 772,
+ "cipher_suite": 4865,
+ "proto": "h2",
+ "server_name": "pgs.sh"
+ }
+ },
+ "bytes_read": 0,
+ "user_id": "",
+ "duration": 0.001207084,
+ "size": 3718,
+ "status": 200,
+ "resp_headers": {
+ "Referrer-Policy": ["no-referrer-when-downgrade"],
+ "Strict-Transport-Security": ["max-age=31536000;"],
+ "X-Content-Type-Options": ["nosniff"],
+ "X-Frame-Options": ["DENY"],
+ "Server": ["Caddy"],
+ "Alt-Svc": ["h3=\":443\"; ma=2592000"],
+ "Date": ["Fri, 15 Nov 2024 04:21:17 GMT"],
+ "Content-Type": ["text/html; charset=utf-8"],
+ "X-Xss-Protection": ["1; mode=block"],
+ "Permissions-Policy": ["interest-cohort=()"]
+ }
+}
+1
-1
pgs/tunnel.go
#
@@ -51,7 +51,7 @@ func createHttpHandler(apiConfig *shared.ApiConfig) CtxHttpBridge {
"pubkey", pubkeyStr,
)
- props, err := getProjectFromSubdomain(subdomain)
+ props, err := shared.GetProjectFromSubdomain(subdomain)
if err != nil {
log.Error(err.Error())
return http.HandlerFunc(shared.UnauthorizedHandler)
+2
-19
pgs/web.go
#
@@ -177,7 +177,7 @@ func (web *WebRouter) checkHandler(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(hostDomain, appDomain) {
subdomain := shared.GetCustomDomain(hostDomain, cfg.Space)
- props, err := getProjectFromSubdomain(subdomain)
+ props, err := shared.GetProjectFromSubdomain(subdomain)
if err != nil {
logger.Error(
"could not get project from subdomain",
@@ -333,7 +333,7 @@ func (web *WebRouter) ServeAsset(fname string, opts *storage.ImgProcessOpts, fro
"host", r.Host,
)
- props, err := getProjectFromSubdomain(subdomain)
+ props, err := shared.GetProjectFromSubdomain(subdomain)
if err != nil {
logger.Info(
"could not determine project from subdomain",
@@ -450,20 +450,3 @@ func (web *WebRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx = context.WithValue(ctx, shared.CtxSubdomainKey{}, subdomain)
router.ServeHTTP(w, r.WithContext(ctx))
}
-
-type SubdomainProps struct {
- ProjectName string
- Username string
-}
-
-func getProjectFromSubdomain(subdomain string) (*SubdomainProps, error) {
- props := &SubdomainProps{}
- strs := strings.SplitN(subdomain, "-", 2)
- props.Username = strs[0]
- if len(strs) == 2 {
- props.ProjectName = strs[1]
- } else {
- props.ProjectName = props.Username
- }
- return props, nil
-}