diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..8f6cb21 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$USER_HOME$/arc42-stats-dev.db + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/documentation/3-context-status-arc42-org4canvas.drawio.png b/documentation/3-context-status-arc42-org4canvas.drawio.png deleted file mode 100644 index 0f80196..0000000 Binary files a/documentation/3-context-status-arc42-org4canvas.drawio.png and /dev/null differ diff --git a/documentation/5-building-blocks-status-arc42-org.drawio b/documentation/5-building-blocks-status-arc42-org.drawio deleted file mode 100644 index c5d97ad..0000000 --- a/documentation/5-building-blocks-status-arc42-org.drawio +++ /dev/null @@ -1,283 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/documentation/5-building-blocks-status-arc42-org.drawio.png b/documentation/5-building-blocks-status-arc42-org.drawio.png deleted file mode 100644 index cf3ea14..0000000 Binary files a/documentation/5-building-blocks-status-arc42-org.drawio.png and /dev/null differ diff --git a/documentation/adrs/0011-rate-limiter-with-persistently-stored-last-query-time.md b/documentation/adrs/0011-rate-limiter-with-persistently-stored-last-query-time.md index ba48644..451b826 100644 --- a/documentation/adrs/0011-rate-limiter-with-persistently-stored-last-query-time.md +++ b/documentation/adrs/0011-rate-limiter-with-persistently-stored-last-query-time.md @@ -1,10 +1,11 @@ # 11. rate-limiter with persistently stored last-query-time Date: 2023-12-29 +modified: 2024-01-25 ## Status -Accepted +Accepted. ## Context @@ -23,10 +24,10 @@ If our current server is a "fresh instance", we have to ignore the last-time-cal ## Decision -1. Create a table that keeps the invocation times of our API +1. Create a table that keeps the invocation times of **our** API 2. Create tables that keep the last-query time for both Plausible and GitHub requests. -2. Call the Plausible.io API only once every `Plausible_Rate_Limit_Minutes` (defaulting to 20) -3. Call the GitHub API only once every `GitHub_Rate_Limit_Minutes` (defaulting to 3) +2. Call the Plausible.io API only once every `plausible_Rate_Limit_Minutes` (defaulting to 20) +3. Call the GitHub API only once every `gitHub_Rate_Limit_Minutes` (defaulting to 3) The tables shall have the following format: diff --git a/documentation/arc42/chapters/03_system_scope_and_context.adoc b/documentation/arc42/chapters/03_system_scope_and_context.adoc index d4922c8..dfd8099 100644 --- a/documentation/arc42/chapters/03_system_scope_and_context.adoc +++ b/documentation/arc42/chapters/03_system_scope_and_context.adoc @@ -9,28 +9,34 @@ ifndef::imagesdir[:imagesdir: ../../images] :toc: - [[section-system-scope-and-context]] == System Scope and Context +image::03-context-status-arc42-org4canvas.drawio.png[] +[cols="1,3,1"] +|=== +| Element | Responsibility | Code +| Plausible.io +| Web analytics SAAS platform, counts viewers and pageviews +|`/internal/plausible` +| Fly.io +| Cloud (hyperscaler) platform, where the status.arc42.org application is deployed and executed. Besides deploying the golang application on their servers, We ask fly.io for the current server region, utilizing their https://fly.io/docs/reference/[API]. +|`/internal/fly` -=== Business Context - - - -**** - -**** - -=== Technical Context - +| GitHub +| Hosting all repositories for arc42 sites and subdomains. API returns bug and issue count plus other info related to these repositories. +| `/internal/github` +| Slack +| Notification service: When users request certain actions in the status app, notifications are sent to a specific Slack channel/app. +| currently in planning -**** +| Turso Database +| A cloud-hosted, multi-instance database storing several types of runtime data we need to persist. For example: startup times of the app. +| ìnternal/database` -**** +|=== -**** diff --git a/documentation/arc42/chapters/04_solution_strategy.adoc b/documentation/arc42/chapters/04_solution_strategy.adoc index dfd51ec..feddaa1 100644 --- a/documentation/arc42/chapters/04_solution_strategy.adoc +++ b/documentation/arc42/chapters/04_solution_strategy.adoc @@ -13,5 +13,13 @@ ifndef::imagesdir[:imagesdir: ../../images] [[section-solution-strategy]] == Solution Strategy +* implement in Golang (facilitates cloud deployment) +* use https://plausible.io[plausible.io] to collect usage data (commercial service with excellent data privacy, no cookies) +* static site generator (Jekyll, Github pages) for main site +* use https://fly.io[fly.io] for cloud deployment +* use https://turso.tech[turso.tech] for cloud data storage + +Fully open-source, source hosted on https://github.com/arc42/status.arc42.org-site[public GitHub repository]. + diff --git a/documentation/arc42/chapters/05_building_block_view.adoc b/documentation/arc42/chapters/05_building_block_view.adoc index 41502c4..42d2d10 100644 --- a/documentation/arc42/chapters/05_building_block_view.adoc +++ b/documentation/arc42/chapters/05_building_block_view.adoc @@ -19,22 +19,42 @@ ifndef::imagesdir[:imagesdir: ../../images] === Whitebox Overall System +image::05-building-blocks-status-arc42-org.drawio.png[] -_****_ +[cols="1,3,1"] +|=== +| Element | Responsibility | Code -Motivation:: +| main +| Golang requires a "main" func as entry point in the application. +| -__ +| api gateway +| an http server with several predefined routes. This server is called from the public website, dispatches to domain and returns either plain html to the static site (or other formats when called via the upcoming technical API) +|`internal/api` +| domain +| core functionality, coordination of various subsystems, results collection +| -Contained Building Blocks:: -__ +| types +| a few data types and -structures used by other parts. Extracted from the domain to avoid circular dependencies. +| -Important Interfaces:: -__ +| github +| wrapper for the public GitHub (graphql) API. We query several repository infos (e.g. nr of open issues, bugs and pull-requests). +| +|database +|wrapper for https://turso.tech[Turso], an SQLite clone running in the cloud. We store some usage and operations data, and wanted to avoid hosting our own DB. +| +|fly.io +|wrapper for the https://fly.io[fly.io API], which we use to find out the server region where our application is deployed. Not to be confused with our _hosting and deployment_ concept. +| + +|=== ==== @@ -54,68 +74,3 @@ _<(Optional) Fulfilled Requirements>_ _<(optional) Open Issues/Problems/Risks>_ - - -==== - -__ - -==== - -__ - - -==== - -... - -==== - - - -=== Level 2 - - - -==== White Box __ - - - -__ - -==== White Box __ - - -__ - -... - -==== White Box __ - - -__ - - - -=== Level 3 - - - - -==== White Box <_building block x.1_> - - - - -__ - - -==== White Box <_building block x.2_> - -__ - - - -==== White Box <_building block y.1_> - -__ diff --git a/documentation/concurrent-API-access.puml b/documentation/concurrent-API-access.puml deleted file mode 100644 index 76ac95a..0000000 --- a/documentation/concurrent-API-access.puml +++ /dev/null @@ -1,28 +0,0 @@ -@startuml -'https://plantuml.com/sequence-diagram - -autonumber - - -group parallel [domain.go] - domain -> domain : init logger - loop Arc42Sites - par LoadStats4Sites - domain -> siteStats - activate siteStats - par getSiteStatistics - siteStats -> plausible: get 7D - siteStats -> plausible: get 30D - siteStats -> plausible: get 12M - end - deactivate siteStats - domain -> repoStats - par getRepoStatistics - repoStats -> github - end - domain -> github: getRepoStatistics - - end - -end -@enduml \ No newline at end of file diff --git a/documentation/images/03-context-status-arc42-org4canvas.drawio.png b/documentation/images/03-context-status-arc42-org4canvas.drawio.png new file mode 100644 index 0000000..7e93477 Binary files /dev/null and b/documentation/images/03-context-status-arc42-org4canvas.drawio.png differ diff --git a/documentation/images/05-building-blocks-status-arc42-org.drawio.png b/documentation/images/05-building-blocks-status-arc42-org.drawio.png new file mode 100644 index 0000000..797b63d Binary files /dev/null and b/documentation/images/05-building-blocks-status-arc42-org.drawio.png differ diff --git a/documentation/images/06-api-call-with-cors.png b/documentation/images/06-api-call-with-cors.png new file mode 100644 index 0000000..9164b75 Binary files /dev/null and b/documentation/images/06-api-call-with-cors.png differ diff --git a/documentation/images/06-api-call-with-cors.puml b/documentation/images/06-api-call-with-cors.puml new file mode 100644 index 0000000..d979b99 --- /dev/null +++ b/documentation/images/06-api-call-with-cors.puml @@ -0,0 +1,29 @@ +@startuml +'https://plantuml.com/sequence-diagram + +autonumber + + +actor client as "Client" +participant site as "status.arc42.org\nstatic website" +participant htmx as "htmx\nJavaScript lib" +participant server as "api Gateway" + +client -> site : "https://status.arc42.org" +site -> htmx: replace table +activate htmx + +htmx -> server: OPTIONS statsTable +activate server +server -> server : Set CORS headers +server --> htmx : return 200 OK + +htmx -> server : GET /statsTable +server -> server : Set CORS headers +server -> server : Perform additional processing +server --> htmx : return 200 OK with data +deactivate server +htmx --> site: html table\ncontaining results +deactivate htmx +site --> client: display table +@enduml diff --git a/documentation/images/06-concurrent-API-access.puml b/documentation/images/06-concurrent-API-access.puml new file mode 100644 index 0000000..523e563 --- /dev/null +++ b/documentation/images/06-concurrent-API-access.puml @@ -0,0 +1,34 @@ +@startuml +'https://plantuml.com/sequence-diagram + +autonumber + +participant domain +participant "domain.\nsiteStats" as siteStats +participant "domain.\nrepoStats" as repoStats +group parallel [domain.go] + domain -> domain : init logger + loop for site in allSites + par + domain -> siteStats: get SiteStatistics(site) + activate siteStats + par + siteStats -> plausible: get 7D( site ) + else + siteStats -> plausible: get 30D( site ) + else + siteStats -> plausible: get 12M( site ) + end + deactivate siteStats + else + domain -> repoStats: getRepoStatistics(site) + activate repoStats + repoStats -> github: getRepoStatistics + activate github + deactivate github + deactivate repoStats + end + domain -> domain: aggregateResults( site ) + +end +@enduml \ No newline at end of file diff --git a/go-app/go.mod b/go-app/go.mod index c4618a4..eb4c5ff 100644 --- a/go-app/go.mod +++ b/go-app/go.mod @@ -7,7 +7,7 @@ require ( github.com/rs/zerolog v1.31.0 github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 github.com/tursodatabase/libsql-client-go v0.0.0-20231216154754-8383a53d618f - golang.org/x/net v0.18.0 + golang.org/x/net v0.20.0 golang.org/x/oauth2 v0.14.0 golang.org/x/text v0.14.0 ) @@ -20,11 +20,14 @@ require ( github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.19 // indirect github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.50.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/tools v0.17.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect nhooyr.io/websocket v1.8.7 // indirect diff --git a/go-app/go.sum b/go-app/go.sum index 64d2757..f5bf3dd 100644 --- a/go-app/go.sum +++ b/go-app/go.sum @@ -60,6 +60,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -93,11 +95,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -110,6 +116,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -119,6 +127,8 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/go-app/internal/api/apiGateway.go b/go-app/internal/api/apiGateway.go index f63123c..6690fd5 100644 --- a/go-app/internal/api/apiGateway.go +++ b/go-app/internal/api/apiGateway.go @@ -35,42 +35,53 @@ func init() { var embeddedTemplatesFolder embed.FS // statsHTMLTableHandler returns the usage statistics as html table -// 1. start timer -// 2. update ArcStats -// 3. sets required http headers needed for CORS +// 1. sets required http headers needed for CORS +// 2a. for the preflight OPTIONS request, just return the CORS header and OK. +// otherwise: +// 2b. start timer +// 3. update ArcStats // 4. render the output via HtmlTableTmpl func statsHTMLTableHandler(w http.ResponseWriter, r *http.Request) { log.Debug().Msg("received statsTable request") - // 1. set timer - var startOfProcessing = time.Now() + // handle the CORS stuff + SetCORSHeaders(&w, r) - // 2. update ArcStats - domain.ArcStats = domain.LoadStats4AllSites() + //2a. Check if it's an OPTIONS request (preflight) + if r.Method == "OPTIONS" { + // No further action beyond setting headers is required for the preflight request + w.WriteHeader(http.StatusOK) + return + } else { + + // 2b. set timer + var startOfProcessing = time.Now() - // remember how long it took to update statistics - domain.ArcStats.HowLongDidItTake = strconv.FormatInt(time.Since(startOfProcessing).Milliseconds(), 10) + // 3. update ArcStats + domain.ArcStats = domain.LoadStats4AllSites() - //find out where this service is running - domain.ArcStats.FlyRegion, domain.ArcStats.WhereDoesItRun = fly.RegionAndLocation() + // remember how long it took to update statistics + domain.ArcStats.HowLongDidItTake = strconv.FormatInt(time.Since(startOfProcessing).Milliseconds(), 10) - // 3. handle the CORS stuff - SendCORSHeaders(&w, r) + // find out where this service is running + domain.ArcStats.FlyRegion, domain.ArcStats.WhereDoesItRun = fly.RegionAndLocation() - // 4. store request params in database - // TODO: make this asyn - database.SaveInvocationParams(r.Host, r.RequestURI) + // 4. store request params in database + // TODO: make this async + // TODO: include real IP address + database.SaveInvocationParams(r.Host, r.RequestURI) - // 5. finally, render the template - executeTemplate(w, filepath.Join(TemplatesDir, HtmlTableTmpl), domain.ArcStats) + // 5. finally, render the template + executeTemplate(w, filepath.Join(TemplatesDir, HtmlTableTmpl), domain.ArcStats) + } } // pingHandler returns a message and the time func pingHandler(w http.ResponseWriter, r *http.Request) { // need to set specific headers, depending on request origin - SendCORSHeaders(&w, r) + SetCORSHeaders(&w, r) var Host string = r.Host var RequestURI string = r.RequestURI @@ -80,10 +91,10 @@ func pingHandler(w http.ResponseWriter, r *http.Request) { executeTemplate(w, filepath.Join(TemplatesDir, PingTmpl), r) } -// sendCORSHeaders sets specific headers +// setCORSHeaders sets specific headers // * calls from the "official" URL status.arc42.org are allowed // * calls from localhost or "null" are also allowed -func SendCORSHeaders(w *http.ResponseWriter, r *http.Request) { +func SetCORSHeaders(w *http.ResponseWriter, r *http.Request) { // TODO: why do we use * here? diff --git a/go-app/internal/database/atlas.hcl b/go-app/internal/database/atlas.hcl index 15adaf1..078863a 100644 --- a/go-app/internal/database/atlas.hcl +++ b/go-app/internal/database/atlas.hcl @@ -9,14 +9,20 @@ variable "token" { default = getenv("TURSO_AUTH_TOKEN") } -// Define an environment named "local" +# set user home directory +variable "userhome" { + type = string + default = getenv("HOME") +} + +// Define an environment named "dev" env "dev" { // Declare where the schema definition resides. // Also supported: ["file://multi.hcl", "file://schema.hcl"]. src = "file://schema.hcl" // Define the URL of the database which is managed in this environment. - url = "sqlite://file.db?_fk=1" + url = "sqlite://${var.userhome}/arc42-stats-dev.db?_fk=1" // Define the URL of the Dev Database for this environment // See: https://atlasgo.io/concepts/dev-database diff --git a/go-app/internal/database/schema.hcl b/go-app/internal/database/schema.hcl index 4dbfe10..f29220c 100644 --- a/go-app/internal/database/schema.hcl +++ b/go-app/internal/database/schema.hcl @@ -1,6 +1,8 @@ # schema "main" { } + + table "system_startup" { schema = schema.main column "startup" { @@ -17,6 +19,7 @@ table "system_startup" { } } +# table "time_of_invocation" { schema = schema.main column "invocation_time" { @@ -27,9 +30,24 @@ table "time_of_invocation" { null = false type = varchar(16) } - column "route"{ + column "route" { null = false type = varchar(50) } +} +table "time_of_plausible_call" { + schema = schema.main + column "invocation_time" { + null = false + type = datetime + } } + +table "time_of_github_call" { + schema = schema.main + column "invocation_time" { + null = false + type = datetime + } +} \ No newline at end of file diff --git a/go-app/internal/database/tursoDB.go b/go-app/internal/database/tursoDB.go index 964ca28..8072bc3 100644 --- a/go-app/internal/database/tursoDB.go +++ b/go-app/internal/database/tursoDB.go @@ -1,11 +1,15 @@ package database import ( + "arc42-status/internal/env" "database/sql" "fmt" + _ "github.com/mattn/go-sqlite3" "github.com/rs/zerolog/log" _ "github.com/tursodatabase/libsql-client-go/libsql" "os" + "os/user" + "path/filepath" "sync" "time" ) @@ -18,8 +22,13 @@ import ( // database schema (tables, columns) are defined in file "schema.hcl" // and managed by Atlas. -const TursoDBName = "arc42-statistics" -const TursoURLPlain = "libsql://" + TursoDBName + "-gernotstarke.turso.io" +const tursoPRODDBName = "arc42-statistics" +const tursoTESTDBName = "arc42-stats-dev.db" + +const tursoPRODUrl = "libsql://" + tursoPRODDBName + "-gernotstarke.turso.io" +const tursoTESTUrl = "libsql://" + tursoTESTDBName + "-gernotstarke.turso.io" + +const LocalSQLiteURL = "sqlite://dev.db?_fk=1" const TableTimeOfSystemStart = "system_startup" const TableTimeOfInvocation = "time_of_invocation" @@ -49,12 +58,6 @@ var ( dbInstance *sql.DB ) -func DatabaseURL(env string) string { - var dburl string - - return dburl -} - // initAuthToken should not be called directly, it is only used by the Singleton GetDB() func initAuthToken() string { tursoAuthToken := os.Getenv("TURSO_AUTH_TOKEN") @@ -69,22 +72,59 @@ func initAuthToken() string { return tursoAuthToken } +// pathToDevDB determines the location of the DEV and TEST database: +// we use the users' home directory with a subdirectory /.arc42-sqlite/ +func pathToDevDB() string { + // CurrentUser returns the current user. + usr, err := user.Current() + if err != nil { + log.Fatal().Msg("error in getting current user") + } + + devDBPath := filepath.Join(usr.HomeDir, tursoTESTDBName) + + log.Debug().Msgf("path to DEV db is %s", devDBPath) + return devDBPath +} + // GetDB is a singleton function that returns a pointer to a sql.DB object. // It ensures that only one instance of the database connection is created. -// For PROD +// For PROD, this is always the Turso libSQL database. +// For DEV or TEST, this is a local instance of SQLite. func GetDB() *sql.DB { once.Do(func() { + var dbUrl string + var driverName string + + switch env.GetEnv() { + case "PROD": + { + dbUrl = tursoPRODUrl + "?authToken=" + initAuthToken() + driverName = "libsql" + break + } + case "DEV", "TEST": + { + dbUrl = pathToDevDB() + driverName = "sqlite3" + break + } + default: + { + // this should never happen, as env.GetEnv() needs to care for valid environments + log.Error().Msgf("Invalid environment %s specified", env.GetEnv()) + os.Exit(13) + } + } - var dbUrl = TursoURLPlain + "?authToken=" + initAuthToken() - - db, err := sql.Open("libsql", dbUrl) + // open the database + db, err := sql.Open(driverName, dbUrl) if err != nil { - log.Error().Msgf("failed to open db %s: %s", dbUrl, err) + log.Error().Msgf("Failed to open db %s: %s", dbUrl, err) os.Exit(13) } dbInstance = db }) - return dbInstance } diff --git a/go-app/internal/domain/domain.go b/go-app/internal/domain/domain.go index 13b19e3..eebad88 100644 --- a/go-app/internal/domain/domain.go +++ b/go-app/internal/domain/domain.go @@ -81,7 +81,6 @@ func LoadStats4AllSites() types.Arc42Statistics { return a42s } -// func calculateTotals(stats []types.SiteStats) types.TotalsForAllSites { func calculateTotals(stats [len(types.Arc42sites)]types.SiteStats) types.TotalsForAllSites { var totals types.TotalsForAllSites diff --git a/go-app/internal/env/environment.go b/go-app/internal/env/environment.go new file mode 100644 index 0000000..af34fe7 --- /dev/null +++ b/go-app/internal/env/environment.go @@ -0,0 +1,44 @@ +package env + +import ( + "github.com/rs/zerolog/log" + "os" + "strings" + "sync" +) + +// Singleton-pattern to ensure the environment is set only once +var ( + once sync.Once + environment string +) + +// GetEnv is a singleton function that returns an Environment. +// It ensures that only one instance is created. +func GetEnv() string { + once.Do(func() { + envi := strings.ToUpper(os.Getenv("ENVIRONMENT")) + switch { + case strings.HasPrefix(envi, "DEV"): + { + envi = "DEV" + log.Info().Msg("Running on localhost") + } + case strings.HasPrefix(envi, "TEST"): + { + envi = "TEST" + log.Info().Msg("Running as TEST on localhost") + } + case strings.HasPrefix(envi, "PROD"): + { + envi = "PROD" + log.Info().Msg("Running on fly.io") + } + default: + envi = "DEV" // Default to Development + log.Info().Msg("No ENVIRONMENT set, presumably running on localhost") + } + environment = envi + }) + return environment +} diff --git a/go-app/internal/github/issuesAndBugs.go b/go-app/internal/github/issuesAndBugs.go index bc87d12..1891e1d 100644 --- a/go-app/internal/github/issuesAndBugs.go +++ b/go-app/internal/github/issuesAndBugs.go @@ -8,12 +8,21 @@ import ( "golang.org/x/net/context" "golang.org/x/oauth2" "os" + "time" ) const GithubArc42URL = "https://github.com/arc42/" const GITHUB_GRAPHQL_API_KEY_NAME = "GITHUB_API_KEY" +// GitHubQueryInterval determines how many minutes to minimally wait prior to calling the external API again +// currently set to 1 minute +const GitHubQueryInterval = time.Minute + +// gitHubLastTimeCalled contains the time we called the public GitHub API the last time. +// Initially, it is set to Jan 1st 2004 - the approximate date arc42 was created. +var gitHubLastTimeCalled = time.Date(2004, time.January, 1, 0, 0, 0, 0, time.UTC) + // Define the query structs, // using JSON GraphQL "struct-tags": // for an explanation, see here: https://www.digitalocean.com/community/tutorials/how-to-use-struct-tags-in-go @@ -25,7 +34,7 @@ type BugsIssuesQuery struct { } `graphql:"issues(states:OPEN)"` Bugs struct { TotalCount githubv4.Int - } `graphql:"bugs: issues(states:OPEN, labels:[\"BUG\"])"` + } `graphql:"bugs: issues(states:OPEN, labels:[\"BUG\", \"BUGS\"])"` } `graphql:"repository(owner: $owner, name: $repo)"` } @@ -66,6 +75,9 @@ func StatsForRepo(thisSite string, stats *types.RepoStats) { stats.NrOfOpenBugs = int(query.Repository.Bugs.TotalCount) stats.NrOfOpenIssues = int(query.Repository.Issues.TotalCount) + // reset timer + gitHubLastTimeCalled = time.Now() + log.Debug().Msgf("%s has %d open issues and %d bugs", thisSite, stats.NrOfOpenIssues, stats.NrOfOpenBugs) } diff --git a/go-app/main.go b/go-app/main.go index cffc406..25fa60b 100644 --- a/go-app/main.go +++ b/go-app/main.go @@ -4,18 +4,19 @@ import ( "arc42-status/internal/api" "arc42-status/internal/database" "arc42-status/internal/domain" + "arc42-status/internal/env" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "os" - "strings" - "sync" "time" ) -const AppVersion = "0.5.1" +const AppVersion = "0.5.3" // version history // 0.5.x rate limit: limit amount of queries to external APIs +// 0.5.2: distinct env package, distinct DB for DEV, handle OPTIONS request +// 0.5.3: BUG and BUGS are both recognized // 0.4.7 replace most inline styles by css // 0.4.6 sortable table (a: initial, b...e: fix layout issues), f: fix #94 // 0.4.5 fix missing separators in large numbers @@ -72,42 +73,6 @@ func init() { log.Info().Msgf("log level set to %s", loglevel) } -// Singleton-pattern to ensure the environment is set only once -var ( - once sync.Once - environment string -) - -// GetEnv is a singleton function that returns an Environment. -// It ensures that only one instance is created. -func GetEnv() string { - once.Do(func() { - envi := strings.ToUpper(os.Getenv("ENVIRONMENT")) - switch { - case strings.HasPrefix(envi, "DEV"): - { - envi = "DEV" - log.Info().Msg("Running on localhost") - } - case strings.HasPrefix(envi, "TEST"): - { - envi = "TEST" - log.Info().Msg("Running as TEST on localhost") - } - case strings.HasPrefix(envi, "PROD"): - { - envi = "PROD" - log.Info().Msg("Running on fly.io") - } - default: - envi = "DEV" // Default to Development - log.Info().Msg("No ENVIRONMENT set, presumably running on localhost") - } - environment = envi - }) - return environment -} - func main() { // As the main package cannot be imported, constants defined here // cannot directly be used in internal/* packages. @@ -115,7 +80,7 @@ func main() { domain.SetAppVersion(AppVersion) // Save the startup metadata persistently, see ADR-0012 - database.SaveStartupTime(time.Now(), AppVersion, GetEnv()) + database.SaveStartupTime(time.Now(), AppVersion, env.GetEnv()) // Start a server which runs in the background, and waits for http requests // to arrive at predefined routes.