Skip to content

Commit d55ae39

Browse files
committed
godev: create godev submodule and content package
The godev submodule contains code necessary to run the telemetry services we plan to host on GCP. The submodule structure will keep packages that rely on third party dependencies separate from the telemetry collection code which may eventually be depended on by gopls and the go command. The content package implements a basic web serving framework with support for markdown, go templates, and typescript files. Change-Id: If3da206d858bd969a681d29d1d9ad829b31211da Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/495755 Reviewed-by: Hyang-Ah Hana Kim <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Jamal Carvalho <[email protected]>
1 parent 2be5042 commit d55ae39

36 files changed

+1045
-0
lines changed

doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package telemetry

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module golang.org/x/telemetry
2+
3+
go 1.20

go.sum

Whitespace-only changes.

godev/go.mod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module golang.org/x/telemetry/godev
2+
3+
go 1.20
4+
5+
require (
6+
github.com/evanw/esbuild v0.17.19
7+
github.com/google/go-cmp v0.5.9
8+
github.com/yuin/goldmark v1.5.4
9+
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
10+
)
11+
12+
require gopkg.in/yaml.v2 v2.4.0 // indirect
13+
14+
require (
15+
github.com/yuin/goldmark-meta v1.1.0
16+
golang.org/x/sys v0.8.0 // indirect
17+
)

godev/go.sum

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
github.com/evanw/esbuild v0.17.19 h1:JdzNCvfFEoUCXKHhdP326Vn2mhCu8PybXeBDHaSRyWo=
2+
github.com/evanw/esbuild v0.17.19/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
3+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
4+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
5+
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
6+
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
7+
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
8+
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
9+
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
10+
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
11+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
12+
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
13+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
14+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
15+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
16+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
17+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

