Commit 26241d5b authored by Sg912's avatar Sg912
Browse files

Merge remote-tracking branch 'upstream/main' into main

parents fec603b7 3e69addc
*This page is a work in progress.*
Unique Devices is a public API developed and maintained by the Wikimedia Foundation that serves analytical
data about number of unique devices of Wikipedia and its sister projects.
\ No newline at end of file
/*
* Copyright 2021 Nikki Nikkhoui <nnikkhoui@wikimedia.org>, Eric Evans <eevans@wikimedia.org>,
* and Wikimedia Foundation
* Copyright 2022 Wikimedia Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"termsOfService": "https://wikimediafoundation.org/wiki/Terms_of_Use",
"contact": {},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/unique-devices/{project}/{accessSite}/{granularity}/{start}/{end}": {
"get": {
"description": "Given a wiki page and a date range, returns number of unique devices that visited that page.",
"produces": [
"application/json"
],
"summary": "Number of unique devices.",
"parameters": [
{
"type": "string",
"example": "en.wikipedia.org",
"description": "Domain of a Wikimedia project",
"name": "project",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.UniqueDevicesResponse"
}
}
}
}
}
},
"definitions": {
"main.UniqueDevices": {
"type": "object",
"properties": {
"access-site": {
"type": "string"
},
"devices": {
"type": "integer"
},
"granularity": {
"type": "string"
},
"offset": {
"type": "integer"
},
"project": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"underestimate": {
"type": "integer"
}
}
},
"main.UniqueDevicesResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/main.UniqueDevices"
}
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "DRAFT",
Host: "localhost:8080",
BasePath: "/metrics/unique-devices/",
Schemes: []string{"http"},
Title: "Wikimedia UniqueDevices API",
Description: "*This page is a work in progress.*\n\nUnique Devices is a public API developed and maintained by the Wikimedia Foundation that serves analytical\ndata about number of unique devices of Wikipedia and its sister projects. ",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
{
"schemes": [
"http"
],
"swagger": "2.0",
"info": {
"description": "*This page is a work in progress.*\n\nUnique Devices is a public API developed and maintained by the Wikimedia Foundation that serves analytical\ndata about number of unique devices of Wikipedia and its sister projects. ",
"title": "Wikimedia UniqueDevices API",
"termsOfService": "https://wikimediafoundation.org/wiki/Terms_of_Use",
"contact": {},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "DRAFT"
},
"host": "localhost:8080",
"basePath": "/metrics/unique-devices/",
"paths": {
"/unique-devices/{project}/{accessSite}/{granularity}/{start}/{end}": {
"get": {
"description": "Given a wiki page and a date range, returns number of unique devices that visited that page.",
"produces": [
"application/json"
],
"summary": "Number of unique devices.",
"parameters": [
{
"type": "string",
"example": "en.wikipedia.org",
"description": "Domain of a Wikimedia project",
"name": "project",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.UniqueDevicesResponse"
}
}
}
}
}
},
"definitions": {
"main.UniqueDevices": {
"type": "object",
"properties": {
"access-site": {
"type": "string"
},
"devices": {
"type": "integer"
},
"granularity": {
"type": "string"
},
"offset": {
"type": "integer"
},
"project": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"underestimate": {
"type": "integer"
}
}
},
"main.UniqueDevicesResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/main.UniqueDevices"
}
}
}
}
}
}
\ No newline at end of file
basePath: /metrics/unique-devices/
definitions:
main.UniqueDevices:
properties:
access-site:
type: string
devices:
type: integer
granularity:
type: string
offset:
type: integer
project:
type: string
timestamp:
type: string
underestimate:
type: integer
type: object
main.UniqueDevicesResponse:
properties:
items:
items:
$ref: '#/definitions/main.UniqueDevices'
type: array
type: object
host: localhost:8080
info:
contact: {}
description: "*This page is a work in progress.*\n\nUnique Devices is a public API
developed and maintained by the Wikimedia Foundation that serves analytical\ndata
about number of unique devices of Wikipedia and its sister projects. "
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
termsOfService: https://wikimediafoundation.org/wiki/Terms_of_Use
title: Wikimedia UniqueDevices API
version: DRAFT
paths:
/unique-devices/{project}/{accessSite}/{granularity}/{start}/{end}:
get:
description: Given a wiki page and a date range, returns number of unique devices
that visited that page.
parameters:
- description: Domain of a Wikimedia project
example: en.wikipedia.org
in: path
name: project
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.UniqueDevicesResponse'
summary: Number of unique devices.
schemes:
- http
swagger: "2.0"
/*
* Copyright 2021 Nikki Nikkhoui <nnikkhoui@wikimedia.org> and Wikimedia Foundation
* Copyright 2022 Wikimedia Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......
......@@ -8,13 +8,14 @@ import (
"gerrit.wikimedia.org/r/mediawiki/services/servicelib-golang/logger"
"github.com/gocql/gocql"
"github.com/valyala/fasthttp"
"gitlab.wikimedia.org/frankie/aqsassist"
"schneider.vip/problem"
)
type UniqueDevicesLogic struct {
}
func (s *UniqueDevicesLogic) ProcessUniqueDevicesLogic(context context.Context, ctx *fasthttp.RequestCtx, project, accessSite, granularity, start, end string, session *gocql.Session, rLogger *logger.Logger) (*problem.Problem, entities.UniqueDevicesResponse, int) {
func (s *UniqueDevicesLogic) ProcessUniqueDevicesLogic(context context.Context, ctx *fasthttp.RequestCtx, project, accessSite, granularity, start, end string, session *gocql.Session, rLogger *logger.Logger) (*problem.Problem, entities.UniqueDevicesResponse) {
var err error
var problemData *problem.Problem
var response = entities.UniqueDevicesResponse{Items: make([]entities.UniqueDevices, 0)}
......@@ -26,15 +27,10 @@ func (s *UniqueDevicesLogic) ProcessUniqueDevicesLogic(context context.Context,
for scanner.Next() {
if err = scanner.Scan(&devices, &offset, &underestimate, &timestamp); err != nil {
rLogger.Log(logger.ERROR, "Query failed: %s", err)
return problem.New(
problem.Type("about:blank"),
problem.Title(http.StatusText(http.StatusInternalServerError)),
problem.Custom("method", http.MethodGet),
problem.Status(http.StatusInternalServerError),
problem.Detail(err.Error()),
problem.Custom("uri", ctx.Request.URI().RequestURI())), entities.UniqueDevicesResponse{}, http.StatusInternalServerError
problemResp := aqsassist.CreateProblem(http.StatusInternalServerError, err.Error(), string(ctx.Request.URI().RequestURI())).JSON()
ctx.SetStatusCode(http.StatusInternalServerError)
ctx.SetBody(problemResp)
}
response.Items = append(response.Items, entities.UniqueDevices{
Project: project,
AccessSite: accessSite,
......@@ -48,23 +44,17 @@ func (s *UniqueDevicesLogic) ProcessUniqueDevicesLogic(context context.Context,
str := "The date(s) you used are valid, but we either do not have data for those date(s), or the project you asked for is not loaded yet. Please check documentation for more information."
if len(response.Items) == 0 {
return problem.New(
problem.Type("about:blank"),
problem.Title(http.StatusText(http.StatusNotFound)),
problem.Custom("method", http.MethodGet),
problem.Detail(str),
problem.Custom("uri", ctx.Request.URI().RequestURI())), entities.UniqueDevicesResponse{}, 0
problemResp := aqsassist.CreateProblem(http.StatusNotFound, str, string(ctx.Request.URI().RequestURI()))
ctx.SetStatusCode(http.StatusNotFound)
ctx.SetBody(problemResp.JSON())
return problemResp, entities.UniqueDevicesResponse{}
}
if err := scanner.Err(); err != nil {
//s.logger.Request(r).Log(logger.ERROR, "Error querying database: %s", err)
return (problem.New(
problem.Type("about:blank"),
problem.Title(http.StatusText(http.StatusInternalServerError)),
problem.Custom("method", http.MethodGet),
problem.Status(http.StatusInternalServerError),
problem.Detail(err.Error()),
problem.Custom("uri", ctx.Request.URI().RequestURI()))), entities.UniqueDevicesResponse{}, 0
problemResp := aqsassist.CreateProblem(http.StatusInternalServerError, err.Error(), string(ctx.Request.URI().RequestURI()))
ctx.SetStatusCode(http.StatusInternalServerError)
ctx.SetBody(problemResp.JSON())
return problemResp, entities.UniqueDevicesResponse{}
}
return problemData, response, 0
return problemData, response
}
/*
* Copyright 2021 Nikki Nikkhoui <nnikkhoui@wikimedia.org> and Wikimedia Foundation
* Copyright 2022 Wikimedia Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -38,6 +38,20 @@ var (
version = "unknown"
)
// API documentation
// @title Wikimedia UniqueDevices API
// @version DRAFT
// @description.markdown api.md
// @contact.name
// @contact.url
// @contact.email
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @termsOfService https://wikimediafoundation.org/wiki/Terms_of_Use
// @host localhost:8080
// @basePath /metrics/unique-devices/
// @schemes http
// Entrypoint for the service
func main() {
var confFile = flag.String("config", "./config.yaml", "Path to the configuration file")
......@@ -94,5 +108,6 @@ func main() {
r.GET(path.Join(config.BaseURI, "/{project}/{access-site}/{granularity}/{start}/{end}"), midAccessGroup(uniqueDevicesHandler.HandleFastHTTP))
fasthttp.ListenAndServe(fmt.Sprintf("%s:%d", config.Address, config.Port), r.Handler)
err = fasthttp.ListenAndServe(fmt.Sprintf("%s:%d", config.Address, config.Port), r.Handler)
logger.Info(err.Error())
}
......@@ -12,7 +12,6 @@ import (
"github.com/gocql/gocql"
"github.com/valyala/fasthttp"
"gitlab.wikimedia.org/frankie/aqsassist"
"schneider.vip/problem"
)
// UniqueDevicesHandler is the HTTP handler for unique devices API requests.
......@@ -31,64 +30,46 @@ func (s *UniqueDevicesHandler) HandleFastHTTP(ctx *fasthttp.RequestCtx) {
var start, end string
if granularity != "daily" && granularity != "monthly" && granularity != "hourly" {
problemResp, _ := json.Marshal(problem.New(
problem.Type("about:blank"),
problem.Title(http.StatusText(http.StatusBadRequest)),
problem.Custom("method", http.MethodGet),
problem.Status(http.StatusBadRequest),
problem.Detail("Invalid granularity"),
problem.Custom("uri", ctx.Request.URI().RequestURI())))
ctx.SetBody(problemResp)
problemResp := aqsassist.CreateProblem(http.StatusBadRequest, "Invalid granularity", string(ctx.Request.URI().RequestURI())).JSON()
ctx.SetStatusCode(http.StatusBadRequest)
ctx.SetBody(problemResp)
return
}
if start, err = aqsassist.ValidateTimestamp(ctx.UserValue("start").(string)); err != nil {
problemResp, _ := json.Marshal(problem.New(
problem.Type("about:blank"),
problem.Title(http.StatusText(http.StatusBadRequest)),
problem.Custom("method", http.MethodGet),
problem.Status(http.StatusBadRequest),
problem.Detail("Invalid timestamp"),
problem.Custom("uri", ctx.Request.URI().RequestURI())))
ctx.SetBody(problemResp)
problemResp := aqsassist.CreateProblem(http.StatusBadRequest, "start timestamp is invalid, must be a valid date in YYYYMMDD format", string(ctx.Request.URI().RequestURI())).JSON()
ctx.SetStatusCode(http.StatusBadRequest)
ctx.SetBody(problemResp)
return
}
if end, err = aqsassist.ValidateTimestamp(ctx.UserValue("end").(string)); err != nil {
problemResp, _ := json.Marshal(problem.New(
problem.Type("about:blank"),
problem.Title(http.StatusText(http.StatusBadRequest)),
problem.Custom("method", http.MethodGet),
problem.Status(http.StatusBadRequest),
problem.Detail("Invalid timestamp"),
problem.Custom("uri", ctx.Request.URI().RequestURI())))
problemResp := aqsassist.CreateProblem(http.StatusBadRequest, "end timestamp is invalid, must be a valid date in YYYYMMDD format", string(ctx.Request.URI().RequestURI())).JSON()
ctx.SetStatusCode(http.StatusBadRequest)
ctx.SetBody(problemResp)
return
}
if err = aqsassist.StartBeforeEnd(start, end); err != nil {
problemResp := aqsassist.CreateProblem(http.StatusBadRequest, err.Error(), string(ctx.Request.URI().RequestURI())).JSON()
ctx.SetStatusCode(http.StatusBadRequest)
ctx.SetBody(problemResp)
return
}
c, _ := context.WithTimeout(ctx, 40*time.Millisecond)
pbm, response, pbmStatus := s.logic.ProcessUniqueDevicesLogic(c, ctx, project, accessSite, granularity, start, end, s.session, s.logger)
pbm, response := s.logic.ProcessUniqueDevicesLogic(c, ctx, project, accessSite, granularity, start, end, s.session, s.logger)
if pbm != nil {
problemResp, _ := json.Marshal(pbm)
ctx.SetBody(problemResp)
ctx.SetStatusCode(pbmStatus)
return
}
var data []byte
if data, err = json.MarshalIndent(response, "", " "); err != nil {
s.logger.Log(logger.ERROR, "Unable to marshal response object: %s", err)
problemResp, _ := json.Marshal(problem.New(
problem.Type("about:blank"),
problem.Title(http.StatusText(http.StatusInternalServerError)),
problem.Custom("method", http.MethodGet),
problem.Status(http.StatusInternalServerError),
problem.Detail(err.Error()),
problem.Custom("uri", ctx.Request.URI().RequestURI())))
ctx.SetBody(problemResp)
problemResp := aqsassist.CreateProblem(http.StatusInternalServerError, err.Error(), string(ctx.Request.URI().RequestURI())).JSON()
ctx.SetStatusCode(http.StatusInternalServerError)
ctx.SetBody(problemResp)
return
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment