├── .dockerignore ├── .github └── workflows │ └── push.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── dashboard ├── Github Traffic-1631445056230.json └── screenshot.png ├── docker-compose.yaml ├── github-traffic.py ├── prometheus └── prometheus.yaml └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | .git -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "push" 3 | 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | build: 11 | name: "Build" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check Out Repo 15 | uses: actions/checkout@v2 16 | 17 | - name: "Setup buildx" 18 | uses: docker/setup-buildx-action@v1 19 | 20 | - name: "Login into ghcr" 21 | uses: docker/login-action@v1 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: "Build image" 28 | uses: docker/build-push-action@v2 29 | with: 30 | context: . 31 | push: true 32 | file: Dockerfile 33 | tags: ghcr.io/grafana/github-traffic:latest,ghcr.io/grafana/github-traffic:${{ github.event.release.tag_name }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt requirements.txt 6 | RUN pip3 install -r requirements.txt 7 | 8 | COPY github-traffic.py /app/github-traffic.py 9 | 10 | CMD [ "python3", "github-traffic.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-traffic 2 | Github Traffic collects your repository's traffic data and exposes it as Prometheus metrics. 3 | 4 | ![Grafana dashboard](dashboard/screenshot.png) 5 | *Grafana dashboard that displays the metrics generated by Github Traffic.* 6 | 7 | ## Quickstart 8 | 9 | Requirements: 10 | - Docker >= 20.10.3 11 | 12 | To run **github-traffic** locally you've to create a `.env` file like this one: 13 | 14 | ```sh 15 | $ cat .env 16 | # Required 17 | GITHUB_TOKEN=your-github-token-goes-here 18 | ORG_NAME=the-name-of-your-organization-goes-here 19 | # Optional 20 | REPO_TYPE=public-or-private # Default: public 21 | REPO_NAME_CONTAINS=string-to-match-repositories-with # Default: "" 22 | CRONTAB_SCHEDULE=crontab-schedule-to-get-data-from-github # Default: "0 * * * *" 23 | ``` 24 | 25 | **_NOTE:_** You also can use your username if you don't belong to any organization. In this case, use "USER_NAME" instead of "ORG_NAME". 26 | 27 | Run the image: 28 | ``` 29 | $ docker run --env-file .env -it -p 8001:8001 ghcr.io/grafana/github-traffic 30 | level=INFO msg="Github traffic is running!" 31 | level=INFO msg="Gather insights" repo="k6" views=163 unique_views=90 clones=406 unique_clones=109 stars=13805 32 | level=INFO msg="Gather insights" repo="postman-to-k6" views=3 unique_views=2 clones=1 unique_clones=1 stars=238 33 | level=INFO msg="Gather insights" repo="jmeter-to-k6" views=1 unique_views=1 clones=2 unique_clones=2 stars=44 34 | ... 35 | Go to http://localhost:8001/metrics 36 | ``` 37 | Profit! 38 | 39 | Now you can collect those metrics as you would do with any other service. To visualize them, we provide an example/template Grafana dashboard: https://grafana.com/grafana/dashboards/15000 40 | 41 | ## Docker compose 42 | In addition, a docker-compose.yaml file is provided. This file also run Prometheus and Grafana. 43 | 44 | **_NOTE:_** You've to create the .env file with you configuration. 45 | 46 | Run compose: 47 | ```zsh 48 | ❯ docker-compose up -d 49 | Creating network "github-traffic_default" with the default driver 50 | Creating github-traffic_traffic_1 ... done 51 | Creating github-traffic_prometheus_1 ... done 52 | Creating github-traffic_grafana_1 ... done 53 | ``` 54 | 55 | Now, you can access to: 56 | * Grafana: http://localhost:3000 57 | * Prometheus: http://localhost:9090 58 | * Github traffic: http://localhost:8001 59 | 60 | Once you navigate to Grafana (http://localhost:3000), the user and password are admin/admin you have to configure a datasource to Prometheus (http://prometheus:9090) and import the dashboard with id 15000. 61 | 62 | Remove containers: 63 | ```zsh 64 | ❯ docker-compose down 65 | Stopping github-traffic_grafana_1 ... done 66 | Stopping github-traffic_prometheus_1 ... done 67 | Stopping github-traffic_traffic_1 ... done 68 | Removing github-traffic_grafana_1 ... done 69 | Removing github-traffic_prometheus_1 ... done 70 | Removing github-traffic_traffic_1 ... done 71 | Removing network github-traffic_default 72 | ``` 73 | -------------------------------------------------------------------------------- /dashboard/Github Traffic-1631445056230.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_GRAFANACLOUD-K6CLOUD-PROM", 5 | "label": "grafanacloud-k6cloud-prom", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "grafana", 15 | "id": "grafana", 16 | "name": "Grafana", 17 | "version": "8.1.2" 18 | }, 19 | { 20 | "type": "panel", 21 | "id": "graph", 22 | "name": "Graph (old)", 23 | "version": "" 24 | }, 25 | { 26 | "type": "datasource", 27 | "id": "prometheus", 28 | "name": "Prometheus", 29 | "version": "1.0.0" 30 | } 31 | ], 32 | "annotations": { 33 | "list": [ 34 | { 35 | "builtIn": 1, 36 | "datasource": "-- Grafana --", 37 | "enable": true, 38 | "hide": true, 39 | "iconColor": "rgba(0, 211, 255, 1)", 40 | "name": "Annotations & Alerts", 41 | "target": { 42 | "limit": 100, 43 | "matchAny": false, 44 | "tags": [], 45 | "type": "dashboard" 46 | }, 47 | "type": "dashboard" 48 | } 49 | ] 50 | }, 51 | "editable": true, 52 | "gnetId": null, 53 | "graphTooltip": 0, 54 | "id": null, 55 | "links": [], 56 | "panels": [ 57 | { 58 | "aliasColors": {}, 59 | "bars": false, 60 | "dashLength": 10, 61 | "dashes": false, 62 | "datasource": "${DS_GRAFANACLOUD-K6CLOUD-PROM}", 63 | "fill": 1, 64 | "fillGradient": 0, 65 | "gridPos": { 66 | "h": 6, 67 | "w": 24, 68 | "x": 0, 69 | "y": 0 70 | }, 71 | "hiddenSeries": false, 72 | "id": 6, 73 | "legend": { 74 | "alignAsTable": true, 75 | "avg": false, 76 | "current": true, 77 | "max": false, 78 | "min": false, 79 | "rightSide": true, 80 | "show": true, 81 | "sort": "current", 82 | "sortDesc": true, 83 | "total": false, 84 | "values": true 85 | }, 86 | "lines": true, 87 | "linewidth": 1, 88 | "nullPointMode": "null", 89 | "options": { 90 | "alertThreshold": true 91 | }, 92 | "percentage": false, 93 | "pluginVersion": "8.1.2", 94 | "pointradius": 2, 95 | "points": false, 96 | "renderer": "flot", 97 | "seriesOverrides": [], 98 | "spaceLength": 10, 99 | "stack": false, 100 | "steppedLine": false, 101 | "targets": [ 102 | { 103 | "exemplar": true, 104 | "expr": "avg(github_traffic_stars) by (repository)", 105 | "interval": "", 106 | "legendFormat": "{{repository}}", 107 | "refId": "A" 108 | } 109 | ], 110 | "thresholds": [], 111 | "timeFrom": "60d", 112 | "timeRegions": [], 113 | "timeShift": null, 114 | "title": "Stars", 115 | "tooltip": { 116 | "shared": true, 117 | "sort": 0, 118 | "value_type": "individual" 119 | }, 120 | "type": "graph", 121 | "xaxis": { 122 | "buckets": null, 123 | "mode": "time", 124 | "name": null, 125 | "show": true, 126 | "values": [] 127 | }, 128 | "yaxes": [ 129 | { 130 | "$$hashKey": "object:164", 131 | "format": "short", 132 | "label": null, 133 | "logBase": 1, 134 | "max": null, 135 | "min": null, 136 | "show": true 137 | }, 138 | { 139 | "$$hashKey": "object:165", 140 | "format": "short", 141 | "label": null, 142 | "logBase": 1, 143 | "max": null, 144 | "min": null, 145 | "show": true 146 | } 147 | ], 148 | "yaxis": { 149 | "align": false, 150 | "alignLevel": null 151 | } 152 | }, 153 | { 154 | "collapsed": false, 155 | "datasource": null, 156 | "gridPos": { 157 | "h": 1, 158 | "w": 24, 159 | "x": 0, 160 | "y": 6 161 | }, 162 | "id": 12, 163 | "panels": [], 164 | "title": "Views", 165 | "type": "row" 166 | }, 167 | { 168 | "aliasColors": {}, 169 | "bars": false, 170 | "dashLength": 10, 171 | "dashes": false, 172 | "datasource": "${DS_GRAFANACLOUD-K6CLOUD-PROM}", 173 | "fieldConfig": { 174 | "defaults": { 175 | "unit": "short" 176 | }, 177 | "overrides": [] 178 | }, 179 | "fill": 1, 180 | "fillGradient": 0, 181 | "gridPos": { 182 | "h": 7, 183 | "w": 12, 184 | "x": 0, 185 | "y": 7 186 | }, 187 | "hiddenSeries": false, 188 | "id": 2, 189 | "legend": { 190 | "alignAsTable": true, 191 | "avg": false, 192 | "current": true, 193 | "max": true, 194 | "min": false, 195 | "rightSide": true, 196 | "show": true, 197 | "sort": "max", 198 | "sortDesc": true, 199 | "total": false, 200 | "values": true 201 | }, 202 | "lines": true, 203 | "linewidth": 1, 204 | "nullPointMode": "null", 205 | "options": { 206 | "alertThreshold": true 207 | }, 208 | "percentage": false, 209 | "pluginVersion": "8.1.2", 210 | "pointradius": 2, 211 | "points": false, 212 | "renderer": "flot", 213 | "seriesOverrides": [], 214 | "spaceLength": 10, 215 | "stack": false, 216 | "steppedLine": false, 217 | "targets": [ 218 | { 219 | "exemplar": true, 220 | "expr": "increase(github_traffic_views[1h])", 221 | "interval": "", 222 | "legendFormat": "{{repository}}", 223 | "refId": "A" 224 | } 225 | ], 226 | "thresholds": [], 227 | "timeFrom": null, 228 | "timeRegions": [], 229 | "timeShift": null, 230 | "title": "Views", 231 | "tooltip": { 232 | "shared": true, 233 | "sort": 0, 234 | "value_type": "individual" 235 | }, 236 | "type": "graph", 237 | "xaxis": { 238 | "buckets": null, 239 | "mode": "time", 240 | "name": null, 241 | "show": true, 242 | "values": [] 243 | }, 244 | "yaxes": [ 245 | { 246 | "$$hashKey": "object:348", 247 | "format": "short", 248 | "label": null, 249 | "logBase": 1, 250 | "max": null, 251 | "min": null, 252 | "show": true 253 | }, 254 | { 255 | "$$hashKey": "object:349", 256 | "format": "short", 257 | "label": null, 258 | "logBase": 1, 259 | "max": null, 260 | "min": null, 261 | "show": true 262 | } 263 | ], 264 | "yaxis": { 265 | "align": false, 266 | "alignLevel": null 267 | } 268 | }, 269 | { 270 | "aliasColors": {}, 271 | "bars": false, 272 | "dashLength": 10, 273 | "dashes": false, 274 | "datasource": "${DS_GRAFANACLOUD-K6CLOUD-PROM}", 275 | "fill": 1, 276 | "fillGradient": 0, 277 | "gridPos": { 278 | "h": 7, 279 | "w": 12, 280 | "x": 12, 281 | "y": 7 282 | }, 283 | "hiddenSeries": false, 284 | "id": 3, 285 | "legend": { 286 | "alignAsTable": true, 287 | "avg": false, 288 | "current": true, 289 | "max": true, 290 | "min": false, 291 | "rightSide": true, 292 | "show": true, 293 | "sort": "max", 294 | "sortDesc": true, 295 | "total": false, 296 | "values": true 297 | }, 298 | "lines": true, 299 | "linewidth": 1, 300 | "nullPointMode": "null", 301 | "options": { 302 | "alertThreshold": true 303 | }, 304 | "percentage": false, 305 | "pluginVersion": "8.1.2", 306 | "pointradius": 2, 307 | "points": false, 308 | "renderer": "flot", 309 | "seriesOverrides": [], 310 | "spaceLength": 10, 311 | "stack": false, 312 | "steppedLine": false, 313 | "targets": [ 314 | { 315 | "exemplar": true, 316 | "expr": "increase(github_traffic_unique_views[1h])", 317 | "interval": "", 318 | "legendFormat": "{{repository}}", 319 | "refId": "A" 320 | } 321 | ], 322 | "thresholds": [], 323 | "timeFrom": null, 324 | "timeRegions": [], 325 | "timeShift": null, 326 | "title": "Views (unique)", 327 | "tooltip": { 328 | "shared": true, 329 | "sort": 0, 330 | "value_type": "individual" 331 | }, 332 | "type": "graph", 333 | "xaxis": { 334 | "buckets": null, 335 | "mode": "time", 336 | "name": null, 337 | "show": true, 338 | "values": [] 339 | }, 340 | "yaxes": [ 341 | { 342 | "$$hashKey": "object:524", 343 | "format": "short", 344 | "label": null, 345 | "logBase": 1, 346 | "max": null, 347 | "min": null, 348 | "show": true 349 | }, 350 | { 351 | "$$hashKey": "object:525", 352 | "format": "short", 353 | "label": null, 354 | "logBase": 1, 355 | "max": null, 356 | "min": null, 357 | "show": true 358 | } 359 | ], 360 | "yaxis": { 361 | "align": false, 362 | "alignLevel": null 363 | } 364 | }, 365 | { 366 | "collapsed": false, 367 | "datasource": null, 368 | "gridPos": { 369 | "h": 1, 370 | "w": 24, 371 | "x": 0, 372 | "y": 14 373 | }, 374 | "id": 10, 375 | "panels": [], 376 | "title": "Clones", 377 | "type": "row" 378 | }, 379 | { 380 | "aliasColors": {}, 381 | "bars": false, 382 | "dashLength": 10, 383 | "dashes": false, 384 | "datasource": "${DS_GRAFANACLOUD-K6CLOUD-PROM}", 385 | "fill": 1, 386 | "fillGradient": 0, 387 | "gridPos": { 388 | "h": 7, 389 | "w": 12, 390 | "x": 0, 391 | "y": 15 392 | }, 393 | "hiddenSeries": false, 394 | "id": 4, 395 | "legend": { 396 | "alignAsTable": true, 397 | "avg": false, 398 | "current": true, 399 | "max": true, 400 | "min": false, 401 | "rightSide": true, 402 | "show": true, 403 | "sort": "max", 404 | "sortDesc": true, 405 | "total": false, 406 | "values": true 407 | }, 408 | "lines": true, 409 | "linewidth": 1, 410 | "nullPointMode": "null", 411 | "options": { 412 | "alertThreshold": true 413 | }, 414 | "percentage": false, 415 | "pluginVersion": "8.1.2", 416 | "pointradius": 2, 417 | "points": false, 418 | "renderer": "flot", 419 | "seriesOverrides": [], 420 | "spaceLength": 10, 421 | "stack": false, 422 | "steppedLine": false, 423 | "targets": [ 424 | { 425 | "exemplar": true, 426 | "expr": "increase(github_traffic_clones[1h])", 427 | "interval": "", 428 | "legendFormat": "{{repository}}", 429 | "refId": "A" 430 | } 431 | ], 432 | "thresholds": [], 433 | "timeFrom": null, 434 | "timeRegions": [], 435 | "timeShift": null, 436 | "title": "Clones", 437 | "tooltip": { 438 | "shared": true, 439 | "sort": 0, 440 | "value_type": "individual" 441 | }, 442 | "type": "graph", 443 | "xaxis": { 444 | "buckets": null, 445 | "mode": "time", 446 | "name": null, 447 | "show": true, 448 | "values": [] 449 | }, 450 | "yaxes": [ 451 | { 452 | "format": "short", 453 | "label": null, 454 | "logBase": 1, 455 | "max": null, 456 | "min": null, 457 | "show": true 458 | }, 459 | { 460 | "format": "short", 461 | "label": null, 462 | "logBase": 1, 463 | "max": null, 464 | "min": null, 465 | "show": true 466 | } 467 | ], 468 | "yaxis": { 469 | "align": false, 470 | "alignLevel": null 471 | } 472 | }, 473 | { 474 | "aliasColors": {}, 475 | "bars": false, 476 | "dashLength": 10, 477 | "dashes": false, 478 | "datasource": "${DS_GRAFANACLOUD-K6CLOUD-PROM}", 479 | "fill": 1, 480 | "fillGradient": 0, 481 | "gridPos": { 482 | "h": 7, 483 | "w": 12, 484 | "x": 12, 485 | "y": 15 486 | }, 487 | "hiddenSeries": false, 488 | "id": 5, 489 | "legend": { 490 | "alignAsTable": true, 491 | "avg": false, 492 | "current": true, 493 | "max": true, 494 | "min": false, 495 | "rightSide": true, 496 | "show": true, 497 | "sort": "max", 498 | "sortDesc": true, 499 | "total": false, 500 | "values": true 501 | }, 502 | "lines": true, 503 | "linewidth": 1, 504 | "nullPointMode": "null", 505 | "options": { 506 | "alertThreshold": true 507 | }, 508 | "percentage": false, 509 | "pluginVersion": "8.1.2", 510 | "pointradius": 2, 511 | "points": false, 512 | "renderer": "flot", 513 | "seriesOverrides": [], 514 | "spaceLength": 10, 515 | "stack": false, 516 | "steppedLine": false, 517 | "targets": [ 518 | { 519 | "exemplar": true, 520 | "expr": "increase(github_traffic_unique_clones[1h])", 521 | "interval": "", 522 | "legendFormat": "{{repository}}", 523 | "refId": "A" 524 | } 525 | ], 526 | "thresholds": [], 527 | "timeFrom": null, 528 | "timeRegions": [], 529 | "timeShift": null, 530 | "title": "Clones (unique)", 531 | "tooltip": { 532 | "shared": true, 533 | "sort": 0, 534 | "value_type": "individual" 535 | }, 536 | "type": "graph", 537 | "xaxis": { 538 | "buckets": null, 539 | "mode": "time", 540 | "name": null, 541 | "show": true, 542 | "values": [] 543 | }, 544 | "yaxes": [ 545 | { 546 | "format": "short", 547 | "label": null, 548 | "logBase": 1, 549 | "max": null, 550 | "min": null, 551 | "show": true 552 | }, 553 | { 554 | "format": "short", 555 | "label": null, 556 | "logBase": 1, 557 | "max": null, 558 | "min": null, 559 | "show": true 560 | } 561 | ], 562 | "yaxis": { 563 | "align": false, 564 | "alignLevel": null 565 | } 566 | } 567 | ], 568 | "refresh": false, 569 | "schemaVersion": 30, 570 | "style": "dark", 571 | "tags": [], 572 | "templating": { 573 | "list": [] 574 | }, 575 | "time": { 576 | "from": "now-2d", 577 | "to": "now" 578 | }, 579 | "timepicker": {}, 580 | "timezone": "", 581 | "title": "Github Traffic", 582 | "uid": "xgtdaoI7z", 583 | "version": 3 584 | } -------------------------------------------------------------------------------- /dashboard/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/github-traffic/908408dadf6b13de62875d9e4a0eddb1d0838c8f/dashboard/screenshot.png -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | grafana: 3 | image: 'grafana/grafana-oss' 4 | ports: 5 | - "3000:3000" 6 | links: 7 | - prometheus 8 | dns: 9 | - 8.8.8.8 10 | prometheus: 11 | image: 'prom/prometheus' 12 | volumes: 13 | - ./prometheus/:/etc/prometheus/ 14 | command: 15 | - '--config.file=/etc/prometheus/prometheus.yaml' 16 | ports: 17 | - "9090:9090" 18 | links: 19 | - traffic 20 | traffic: 21 | image: 'ghcr.io/grafana/github-traffic' 22 | ports: 23 | - "8001:8001" 24 | env_file: 25 | - .env 26 | restart: 27 | on-failure 28 | dns: 29 | - 8.8.8.8 30 | -------------------------------------------------------------------------------- /github-traffic.py: -------------------------------------------------------------------------------- 1 | from github import Github 2 | from decouple import config 3 | from prometheus_client import start_http_server, Gauge 4 | from decouple import config 5 | from apscheduler.schedulers.blocking import BlockingScheduler 6 | from apscheduler.triggers.cron import CronTrigger 7 | from prometheus_client import start_http_server, Gauge 8 | from logfmt_logger import getLogger 9 | 10 | ORG_NAME = config("ORG_NAME", default="") 11 | USER_NAME = config("USER_NAME", default="") 12 | REPO_TYPE = config("REPO_TYPE", default="public") 13 | REPO_NAME_CONTAINS = config("REPO_NAME_CONTAINS", default="") 14 | CRONTAB_SCHEDULE = config("CRONTAB_SCHEDULE", default="0 * * * *") 15 | GITHUB_TOKEN = config('GITHUB_TOKEN') 16 | 17 | github = Github(GITHUB_TOKEN) 18 | 19 | logger = getLogger("github_traffic") 20 | 21 | gh_traffic_views = Gauge( 22 | "github_traffic_views", 23 | "Number of views", 24 | ["repository"], 25 | ) 26 | 27 | gh_traffic_unique_views = Gauge( 28 | "github_traffic_unique_views", 29 | "Number of unique views", 30 | ["repository"], 31 | ) 32 | 33 | gh_traffic_clones = Gauge( 34 | "github_traffic_clones", 35 | "Number of clones", 36 | ["repository"], 37 | ) 38 | 39 | gh_traffic_unique_clones = Gauge( 40 | "github_traffic_unique_clones", 41 | "Number of unique views", 42 | ["repository"], 43 | ) 44 | 45 | gh_traffic_top_paths = Gauge( 46 | "github_traffic_top_paths", 47 | "Number of visits to top paths", 48 | ["repository", "path", "title"], 49 | ) 50 | 51 | gh_traffic_top_unique_paths = Gauge( 52 | "github_traffic_top_unique_paths", 53 | "Number of unique visits to top paths", 54 | ["repository", "path", "title"], 55 | ) 56 | 57 | gh_traffic_top_referrers = Gauge( 58 | "github_traffic_top_referrers", 59 | "Number of visits from top referrers", 60 | ["repository", "referrer"], 61 | ) 62 | 63 | gh_traffic_top_unique_referrers = Gauge( 64 | "github_traffic_top_unique_referrers", 65 | "Number of unique visits from top referrers", 66 | ["repository", "referrer"], 67 | ) 68 | 69 | gh_traffic_stars = Gauge( 70 | "github_traffic_stars", 71 | "Number of stars", 72 | ["repository"], 73 | ) 74 | 75 | gh_traffic_api_requests_limit = Gauge( 76 | "github_traffic_api_requests_limit", 77 | "Requests limit from Github API limits", 78 | ) 79 | 80 | gh_traffic_api_requests_remaining = Gauge( 81 | "github_traffic_api_requests_remaining", 82 | "Requests remaining from Github API limits", 83 | ) 84 | 85 | def job_function(): 86 | api_limits = github.get_rate_limit().core 87 | gh_traffic_api_requests_limit.set(api_limits.limit) 88 | gh_traffic_api_requests_remaining.set(api_limits.remaining) 89 | if(not ORG_NAME and not USER_NAME): 90 | logger.error(f"Please fill ORG_NAME or USER_NAME in config") 91 | if(ORG_NAME): 92 | repositories = github.get_organization(ORG_NAME).get_repos(type=REPO_TYPE) 93 | if(USER_NAME): 94 | repositories = github.get_user(USER_NAME).get_repos(type=REPO_TYPE) 95 | for repo in repositories: 96 | if REPO_NAME_CONTAINS in repo.name: 97 | repo_name = repo.name 98 | toShow = dict() 99 | toShow['repo'] = repo_name 100 | 101 | try: 102 | # Views stats 103 | data_views = repo.get_views_traffic(per="day") 104 | gh_traffic_views.labels(repo_name).set(data_views["views"][-1].count) 105 | gh_traffic_unique_views.labels(repo_name).set(data_views["views"][-1].uniques) 106 | toShow['views'] = data_views["views"][-1].count 107 | toShow['unique_views'] = data_views["views"][-1].uniques 108 | except Exception as e: 109 | logger.error(f"Failed to extract views on {repo_name}: {e}") 110 | try: 111 | # Clones stats 112 | data_clones = repo.get_clones_traffic(per="day") 113 | gh_traffic_clones.labels(repo_name).set(data_clones["clones"][-1].count) 114 | gh_traffic_unique_clones.labels(repo_name).set(data_clones["clones"][-1].uniques) 115 | toShow['clones'] = data_clones["clones"][-1].count 116 | toShow['unique_clones'] = data_clones["clones"][-1].uniques 117 | except Exception as e: 118 | logger.error(f"Failed to extract clones on {repo_name}: {e}") 119 | try: 120 | # Star stats 121 | gh_traffic_stars.labels(repo_name).set(repo.stargazers_count) 122 | toShow['stars'] = repo.stargazers_count 123 | except Exception as e: 124 | logger.error(f"Failed to get stars on {repo_name}: {e}") 125 | try: 126 | # Top Paths 127 | total_count = 0 128 | total_unique_count = 0 129 | for path in repo.get_top_paths(): 130 | gh_traffic_top_paths.labels(repo_name, path.path, path.title).set(path.count) 131 | total_count = total_count + path.count 132 | gh_traffic_top_unique_paths.labels(repo_name, path.path, path.title).set(path.uniques) 133 | total_unique_count = total_unique_count + path.uniques 134 | toShow['sum(top_paths)'] = total_count 135 | toShow['sum(top_unique_paths)'] = total_unique_count 136 | except Exception as e: 137 | logger.error(f"Failed to get top paths on {repo_name}: {e}") 138 | try: 139 | # Top Referrers 140 | total_count = 0 141 | total_unique_count = 0 142 | for referrer in repo.get_top_referrers(): 143 | gh_traffic_top_referrers.labels(repo_name, referrer.referrer).set(referrer.count) 144 | total_count = total_count + referrer.count 145 | gh_traffic_top_unique_referrers.labels(repo_name, referrer.referrer).set(referrer.uniques) 146 | total_unique_count = total_unique_count + referrer.uniques 147 | toShow['sum(top_referrers)'] = total_count 148 | toShow['sum(top_unique_referrers)'] = total_unique_count 149 | except Exception as e: 150 | logger.error(f"Failed to get top referrers on {repo_name}: {e}") 151 | 152 | logger.info('Gather insights', extra={"context": toShow}) 153 | 154 | 155 | if __name__ == "__main__": 156 | logger.info('Github traffic is running!') 157 | # Start up the server to expose the metrics. 158 | start_http_server(8001) 159 | # Schedule run the job on startup. 160 | job_function() 161 | # Start scheduler 162 | sched = BlockingScheduler() 163 | sched.add_job(job_function, CronTrigger.from_crontab(CRONTAB_SCHEDULE)) 164 | sched.start() 165 | -------------------------------------------------------------------------------- /prometheus/prometheus.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 30s 3 | scrape_timeout: 10s 4 | 5 | rule_files: 6 | - alert.yml 7 | 8 | scrape_configs: 9 | - job_name: services 10 | metrics_path: /metrics 11 | static_configs: 12 | - targets: 13 | - 'traffic:8001' 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | apscheduler==3.7.0 2 | requests==2.25.0 3 | python-decouple==3.3 4 | prometheus-client==0.9.0 5 | pygithub==1.51 6 | logfmt-logger==0.1.2 --------------------------------------------------------------------------------