godev/internal/content/content.go

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package content implements a basic web serving framework.
6+
//
7+
// # Content Server
8+
//
9+
// A content server is an http.Handler that serves requests from a file system.
10+
// Use Server(fsys) to create a new content server.
11+
//
12+
// The server is defined primarily by the content of its file system fsys,
13+
// which holds files to be served. It renders markdown files and golang
14+
// templates into HTML and transforms TypeScript into JavaScript.
15+
//
16+
// # Page Rendering
17+
//
18+
// A request for a path like "/page" will search the file system for
19+
// "page.md", "page.html", "page/index.md", and "page/index.html" and
20+
// render HTML output for the first file found.
21+
//
22+
// Partial templates with the extension ".tmpl" at the root of the file system
23+
// and in the same directory as the requested page are included in the
24+
// html/template execution step to allow for sharing and composing logic from
25+
// multiple templates.
26+
package content
27+
28+
import (
29+
"bytes"
30+
"encoding/json"
31+
"errors"
32+
"fmt"
33+
"html/template"
34+
"io/fs"
35+
"net/http"
36+
"path"
37+
"strconv"
38+
"strings"
39+
40+
"github.com/yuin/goldmark"
41+
meta "github.com/yuin/goldmark-meta"
42+
"github.com/yuin/goldmark/extension"
43+
"github.com/yuin/goldmark/parser"
44+
"github.com/yuin/goldmark/renderer/html"
45+
"golang.org/x/exp/slog"
46+
)
47+
48+
// contentServer serves requests for a given file system. It can also render
49+
// templates and transform TypeScript into JavaScript.
50+
type contentServer struct {
51+
fsys fs.FS
52+
fserv http.Handler
53+
handlers map[string]handlerFunc
54+
}
55+
56+
type handler struct {
57+
path string
58+
fn handlerFunc
59+
}
60+
61+
type handlerFunc func(http.ResponseWriter, *http.Request, fs.FS) error
62+
63+
// Server returns a handler that serves HTTP requests with the contents
64+
// of the file system rooted at fsys. For requests to a path without an
65+
// extension, the server will search fsys for markdown or html templates
66+
// first by appending .md, .html, /index.md, and /index.html to the
67+
// requested url path.
68+
//
69+
// The default behavior of looking for templates within fsys can be overriden
70+
// by using an optional set of content handlers.
71+
//
72+
// For example, a server can be constructed for a file system with a single
73+
// template, “index.html“, in a directory, “content“, and a handler:
74+
//
75+
// s := content.Server(os.DirFS("content"),
76+
// content.Handler("/", func(w http.ReponseWriter, _ *http.Request, fsys fs.FS) error {
77+
// return content.Template(w, fsys, "index.html", nil, http.StatusOK)
78+
// }))
79+
//
80+
// or without a handler:
81+
//
82+
// content.Server(os.DirFS("content"))
83+
//
84+
// Both examples will render the template index.html for requests to "/".
85+
func Server(fsys fs.FS, handlers ...*handler) http.Handler {
86+
fserv := http.FileServer(http.FS(fsys))
87+
hs := make(map[string]handlerFunc)
88+
for _, h := range handlers {
89+
if _, ok := hs[h.path]; ok {
90+
panic("multiple registrations for " + h.path)
91+
}
92+
hs[h.path] = h.fn
93+
}
94+
return &contentServer{fsys, fserv, hs}
95+
}
96+
97+
func Handler(path string, h handlerFunc) *handler {
98+
return &handler{path, h}
99+
}
100+
101+
func (c *contentServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
102+
if len(r.URL.Path) > 255 {
103+
Error(w, r, errors.New("url too long"), http.StatusBadRequest)
104+
return
105+
}
106+
107+
if handler, ok := c.handlers[r.URL.Path]; ok {
108+
err := handler(w, r, c.fsys)
109+
if err != nil {
110+
Error(w, r, err, http.StatusInternalServerError)
111+
}
112+
return
113+
}
114+
115+
ext := path.Ext(r.URL.Path)
116+
if ext == ".md" || ext == ".html" {
117+
http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, ext), http.StatusMovedPermanently)
118+
return
119+
}
120+
121+
filepath, info, err := stat(c.fsys, r.URL.Path)
122+
if errors.Is(err, fs.ErrNotExist) {
123+
Error(w, r, errors.New("page not found"), http.StatusNotFound)
124+
return
125+
}
126+
if err == nil {
127+
if strings.HasSuffix(r.URL.Path, "/index") {
128+
http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, "/index"), http.StatusMovedPermanently)
129+
return
130+
}
131+
switch path.Ext(filepath) {
132+
case ".html":
133+
err = Template(w, c.fsys, filepath, nil, http.StatusOK)
134+
case ".md":
135+
err = markdown(w, c.fsys, filepath, http.StatusOK)
136+
case ".ts":
137+
err = script(w, c.fsys, filepath, info)
138+
default:
139+
c.fserv.ServeHTTP(w, r)
140+
}
141+
}
142+
if err != nil {
143+
Error(w, r, err, http.StatusInternalServerError)
144+
}
145+
}
146+
147+
// Template executes a template response.
148+
func Template(w http.ResponseWriter, fsys fs.FS, tmplPath string, data any, code int) error {
149+
patterns, err := tmplPatterns(fsys, tmplPath)
150+
if err != nil {
151+
return err
152+
}
153+
patterns = append(patterns, tmplPath)
154+
tmpl, err := template.ParseFS(fsys, patterns...)
155+
if err != nil {
156+
return err
157+
}
158+
name := path.Base(tmplPath)
159+
var buf bytes.Buffer
160+
if err := tmpl.ExecuteTemplate(&buf, name, data); err != nil {
161+
return err
162+
}
163+
if code != 0 {
164+
w.WriteHeader(code)
165+
}
166+
w.Header().Set("Content-Type", "text/html")
167+
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
168+
if _, err := w.Write(buf.Bytes()); err != nil {
169+
return err
170+
}
171+
return nil
172+
}
173+
174+
// JSON encodes data as JSON response with a status code.
175+
func JSON(w http.ResponseWriter, data any, code int) error {
176+
var buf bytes.Buffer
177+
if err := json.NewEncoder(&buf).Encode(data); err != nil {
178+
return err
179+
}
180+
if code != 0 {
181+
w.WriteHeader(code)
182+
}
183+
w.Header().Set("Content-Type", "application/json")
184+
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
185+
if _, err := w.Write(buf.Bytes()); err != nil {
186+
return err
187+
}
188+
return nil
189+
}
190+
191+
// Text formats data as a text response with a status code.
192+
func Text(w http.ResponseWriter, data any, code int) error {
193+
var buf bytes.Buffer
194+
if _, err := fmt.Fprint(&buf, data); err != nil {
195+
return err
196+
}
197+
if code != 0 {
198+
w.WriteHeader(code)
199+
}
200+
w.Header().Set("Content-Type", "text/plain")
201+
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
202+
if _, err := w.Write(buf.Bytes()); err != nil {
203+
return err
204+
}
205+
return nil
206+
}
207+
208+
// Error writes an error as an HTTP response with a status code.
209+
func Error(w http.ResponseWriter, req *http.Request, err error, code int) {
210+
if code == http.StatusInternalServerError {
211+
http.Error(w, http.StatusText(http.StatusInternalServerError), code)
212+
} else {
213+
http.Error(w, err.Error(), code)
214+
}
215+
slog.Error("request error",
216+
slog.String("method", req.Method),
217+
slog.String("uri", req.RequestURI),
218+
slog.Int("status", code),
219+
slog.String("error", err.Error()),
220+
)
221+
}
222+
223+
// markdown renders a markdown template as html.
224+
func markdown(w http.ResponseWriter, fsys fs.FS, tmplPath string, code int) error {
225+
markdown, err := fs.ReadFile(fsys, tmplPath)
226+
if err != nil {
227+
return err
228+
}
229+
md := goldmark.New(
230+
goldmark.WithParserOptions(
231+
parser.WithHeadingAttribute(),
232+
parser.WithAutoHeadingID(),
233+
),
234+
goldmark.WithRendererOptions(
235+
html.WithUnsafe(),
236+
html.WithXHTML(),
237+
),
238+
goldmark.WithExtensions(
239+
extension.GFM,
240+
extension.NewTypographer(),
241+
meta.Meta,
242+
),
243+
)
244+
var content bytes.Buffer
245+
ctx := parser.NewContext()
246+
if err := md.Convert(markdown, &content, parser.WithContext(ctx)); err != nil {
247+
return err
248+
}
249+
data := meta.Get(ctx)
250+
if data == nil {
251+
data = map[string]interface{}{}
252+
}
253+
data["Content"] = template.HTML(content.String())
254+
if _, ok := data["Template"]; !ok {
255+
data["Template"] = "base.html"
256+
}
257+
return Template(w, fsys, data["Template"].(string), data, code)
258+
}
259+
260+
// script serves TypeScript code tranformed into JavaScript.
261+
func script(w http.ResponseWriter, fsys fs.FS, filepath string, info fs.FileInfo) error {
262+
data, err := fs.ReadFile(fsys, filepath)
263+
if err != nil {
264+
return err
265+
}
266+
output := esbuild(data)
267+
w.Header().Set("Content-Type", "text/javascript")
268+
w.Header().Set("Content-Length", strconv.Itoa(output.Len()))
269+
w.Header().Set("Last-Modified", info.ModTime().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
270+
if _, err := w.Write(output.Bytes()); err != nil {
271+
return err
272+
}
273+
return nil
274+
}
275+
276+
// stat trys to coerce a urlPath into an openable file then returns the
277+
// file path and file info.
278+
func stat(fsys fs.FS, urlPath string) (string, fs.FileInfo, error) {
279+
cleanPath := path.Clean(strings.TrimPrefix(urlPath, "/"))
280+
ext := path.Ext(cleanPath)
281+
filePaths := []string{cleanPath}
282+
if ext == "" || ext == "." {
283+
md := cleanPath + ".md"
284+
html := cleanPath + ".html"
285+
indexMD := path.Join(cleanPath, "index.md")
286+
indexHTML := path.Join(cleanPath, "index.html")
287+
filePaths = []string{md, html, indexMD, indexHTML, cleanPath}
288+
}
289+
var p string
290+
var stat fs.FileInfo
291+
var err error
292+
for _, p = range filePaths {
293+
if stat, err = fs.Stat(fsys, p); err == nil {
294+
break
295+
}
296+
}
297+
return p, stat, err
298+
}
299+
300+
// tmplPatters generates a slice of file patterns to use in template.ParseFS.
301+
func tmplPatterns(fsys fs.FS, tmplPath string) ([]string, error) {
302+
var patterns []string
303+
globs := []string{"*.tmpl", path.Join(path.Dir(tmplPath), "*.tmpl")}
304+
for _, g := range globs {
305+
matches, err := fs.Glob(fsys, g)
306+
if err != nil {
307+
return nil, err
308+
}
309+
patterns = append(patterns, matches...)
310+
}
311+
return patterns, nil
312+
}

0 commit comments

Comments
 (0)