├── .dockerignore ├── .env ├── .github └── workflows │ ├── dockerhub.yaml │ ├── dockerimage.yaml │ └── dotnetbuild.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose-pull.yml ├── docker-compose.yml ├── metrics.json └── src ├── .run ├── Pull Compose Deployment.run.xml └── server.run.xml ├── core ├── CounterExtensions.cs ├── GlobalSuppressions.cs ├── IConfigure.cs ├── IQuery.cs ├── MetricQueryFactory.cs ├── QueryExtensions.cs ├── config │ ├── ColumnUsage.cs │ ├── Constants.cs │ ├── MeasureResult.cs │ ├── MetricFile.cs │ ├── MetricQuery.cs │ ├── MetricQueryColumn.cs │ ├── Parser.cs │ └── QueryUsage.cs ├── core.csproj ├── metrics │ └── ConnectionUp.cs └── queries │ ├── CounterGroupQuery.cs │ ├── GaugeGroupQuery.cs │ └── GenericQuery.cs ├── global.json ├── mssql_exporter.sln └── server ├── ConfigurationOptions.cs ├── GlobalSuppressions.cs ├── OnDemandCollector.cs ├── Program.cs ├── config.json ├── metrics.json └── server.csproj /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | Dockerfile 3 | docker-compose.yml 4 | .DS_Store 5 | .gitignore 6 | README.md 7 | env.* 8 | bin 9 | obj 10 | .idea 11 | .vs 12 | 13 | # To prevent storing dev/temporary container data 14 | *.csv 15 | /tmp/* 16 | out -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PROMETHEUS_MSSQL_DataSource=Server=tcp:localhost,1433;Initial Catalog=master;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=10; 2 | PROMETHEUS_MSSQL_ConfigFile=metrics.json 3 | PROMETHEUS_MSSQL_ServerPath=metrics 4 | PROMETHEUS_MSSQL_ServerPort=80 5 | PROMETHEUS_MSSQL_LogLevel=Error 6 | ACCEPT_EULA=Y 7 | SA_PASSWORD=yourStrong(!)Password -------------------------------------------------------------------------------- /.github/workflows/dockerhub.yaml: -------------------------------------------------------------------------------- 1 | name: DockerHub 2 | 3 | on: 4 | push: 5 | branches: 6 | - "develop" 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - "develop" 12 | release: 13 | types: [published] 14 | jobs: 15 | docker: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | - name: Docker meta 21 | id: meta 22 | uses: docker/metadata-action@v4 23 | with: 24 | images: danieloliver/mssql_exporter 25 | tags: | 26 | type=ref,event=branch 27 | type=ref,event=pr 28 | type=semver,pattern={{version}} 29 | type=semver,pattern={{major}}.{{minor}} 30 | - name: Login to DockerHub 31 | if: github.event_name != 'pull_request' 32 | uses: docker/login-action@v2 33 | with: 34 | username: ${{ secrets.DOCKERHUB_USERNAME }} 35 | password: ${{ secrets.DOCKERHUB_TOKEN }} 36 | - name: Build and push 37 | uses: docker/build-push-action@v3 38 | with: 39 | context: . 40 | push: ${{ github.event_name != 'pull_request' }} 41 | tags: ${{ steps.meta.outputs.tags }} 42 | labels: ${{ steps.meta.outputs.labels }} 43 | -------------------------------------------------------------------------------- /.github/workflows/dockerimage.yaml: -------------------------------------------------------------------------------- 1 | name: Build Dockerfile check 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Build the Docker image 9 | run: docker build . --file Dockerfile --tag mssql_exporter:$(date +%s) 10 | -------------------------------------------------------------------------------- /.github/workflows/dotnetbuild.yaml: -------------------------------------------------------------------------------- 1 | name: dotnet package 2 | 3 | on: 4 | push: 5 | branches: 6 | - "develop" 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - "develop" 12 | release: 13 | types: [published] 14 | 15 | jobs: 16 | build-ubuntu: 17 | strategy: 18 | matrix: 19 | dotnet-version: ["6.0"] 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Setup dotnet 24 | uses: actions/setup-dotnet@v2 25 | with: 26 | dotnet-version: ${{ matrix.dotnet-version }} 27 | - name: Install dependencies 28 | run: | 29 | cd ./src/ 30 | dotnet restore 31 | - name: Build 32 | run: | 33 | cd ./src/server 34 | dotnet build --configuration Release --no-restore 35 | dotnet publish -c Release -o ../../mssql_exporter_linux_x64 -r linux-x64 --self-contained true 36 | - name: Upload dotnet exe 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: mssql_exporter_linux_x64 40 | path: mssql_exporter_linux_x64 41 | 42 | build-windows: 43 | strategy: 44 | matrix: 45 | dotnet-version: ["6.0"] 46 | runs-on: windows-latest 47 | steps: 48 | - uses: actions/checkout@v3 49 | - name: Setup dotnet 50 | uses: actions/setup-dotnet@v2 51 | with: 52 | dotnet-version: ${{ matrix.dotnet-version }} 53 | - name: Install dependencies 54 | run: | 55 | cd ./src/ 56 | dotnet restore 57 | - name: Build 58 | run: | 59 | cd ./src/server 60 | dotnet build --configuration Release --no-restore 61 | dotnet publish -c Release -o ../../mssql_exporter_win_x64 -r win-x64 --self-contained true 62 | - name: Upload dotnet exe 63 | uses: actions/upload-artifact@v3 64 | with: 65 | name: mssql_exporter_win_x64 66 | path: mssql_exporter_win_x64 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | bin 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush 296 | .cr/ 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | 333 | zip 334 | 7Zip4Powershell 335 | out 336 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 2 | WORKDIR /app 3 | 4 | # copy csproj and restore as distinct layers 5 | COPY src/core/*.csproj ./core/ 6 | COPY src/server/*.csproj ./server/ 7 | WORKDIR /app/core 8 | RUN dotnet restore 9 | WORKDIR /app/server 10 | RUN dotnet restore 11 | 12 | # copy and build app and libraries 13 | WORKDIR /app 14 | COPY src/core/. ./core/ 15 | COPY src/server/. ./server/ 16 | COPY metrics.json ./ 17 | WORKDIR /app/server 18 | RUN dotnet publish -c Release -r linux-x64 -o out -p:PublishSingleFile=true --self-contained true -p:PublishTrimmed=true 19 | 20 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime 21 | EXPOSE 80 22 | WORKDIR /app 23 | COPY --from=build /app/server/out ./ 24 | ENTRYPOINT ["./mssql_exporter", "serve"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mssql_exporter 2 | 3 | MSSQL Exporter for Prometheus 4 | 5 | 6 | | GitHub Actions | GitHub | Docker Hub | 7 | |---:|:---:|:---| 8 | | [![Build Dockerfile check](https://github.com/DanielOliver/mssql_exporter/actions/workflows/dockerimage.yaml/badge.svg)](https://github.com/DanielOliver/mssql_exporter/actions/workflows/dockerimage.yaml) | [![GitHub release](https://img.shields.io/github/release/DanielOliver/mssql_exporter.svg)](https://github.com/DanielOliver/mssql_exporter/releases/latest) | [![Docker Hub](https://img.shields.io/docker/pulls/danieloliver/mssql_exporter)](https://hub.docker.com/r/danieloliver/mssql_exporter) | 9 | 10 | ## Quickstart docker-compose 11 | 12 | ```powershell 13 | docker-compose up 14 | ``` 15 | 16 | docker-compose.yml 17 | 18 | ```yml 19 | version: '3' 20 | services: 21 | mssql_exporter: 22 | image: "danieloliver/mssql_exporter:latest" 23 | ports: 24 | - "80:80" 25 | depends_on: 26 | - sqlserver.dev 27 | environment: 28 | - PROMETHEUS_MSSQL_DataSource=Server=tcp:sqlserver.dev,1433;Initial Catalog=master;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=10; 29 | - PROMETHEUS_MSSQL_ConfigFile=metrics.json 30 | - PROMETHEUS_MSSQL_ServerPath=metrics 31 | - PROMETHEUS_MSSQL_ServerPort=80 32 | - PROMETHEUS_MSSQL_AddExporterMetrics=false 33 | - PROMETHEUS_MSSQL_Serilog__MinimumLevel=Information 34 | - | 35 | PROMETHEUS_MSSQL_ConfigText= 36 | { 37 | "Queries": [ 38 | { 39 | "Name": "mssql_process_status", 40 | "Query": "SELECT status, COUNT(*) count FROM sys.sysprocesses GROUP BY status", 41 | "Description": "Counts the number of processes per status", 42 | "Usage": "GaugesWithLabels", 43 | "Columns": [ 44 | { 45 | "Name": "status", 46 | "Label": "status", 47 | "Usage": "GaugeLabel", 48 | "Order": 0 49 | }, 50 | { 51 | "Name": "count", 52 | "Label": "count", 53 | "Usage": "Gauge" 54 | } 55 | ] 56 | }, 57 | { 58 | "Name": "mssql_process_connections", 59 | "Query": "SELECT ISNULL(DB_NAME(dbid), 'other') as dbname, COUNT(dbid) as connections FROM sys.sysprocesses WHERE dbid > 0 GROUP BY dbid", 60 | "Description": "Counts the number of connections per db", 61 | "Usage": "GaugesWithLabels", 62 | "Columns": [ 63 | { 64 | "Name": "dbname", 65 | "Label": "dbname", 66 | "Usage": "GaugeLabel", 67 | "Order": 0 68 | }, 69 | { 70 | "Name": "connections", 71 | "Label": "count", 72 | "Usage": "Gauge" 73 | } 74 | ] 75 | }, 76 | { 77 | "Name": "mssql_deadlocks", 78 | "Query": "SELECT cntr_value FROM sys.dm_os_performance_counters where counter_name = 'Number of Deadlocks/sec' AND instance_name = '_Total'", 79 | "Description": "Number of lock requests per second that resulted in a deadlock since last restart", 80 | "Columns": [ 81 | { 82 | "Name": "cntr_value", 83 | "Label": "mssql_deadlocks", 84 | "Usage": "Gauge", 85 | "DefaultValue": 0 86 | } 87 | ] 88 | } 89 | ], 90 | "MillisecondTimeout": 4000 91 | } 92 | sqlserver.dev: 93 | image: "mcr.microsoft.com/mssql/server:2017-latest" 94 | ports: 95 | - "1433:1433" 96 | environment: 97 | - ACCEPT_EULA=Y 98 | - SA_PASSWORD=yourStrong(!)Password 99 | ``` 100 | 101 | ## QuickStart binary 102 | 103 | 1. Download system of your choice from [latest release](https://github.com/DanielOliver/mssql_exporter/releases/latest). 104 | 105 | 2. Create a file "metrics.json" and put this in it: 106 | 107 | ```json 108 | { 109 | "Queries": [ 110 | { 111 | "Name": "mssql_process_status", 112 | "Query": "SELECT status, COUNT(*) count FROM sys.sysprocesses GROUP BY status", 113 | "Description": "Counts the number of processes per status", 114 | "Usage": "GaugesWithLabels", 115 | "Columns": [ 116 | { 117 | "Name": "status", 118 | "Label": "status", 119 | "Usage": "GaugeLabel", 120 | "Order": 0 121 | }, 122 | { 123 | "Name": "count", 124 | "Label": "count", 125 | "Usage": "Gauge" 126 | } 127 | ] 128 | }, 129 | { 130 | "Name": "mssql_process_connections", 131 | "Query": "SELECT ISNULL(DB_NAME(dbid), 'other') as dbname, COUNT(dbid) as connections FROM sys.sysprocesses WHERE dbid > 0 GROUP BY dbid", 132 | "Description": "Counts the number of connections per db", 133 | "Usage": "GaugesWithLabels", 134 | "Columns": [ 135 | { 136 | "Name": "dbname", 137 | "Label": "dbname", 138 | "Usage": "GaugeLabel", 139 | "Order": 0 140 | }, 141 | { 142 | "Name": "connections", 143 | "Label": "count", 144 | "Usage": "Gauge" 145 | } 146 | ] 147 | }, 148 | { 149 | "Name": "mssql_deadlocks", 150 | "Query": "SELECT cntr_value FROM sys.dm_os_performance_counters where counter_name = 'Number of Deadlocks/sec' AND instance_name = '_Total'", 151 | "Description": "Number of lock requests per second that resulted in a deadlock since last restart", 152 | "Columns": [ 153 | { 154 | "Name": "cntr_value", 155 | "Label": "mssql_deadlocks", 156 | "Usage": "Gauge", 157 | "DefaultValue": 0 158 | } 159 | ] 160 | } 161 | ], 162 | "MillisecondTimeout": 4000 163 | } 164 | ``` 165 | 166 | 3. Run mssql_exporter 167 | 168 | ```bash 169 | ./mssql_exporter serve -ConfigFile "metrics.json" -DataSource "Server=tcp:{ YOUR DATABASE HERE },1433;Initial Catalog={ YOUR INITIAL CATALOG HERE };Persist Security Info=False;User ID={ USER ID HERE };Password={ PASSWORD HERE };MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=8;" 170 | ``` 171 | 172 | or 173 | 174 | ```powershell 175 | .\mssql_exporter.exe serve -ConfigFile "metrics.json" -DataSource "Server=tcp:{ YOUR DATABASE HERE },1433;Initial Catalog={ YOUR INITIAL CATALOG HERE };Persist Security Info=False;User ID={ USER ID HERE };Password={ PASSWORD HERE };MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=8;" 176 | ``` 177 | 178 | 4. Open http://localhost/metrics 179 | 180 | Content should look like 181 | ```txt 182 | # HELP mssql_process_status Counts the number of processes per status 183 | # TYPE mssql_process_status gauge 184 | mssql_process_status{status="runnable"} 2 185 | mssql_process_status{status="sleeping"} 19 186 | mssql_process_status{status="background"} 24 187 | # HELP mssql_process_connections Counts the number of connections per db 188 | # TYPE mssql_process_connections gauge 189 | mssql_process_connections{dbname="master"} 29 190 | mssql_process_connections{dbname="tempdb"} 1 191 | # HELP mssql_timeouts Number of queries timing out. 192 | # TYPE mssql_timeouts gauge 193 | mssql_timeouts 0 194 | # HELP mssql_exceptions Number of queries throwing exceptions. 195 | # TYPE mssql_exceptions gauge 196 | mssql_exceptions 0 197 | # HELP mssql_deadlocks mssql_deadlocks 198 | # TYPE mssql_deadlocks gauge 199 | mssql_deadlocks 0 200 | # HELP mssql_up mssql_up 201 | # TYPE mssql_up gauge 202 | mssql_up 1 203 | ``` 204 | 205 | _Note_ 206 | 207 | * *mssql_up* gauge is "1" if the database is reachable. "0" if connection to the database fails. 208 | * *mssql_exceptions* gauge is "0" if all queries run successfully. Else, this is the number of queries that throw exceptions. 209 | * *mssql_timeouts* is "0" if all queries are running with the configured timeout. Else, this is the number of queries that are not completing within the configured timeout. 210 | 211 | 5. Add Prometheus scrape target (assuming same machine). 212 | 213 | ```yml 214 | global: 215 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 216 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 217 | 218 | scrape_configs: 219 | - job_name: 'netcore-prometheus' 220 | # metrics_path defaults to '/metrics' 221 | static_configs: 222 | - targets: ['localhost'] 223 | ``` 224 | 225 | ## Command Line Options 226 | 227 | ``` 228 | Commands 229 | help 230 | serve 231 | -DataSource (Connection String) 232 | -ConfigFile (metrics.json) 233 | -ServerPath (/metrics) 234 | -ServerPort (80) 235 | -AddExporterMetrics (false) 236 | -ConfigText () 237 | 238 | Or environment variables: 239 | PROMETHEUS_MSSQL_DataSource 240 | PROMETHEUS_MSSQL_ConfigFile 241 | PROMETHEUS_MSSQL_ServerPath 242 | PROMETHEUS_MSSQL_ServerPort 243 | PROMETHEUS_MSSQL_AddExporterMetrics 244 | PROMETHEUS_MSSQL_ConfigText 245 | PROMETHEUS_MSSQL_Serilog__MinimumLevel 246 | ``` 247 | 248 | * DataSource 249 | * Default: empty 250 | * SQL Server .NET connection String 251 | * ConfigFile 252 | * Default: "metrics.json" 253 | * The path to the configuration file as shown in "metrics.json" above. 254 | * ServerPath 255 | * Default: "metrics" 256 | * specifies the path for prometheus to answer requests on 257 | * ServerPort 258 | * Default: 80 259 | * AddExporterMetrics 260 | * Default: false 261 | * Options: 262 | * true 263 | * false 264 | * ConfigText 265 | * Default: empty 266 | * Optionally fill in this with the contents of the ConfigFile to ignore and not read from the ConfigFile. 267 | 268 | ## Run as windows service 269 | 270 | You can install the exporter as windows service with the following command 271 | ```bash 272 | sc create mssql_exporter binpath="%full_path_to_mssql_exporter.exe%" 273 | ``` 274 | 275 | ## Changing logging configuration 276 | 277 | Logging is configured using [Serilog Settings Configuration](https://github.com/serilog/serilog-settings-configuration) 278 | 279 | Editing "config.json" allows for changing aspects of logging. Console is default, and "Serilog.Sinks.File" is also installed. Further sinks would have to be installed into the project file's dependencies. 280 | 281 | ## Debug Run and Docker 282 | 283 | 1. Run Docker 284 | 285 | ```powershell 286 | docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=yourStrong(!)Password" --net=host -p 1433:1433 -d --rm --name sqlserverdev mcr.microsoft.com/mssql/server:2017-latest 287 | ``` 288 | 289 | 2. Run exporter from "src/server" directory. 290 | 291 | ```powershell 292 | dotnet run -- serve -ConfigFile "../../metrics.json" -DataSource "Server=tcp:localhost,1433;Initial Catalog=master;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=8;" 293 | ``` 294 | 295 | ```bash 296 | dotnet run -- serve -ConfigFile "../../metrics.json" -DataSource 'Server=tcp:localhost,1433;Initial Catalog=master;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=8;' 297 | ``` 298 | 299 | OR 300 | 301 | 3. Docker-compose! 302 | 303 | ```powershell 304 | docker-compose up 305 | ``` 306 | -------------------------------------------------------------------------------- /docker-compose-pull.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mssql_exporter: 4 | image: "danieloliver/mssql_exporter:latest" 5 | ports: 6 | - "80:80" 7 | depends_on: 8 | - sqlserver.dev 9 | environment: 10 | - PROMETHEUS_MSSQL_DataSource=Server=tcp:sqlserver.dev,1433;Initial Catalog=master;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=10; 11 | - PROMETHEUS_MSSQL_ConfigFile=metrics.json 12 | - PROMETHEUS_MSSQL_ServerPath=metrics 13 | - PROMETHEUS_MSSQL_ServerPort=80 14 | - PROMETHEUS_MSSQL_AddExporterMetrics=false 15 | - PROMETHEUS_MSSQL_Serilog__MinimumLevel=Information 16 | - | 17 | PROMETHEUS_MSSQL_ConfigText= 18 | { 19 | "Queries": [ 20 | { 21 | "Name": "mssql_process_status", 22 | "Query": "SELECT status, COUNT(*) count FROM sys.sysprocesses GROUP BY status", 23 | "Description": "Counts the number of processes per status", 24 | "Usage": "GaugesWithLabels", 25 | "Columns": [ 26 | { 27 | "Name": "status", 28 | "Label": "status", 29 | "Usage": "GaugeLabel", 30 | "Order": 0 31 | }, 32 | { 33 | "Name": "count", 34 | "Label": "count", 35 | "Usage": "Gauge" 36 | } 37 | ] 38 | }, 39 | { 40 | "Name": "mssql_process_connections", 41 | "Query": "SELECT ISNULL(DB_NAME(dbid), 'other') as dbname, COUNT(dbid) as connections FROM sys.sysprocesses WHERE dbid > 0 GROUP BY dbid", 42 | "Description": "Counts the number of connections per db", 43 | "Usage": "GaugesWithLabels", 44 | "Columns": [ 45 | { 46 | "Name": "dbname", 47 | "Label": "dbname", 48 | "Usage": "GaugeLabel", 49 | "Order": 0 50 | }, 51 | { 52 | "Name": "connections", 53 | "Label": "count", 54 | "Usage": "Gauge" 55 | } 56 | ] 57 | }, 58 | { 59 | "Name": "mssql_deadlocks", 60 | "Query": "SELECT cntr_value FROM sys.dm_os_performance_counters where counter_name = 'Number of Deadlocks/sec' AND instance_name = '_Total'", 61 | "Description": "Number of lock requests per second that resulted in a deadlock since last restart", 62 | "Columns": [ 63 | { 64 | "Name": "cntr_value", 65 | "Label": "mssql_deadlocks", 66 | "Usage": "Gauge", 67 | "DefaultValue": 0 68 | } 69 | ] 70 | } 71 | ], 72 | "MillisecondTimeout": 4000 73 | } 74 | sqlserver.dev: 75 | image: "mcr.microsoft.com/mssql/server:2017-latest" 76 | ports: 77 | - "1433:1433" 78 | environment: 79 | - ACCEPT_EULA=Y 80 | - SA_PASSWORD=yourStrong(!)Password -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mssql_exporter: 4 | build: . 5 | ports: 6 | - "80:80" 7 | depends_on: 8 | - sqlserver.dev 9 | environment: 10 | - PROMETHEUS_MSSQL_DataSource=Server=tcp:sqlserver.dev,1433;Initial Catalog=master;Persist Security Info=False;User ID=sa;Password=yourStrong(!)Password;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=10; 11 | - PROMETHEUS_MSSQL_ConfigFile=metrics.json 12 | - PROMETHEUS_MSSQL_ServerPath=metrics 13 | - PROMETHEUS_MSSQL_ServerPort=80 14 | - PROMETHEUS_MSSQL_AddExporterMetrics=false 15 | - PROMETHEUS_MSSQL_Serilog__MinimumLevel=Information 16 | - | 17 | PROMETHEUS_MSSQL_ConfigText= 18 | { 19 | "Queries": [ 20 | { 21 | "Name": "mssql_process_status", 22 | "Query": "SELECT status, COUNT(*) count FROM sys.sysprocesses GROUP BY status", 23 | "Description": "Counts the number of processes per status", 24 | "Usage": "GaugesWithLabels", 25 | "Columns": [ 26 | { 27 | "Name": "status", 28 | "Label": "status", 29 | "Usage": "GaugeLabel", 30 | "Order": 0 31 | }, 32 | { 33 | "Name": "count", 34 | "Label": "count", 35 | "Usage": "Gauge" 36 | } 37 | ] 38 | }, 39 | { 40 | "Name": "mssql_process_connections", 41 | "Query": "SELECT ISNULL(DB_NAME(dbid), 'other') as dbname, COUNT(dbid) as connections FROM sys.sysprocesses WHERE dbid > 0 GROUP BY dbid", 42 | "Description": "Counts the number of connections per db", 43 | "Usage": "GaugesWithLabels", 44 | "Columns": [ 45 | { 46 | "Name": "dbname", 47 | "Label": "dbname", 48 | "Usage": "GaugeLabel", 49 | "Order": 0 50 | }, 51 | { 52 | "Name": "connections", 53 | "Label": "count", 54 | "Usage": "Gauge" 55 | } 56 | ] 57 | }, 58 | { 59 | "Name": "mssql_deadlocks", 60 | "Query": "SELECT cntr_value FROM sys.dm_os_performance_counters where counter_name = 'Number of Deadlocks/sec' AND instance_name = '_Total'", 61 | "Description": "Number of lock requests per second that resulted in a deadlock since last restart", 62 | "Columns": [ 63 | { 64 | "Name": "cntr_value", 65 | "Label": "mssql_deadlocks", 66 | "Usage": "Gauge", 67 | "DefaultValue": 0 68 | } 69 | ] 70 | } 71 | ], 72 | "MillisecondTimeout": 4000 73 | } 74 | sqlserver.dev: 75 | image: "mcr.microsoft.com/mssql/server:2017-latest" 76 | ports: 77 | - "1433:1433" 78 | environment: 79 | - ACCEPT_EULA=Y 80 | - SA_PASSWORD=yourStrong(!)Password -------------------------------------------------------------------------------- /metrics.json: -------------------------------------------------------------------------------- 1 | { 2 | "Queries": [ 3 | { 4 | "Name": "mssql_process_status", 5 | "Query": "SELECT status, COUNT(*) count FROM sys.sysprocesses GROUP BY status", 6 | "Description": "Counts the number of processes per status", 7 | "Usage": "GaugesWithLabels", 8 | "Columns": [ 9 | { 10 | "Name": "status", 11 | "Label": "status", 12 | "Usage": "GaugeLabel", 13 | "Order": 0 14 | }, 15 | { 16 | "Name": "count", 17 | "Label": "count", 18 | "Usage": "Gauge" 19 | } 20 | ] 21 | }, 22 | { 23 | "Name": "mssql_process_connections", 24 | "Query": "SELECT ISNULL(DB_NAME(dbid), 'other') as dbname, COUNT(dbid) as connections FROM sys.sysprocesses WHERE dbid > 0 GROUP BY dbid", 25 | "Description": "Counts the number of connections per db", 26 | "Usage": "GaugesWithLabels", 27 | "Columns": [ 28 | { 29 | "Name": "dbname", 30 | "Label": "dbname", 31 | "Usage": "GaugeLabel", 32 | "Order": 0 33 | }, 34 | { 35 | "Name": "connections", 36 | "Label": "count", 37 | "Usage": "Gauge" 38 | } 39 | ] 40 | }, 41 | { 42 | "Name": "mssql_deadlocks", 43 | "Query": "SELECT cntr_value FROM sys.dm_os_performance_counters where counter_name = 'Number of Deadlocks/sec' AND instance_name = '_Total'", 44 | "Description": "Number of lock requests per second that resulted in a deadlock since last restart", 45 | "Columns": [ 46 | { 47 | "Name": "cntr_value", 48 | "Label": "mssql_deadlocks", 49 | "Usage": "Gauge", 50 | "DefaultValue": 0 51 | } 52 | ] 53 | } 54 | ], 55 | "MillisecondTimeout": 4000 56 | } -------------------------------------------------------------------------------- /src/.run/Pull Compose Deployment.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/.run/server.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /src/core/CounterExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace mssql_exporter.core 2 | { 3 | public static class CounterExtensions 4 | { 5 | public static void Set(this Prometheus.Counter counter, double value) 6 | { 7 | if (counter.Value < value) 8 | { 9 | counter.Inc(value - counter.Value); 10 | } 11 | } 12 | 13 | public static void Set(this Prometheus.Counter.Child counter, double value) 14 | { 15 | if (counter.Value < value) 16 | { 17 | counter.Inc(value - counter.Value); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/core/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives must be placed correctly", Justification = "I don't like this.")] 7 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "My naming convention is purposeful.")] 8 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element must begin with upper-case letter", Justification = "Lot of places to fix this.")] 9 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names must not contain underscore", Justification = "My naming convention is purposeful.")] 10 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1623:Property summary documentation must match accessors", Justification = "Don't tell me how to format documentation.")] 11 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "No")] 12 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File must have header", Justification = "No header file in this solution.")] 13 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1652:Enable XML documentation output", Justification = "Not enabling XML documentation.")] 14 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names must not begin with underscore", Justification = "I often use naming conventions with underscores.")] 15 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "I've made my choice")] -------------------------------------------------------------------------------- /src/core/IConfigure.cs: -------------------------------------------------------------------------------- 1 | namespace mssql_exporter.core 2 | { 3 | public interface IConfigure 4 | { 5 | /// 6 | /// Database Connection String 7 | /// 8 | string DataSource { get; set; } 9 | 10 | /// 11 | /// Path to the file containing metric configuration. 12 | /// 13 | string ConfigFile { get; set; } 14 | 15 | /// 16 | /// The text containing metric configuration. 17 | /// 18 | string ConfigText { get; set; } 19 | 20 | /// 21 | /// Default: "/metrics" 22 | /// 23 | /// metrics 24 | string ServerPath { get; set; } 25 | 26 | /// 27 | /// Default: "80" 28 | /// 29 | /// 80 30 | int ServerPort { get; set; } 31 | 32 | /// 33 | /// If true, adds default Prometheus Exporter metrics. 34 | /// 35 | /// false 36 | bool AddExporterMetrics { get; set; } 37 | 38 | /// 39 | /// Gets or sets the log file path. 40 | /// 41 | /// 42 | /// The log file path. 43 | /// 44 | public string LogFilePath { get; set; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/core/IQuery.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace mssql_exporter.core 4 | { 5 | public interface IQuery 6 | { 7 | /// 8 | /// The name of this query. 9 | /// 10 | string Name { get; } 11 | 12 | /// 13 | /// The query that returns results. 14 | /// 15 | string Query { get; } 16 | 17 | /// 18 | /// Query timeout in milliseconds. 19 | /// 20 | int? MillisecondTimeout { get; } 21 | 22 | /// 23 | /// Given a database dataset, gets metrics. 24 | /// 25 | /// May contain multiple tables 26 | void Measure(DataSet dataSet); 27 | 28 | /// 29 | /// Called if a timeout or exception occurs. 30 | /// 31 | void Clear(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/core/MetricQueryFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using mssql_exporter.core.config; 4 | using mssql_exporter.core.queries; 5 | using Serilog; 6 | 7 | namespace mssql_exporter.core 8 | { 9 | public static class MetricQueryFactory 10 | { 11 | public static IQuery GetSpecificQuery(Prometheus.MetricFactory metricFactory, MetricQuery metricQuery, ILogger logger) 12 | { 13 | logger.Information("Creating metric {Name}", metricQuery.Name); 14 | switch (metricQuery.QueryUsage) 15 | { 16 | case QueryUsage.Counter: 17 | var labelColumns = 18 | metricQuery.Columns 19 | .Where(x => x.ColumnUsage == ColumnUsage.CounterLabel) 20 | .Select(x => new CounterGroupQuery.Column(x.Name, x.Order ?? 0, x.Label)); 21 | 22 | var valueColumn = 23 | metricQuery.Columns 24 | .Where(x => x.ColumnUsage == ColumnUsage.Counter) 25 | .Select(x => new CounterGroupQuery.Column(x.Name, x.Order ?? 0, x.Label)) 26 | .FirstOrDefault(); 27 | 28 | return new CounterGroupQuery(metricQuery.Name, metricQuery.Description ?? string.Empty, metricQuery.Query, labelColumns, valueColumn, metricFactory, logger, metricQuery.MillisecondTimeout); 29 | 30 | case QueryUsage.Gauge: 31 | var gaugeLabelColumns = 32 | metricQuery.Columns 33 | .Where(x => x.ColumnUsage == ColumnUsage.GaugeLabel) 34 | .Select(x => new GaugeGroupQuery.Column(x.Name, x.Order ?? 0, x.Label)); 35 | 36 | var gaugeValueColumn = 37 | metricQuery.Columns 38 | .Where(x => x.ColumnUsage == ColumnUsage.Gauge) 39 | .Select(x => new GaugeGroupQuery.Column(x.Name, x.Order ?? 0, x.Label)) 40 | .FirstOrDefault(); 41 | 42 | return new GaugeGroupQuery(metricQuery.Name, metricQuery.Description ?? string.Empty, metricQuery.Query, gaugeLabelColumns, gaugeValueColumn, metricFactory, logger, metricQuery.MillisecondTimeout); 43 | 44 | case QueryUsage.Empty: 45 | var gaugeColumns = 46 | metricQuery.Columns 47 | .Where(x => x.ColumnUsage == ColumnUsage.Gauge) 48 | .Select(x => new GenericQuery.GaugeColumn(x.Name, x.Label, x.Description ?? x.Label, metricFactory, x.DefaultValue)) 49 | .ToArray(); 50 | 51 | var counterColumns = 52 | metricQuery.Columns 53 | .Where(x => x.ColumnUsage == ColumnUsage.Counter) 54 | .Select(x => new GenericQuery.CounterColumn(x.Name, x.Label, x.Description ?? x.Label, metricFactory)) 55 | .ToArray(); 56 | 57 | return new GenericQuery(metricQuery.Name, metricQuery.Query, gaugeColumns, counterColumns, logger, metricQuery.MillisecondTimeout); 58 | 59 | default: 60 | logger.Error("Failed to create query {Name}", metricQuery.Name); 61 | break; 62 | } 63 | 64 | throw new Exception("Undefined QueryUsage."); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/core/QueryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Data.SqlClient; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using mssql_exporter.core.config; 7 | using Serilog; 8 | 9 | namespace mssql_exporter.core 10 | { 11 | public static class QueryExtensions 12 | { 13 | public static async Task MeasureWithConnection(this IQuery query, ILogger logger, string sqlConnectionString, int defaultMillisecondTimeout) 14 | { 15 | var timeout = Math.Min(defaultMillisecondTimeout, query.MillisecondTimeout ?? 100_000_000); 16 | var tokenSource = new CancellationTokenSource(timeout).Token; 17 | 18 | var measureTask = Task.Run( 19 | () => 20 | { 21 | try 22 | { 23 | using (var sqlConnection = new SqlConnection(sqlConnectionString)) 24 | { 25 | sqlConnection.Open(); 26 | tokenSource.ThrowIfCancellationRequested(); 27 | 28 | using (var dataset = new DataSet()) 29 | { 30 | using (var command = new SqlCommand(query.Query, sqlConnection)) 31 | { 32 | tokenSource.ThrowIfCancellationRequested(); 33 | using (var adapter = new SqlDataAdapter 34 | { 35 | SelectCommand = command 36 | }) 37 | { 38 | adapter.Fill(dataset); 39 | tokenSource.ThrowIfCancellationRequested(); 40 | 41 | query.Measure(dataset); 42 | return MeasureResult.Success; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | catch (OperationCanceledException error) 49 | { 50 | logger.Error(error, "Query {Name} timed out", query.Name); 51 | query.Clear(); 52 | return MeasureResult.Timeout; 53 | } 54 | catch (Exception error) 55 | { 56 | query.Clear(); 57 | logger.Error(error, "Query {Name} failed", query.Name); 58 | return MeasureResult.Exception; 59 | } 60 | }, tokenSource); 61 | 62 | var delayTask = Task.Run(async () => 63 | { 64 | #pragma warning disable CA2007 // Do not directly await a Task 65 | await Task.Delay(timeout); 66 | #pragma warning restore CA2007 // Do not directly await a Task 67 | return MeasureResult.Timeout; 68 | }); 69 | 70 | #pragma warning disable CA2007 // Do not directly await a Task 71 | return await await Task.WhenAny(delayTask, measureTask); 72 | #pragma warning restore CA2007 // Do not directly await a Task 73 | } 74 | 75 | public static int GetColumnIndex(DataTable dataTable, string columnName) 76 | { 77 | for (int i = 0; i < dataTable.Columns.Count; i++) 78 | { 79 | if (dataTable.Columns[i].ColumnName.Equals(columnName, StringComparison.CurrentCulture)) 80 | { 81 | return i; 82 | } 83 | } 84 | 85 | throw new ArgumentOutOfRangeException(nameof(columnName), $"Expected to find column {columnName}"); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/core/config/ColumnUsage.cs: -------------------------------------------------------------------------------- 1 | namespace mssql_exporter.core.config 2 | { 3 | public enum ColumnUsage 4 | { 5 | Counter, 6 | CounterLabel, 7 | Gauge, 8 | GaugeLabel, 9 | Empty 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/core/config/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace mssql_exporter.core.config 4 | { 5 | public static class Constants 6 | { 7 | public const string USAGE_COLUMN_COUNTER = "Counter"; 8 | public const string USAGE_COLUMN_COUNTER_LABEL = "CounterLabel"; 9 | public const string USAGE_COLUMN_GUAGE = "Gauge"; 10 | public const string USAGE_COLUMN_GUAGE_LABEL = "GaugeLabel"; 11 | 12 | public const string USAGE_QUERY_COUNTER = "CountersWithLabels"; 13 | public const string USAGE_QUERY_GUAGE = "GaugesWithLabels"; 14 | 15 | public static ColumnUsage? GetColumnUsage(string usage) 16 | { 17 | if (string.IsNullOrWhiteSpace(usage)) 18 | { 19 | return ColumnUsage.Empty; 20 | } 21 | 22 | if (USAGE_COLUMN_COUNTER.Equals(usage, StringComparison.InvariantCultureIgnoreCase)) 23 | { 24 | return ColumnUsage.Counter; 25 | } 26 | 27 | if (USAGE_COLUMN_COUNTER_LABEL.Equals(usage, StringComparison.InvariantCultureIgnoreCase)) 28 | { 29 | return ColumnUsage.CounterLabel; 30 | } 31 | 32 | if (USAGE_COLUMN_GUAGE.Equals(usage, StringComparison.InvariantCultureIgnoreCase)) 33 | { 34 | return ColumnUsage.Gauge; 35 | } 36 | 37 | if (USAGE_COLUMN_GUAGE_LABEL.Equals(usage, StringComparison.InvariantCultureIgnoreCase)) 38 | { 39 | return ColumnUsage.GaugeLabel; 40 | } 41 | 42 | return null; 43 | } 44 | 45 | public static QueryUsage? GetQueryUsage(string usage) 46 | { 47 | if (USAGE_QUERY_COUNTER.Equals(usage, StringComparison.InvariantCultureIgnoreCase)) 48 | { 49 | return QueryUsage.Counter; 50 | } 51 | 52 | if (USAGE_QUERY_GUAGE.Equals(usage, StringComparison.InvariantCultureIgnoreCase)) 53 | { 54 | return QueryUsage.Gauge; 55 | } 56 | 57 | return null; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/core/config/MeasureResult.cs: -------------------------------------------------------------------------------- 1 | namespace mssql_exporter.core.config 2 | { 3 | public enum MeasureResult 4 | { 5 | Success, 6 | Timeout, 7 | Exception 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/core/config/MetricFile.cs: -------------------------------------------------------------------------------- 1 | namespace mssql_exporter.core.config 2 | { 3 | public class MetricFile 4 | { 5 | public MetricQuery[] Queries { get; set; } 6 | 7 | public int MillisecondTimeout { get; set; } = 10_000; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/core/config/MetricQuery.cs: -------------------------------------------------------------------------------- 1 | namespace mssql_exporter.core.config 2 | { 3 | public class MetricQuery 4 | { 5 | public string Name { get; set; } 6 | 7 | public string Description { get; set; } 8 | 9 | public string Query { get; set; } 10 | 11 | public MetricQueryColumn[] Columns { get; set; } 12 | 13 | public string Usage { get; set; } 14 | 15 | public QueryUsage QueryUsage => Constants.GetQueryUsage(this.Usage) ?? QueryUsage.Empty; 16 | 17 | public int? MillisecondTimeout { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/config/MetricQueryColumn.cs: -------------------------------------------------------------------------------- 1 | namespace mssql_exporter.core.config 2 | { 3 | public class MetricQueryColumn 4 | { 5 | public string Name { get; set; } 6 | 7 | public string Description { get; set; } 8 | 9 | public string Usage { get; set; } 10 | 11 | public string Label { get; set; } 12 | 13 | public int? Order { get; set; } 14 | 15 | public ColumnUsage ColumnUsage => Constants.GetColumnUsage(Usage) ?? ColumnUsage.Empty; 16 | 17 | public decimal? DefaultValue { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/config/Parser.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace mssql_exporter.core.config 4 | { 5 | public static class Parser 6 | { 7 | public static MetricFile FromJson(string text) 8 | { 9 | return JsonConvert.DeserializeObject(text); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/core/config/QueryUsage.cs: -------------------------------------------------------------------------------- 1 | namespace mssql_exporter.core.config 2 | { 3 | public enum QueryUsage 4 | { 5 | Counter, 6 | Gauge, 7 | Empty 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/core/core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | mssql_exporter.core 6 | mssql_exporter.core 7 | 10 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ..\..\..\..\..\..\Program Files\dotnet\sdk\NuGetFallbackFolder\system.data.sqlclient\4.5.1\ref\netcoreapp2.1\System.Data.SqlClient.dll 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/core/metrics/ConnectionUp.cs: -------------------------------------------------------------------------------- 1 | using mssql_exporter.core.queries; 2 | using Prometheus; 3 | using Serilog; 4 | 5 | namespace mssql_exporter.core.metrics 6 | { 7 | public class ConnectionUp : GenericQuery 8 | { 9 | private readonly ILogger _logger; 10 | 11 | public ConnectionUp(MetricFactory metricFactory, ILogger logger) 12 | #pragma warning disable CA1825 // Avoid zero-length array allocations. 13 | : base("mssql_up", "SELECT 1 mssql_up", new[] { new GaugeColumn("mssql_up", "mssql_up", "mssql_up", metricFactory, 0) }, new CounterColumn[] { }, logger, null) 14 | #pragma warning restore CA1825 // Avoid zero-length array allocations. 15 | { 16 | _logger = logger; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/queries/CounterGroupQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using Prometheus; 6 | using Serilog; 7 | 8 | namespace mssql_exporter.core.queries 9 | { 10 | #pragma warning disable CA1034 // Nested types should not be visible 11 | public class CounterGroupQuery : IQuery 12 | { 13 | private readonly IEnumerable _labelColumns; 14 | private readonly Counter _counter; 15 | private readonly string _description; 16 | private readonly Column _valueColumn; 17 | private readonly ILogger _logger; 18 | 19 | public CounterGroupQuery(string name, string description, string query, IEnumerable labelColumns, Column valueColumn, MetricFactory metricFactory, ILogger logger, int? millisecondTimeout) 20 | { 21 | Name = name; 22 | this._description = description; 23 | Query = query; 24 | this._valueColumn = valueColumn; 25 | _logger = logger; 26 | MillisecondTimeout = millisecondTimeout; 27 | this._labelColumns = labelColumns.OrderBy(x => x.Order).ToArray(); 28 | 29 | var counterConfiguration = new Prometheus.CounterConfiguration 30 | { 31 | LabelNames = this._labelColumns.Select(x => x.Label).ToArray() 32 | }; 33 | 34 | _counter = metricFactory.CreateCounter(name, description, counterConfiguration); 35 | } 36 | 37 | public string Name { get; } 38 | 39 | public string Query { get; } 40 | 41 | public int? MillisecondTimeout { get; } 42 | 43 | public void Clear() 44 | { 45 | // TODO: What should I do here? 46 | } 47 | 48 | public void Measure(DataSet dataSet) 49 | { 50 | var table = dataSet.Tables[0]; 51 | 52 | var columnIndices = _labelColumns.Select(x => QueryExtensions.GetColumnIndex(table, x.Name)); 53 | var valueIndex = QueryExtensions.GetColumnIndex(table, _valueColumn.Name); 54 | 55 | foreach (var row in table.Rows.Cast()) 56 | { 57 | var labels = columnIndices.Select(x => row.ItemArray[x].ToString().Trim()).ToArray(); 58 | if (double.TryParse(row.ItemArray[valueIndex].ToString(), out double result)) 59 | { 60 | _counter.WithLabels(labels).Set(result); 61 | } 62 | } 63 | } 64 | 65 | public class Column 66 | { 67 | public Column(string name, int order, string label) 68 | { 69 | if (string.IsNullOrWhiteSpace(name)) 70 | { 71 | throw new ArgumentException("Expected name argument", nameof(name)); 72 | } 73 | 74 | if (string.IsNullOrWhiteSpace(label)) 75 | { 76 | throw new ArgumentException("expected label argument", nameof(label)); 77 | } 78 | 79 | Name = name; 80 | Order = order; 81 | Label = label; 82 | } 83 | 84 | public string Name { get; } 85 | 86 | public int Order { get; } 87 | 88 | public string Label { get; } 89 | } 90 | } 91 | #pragma warning restore CA1034 92 | } 93 | -------------------------------------------------------------------------------- /src/core/queries/GaugeGroupQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using Prometheus; 6 | using Serilog; 7 | 8 | namespace mssql_exporter.core.queries 9 | { 10 | #pragma warning disable CA1034 // Nested types should not be visible 11 | public class GaugeGroupQuery : IQuery 12 | { 13 | private readonly IEnumerable _labelColumns; 14 | private readonly Prometheus.Gauge _gauge; 15 | private readonly string _description; 16 | private readonly Column _valueColumn; 17 | private readonly ILogger _logger; 18 | 19 | public GaugeGroupQuery(string name, string description, string query, IEnumerable labelColumns, Column valueColumn, MetricFactory metricFactory, ILogger logger, int? millisecondTimeout) 20 | { 21 | Name = name; 22 | this._description = description; 23 | Query = query; 24 | MillisecondTimeout = millisecondTimeout; 25 | this._valueColumn = valueColumn; 26 | _logger = logger; 27 | this._labelColumns = labelColumns.OrderBy(x => x.Order).ToArray(); 28 | 29 | var gaugeConfiguration = new Prometheus.GaugeConfiguration 30 | { 31 | LabelNames = this._labelColumns.Select(x => x.Label).ToArray(), 32 | SuppressInitialValue = true 33 | }; 34 | 35 | _gauge = metricFactory.CreateGauge(name, description, gaugeConfiguration); 36 | } 37 | 38 | public string Name { get; } 39 | 40 | public string Query { get; } 41 | 42 | public int? MillisecondTimeout { get; } 43 | 44 | public void Measure(DataSet dataSet) 45 | { 46 | var table = dataSet.Tables[0]; 47 | 48 | var columnIndices = _labelColumns.Select(x => QueryExtensions.GetColumnIndex(table, x.Name)); 49 | var valueIndex = QueryExtensions.GetColumnIndex(table, _valueColumn.Name); 50 | 51 | foreach (var row in table.Rows.Cast()) 52 | { 53 | var labels = columnIndices.Select(x => row.ItemArray[x].ToString().Trim()).ToArray(); 54 | if (double.TryParse(row.ItemArray[valueIndex].ToString(), out double result)) 55 | { 56 | _gauge.WithLabels(labels).Set(result); 57 | } 58 | } 59 | } 60 | 61 | public void Clear() 62 | { 63 | // TODO: What should I do here? 64 | } 65 | 66 | public class Column 67 | { 68 | public Column(string name, int order, string label) 69 | { 70 | if (string.IsNullOrWhiteSpace(name)) 71 | { 72 | throw new ArgumentException("Expected name argument", nameof(name)); 73 | } 74 | 75 | if (string.IsNullOrWhiteSpace(label)) 76 | { 77 | throw new ArgumentException("expected label argument", nameof(label)); 78 | } 79 | 80 | Name = name; 81 | Order = order; 82 | Label = label; 83 | } 84 | 85 | public string Name { get; } 86 | 87 | public int Order { get; } 88 | 89 | public string Label { get; } 90 | } 91 | } 92 | #pragma warning restore CA1034 93 | } 94 | -------------------------------------------------------------------------------- /src/core/queries/GenericQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using Prometheus; 4 | using Serilog; 5 | 6 | namespace mssql_exporter.core.queries 7 | { 8 | #pragma warning disable CA1034 // Nested types should not be visible 9 | public class GenericQuery : IQuery 10 | { 11 | private readonly ILogger _logger; 12 | 13 | public GenericQuery(string name, string query, GaugeColumn[] gaugeColumns, CounterColumn[] counterColumns, ILogger logger, int? millisecondTimeout) 14 | { 15 | _logger = logger; 16 | Name = name; 17 | Query = query; 18 | GaugeColumns = gaugeColumns; 19 | CounterColumns = counterColumns; 20 | MillisecondTimeout = millisecondTimeout; 21 | } 22 | 23 | public string Name { get; } 24 | 25 | public string Query { get; } 26 | 27 | public GaugeColumn[] GaugeColumns { get; } 28 | 29 | public CounterColumn[] CounterColumns { get; } 30 | 31 | public int? MillisecondTimeout { get; } 32 | 33 | public void Measure(DataSet dataSet) 34 | { 35 | foreach (var column in GaugeColumns) 36 | { 37 | column.Measure(dataSet); 38 | } 39 | 40 | foreach (var column in CounterColumns) 41 | { 42 | column.Measure(dataSet); 43 | } 44 | } 45 | 46 | public void Clear() 47 | { 48 | foreach (var column in GaugeColumns) 49 | { 50 | column.Clear(); 51 | } 52 | } 53 | 54 | public class GaugeColumn 55 | { 56 | private readonly Gauge _gauge; 57 | private readonly decimal? _defaultValue; 58 | 59 | public GaugeColumn(string name, string label, string description, MetricFactory metricFactory, decimal? defaultValue = 0) 60 | { 61 | if (string.IsNullOrWhiteSpace(name)) 62 | { 63 | throw new ArgumentException("Expected name argument", nameof(name)); 64 | } 65 | 66 | if (string.IsNullOrWhiteSpace(label)) 67 | { 68 | throw new ArgumentException("expected label argument", nameof(label)); 69 | } 70 | 71 | _defaultValue = defaultValue; 72 | Name = name; 73 | Label = label; 74 | _gauge = metricFactory.CreateGauge(label, description, new Prometheus.GaugeConfiguration()); 75 | } 76 | 77 | public string Name { get; } 78 | 79 | public string Label { get; } 80 | 81 | public void Measure(DataSet dataSet) 82 | { 83 | var table = dataSet.Tables[0]; 84 | if (table.Rows.Count >= 0) 85 | { 86 | var row = table.Rows[0]; 87 | var valueIndex = QueryExtensions.GetColumnIndex(table, Name); 88 | if (double.TryParse(row.ItemArray[valueIndex].ToString(), out double result)) 89 | { 90 | _gauge.Set(result); 91 | } 92 | } 93 | } 94 | 95 | public void Clear() 96 | { 97 | if (_defaultValue.HasValue) 98 | { 99 | _gauge.Set(Convert.ToDouble(_defaultValue.Value)); 100 | } 101 | } 102 | } 103 | 104 | public class CounterColumn 105 | { 106 | private readonly Counter _counter; 107 | 108 | public CounterColumn(string name, string label, string description, MetricFactory metricFactory) 109 | { 110 | if (string.IsNullOrWhiteSpace(name)) 111 | { 112 | throw new ArgumentException("Expected name argument", nameof(name)); 113 | } 114 | 115 | if (string.IsNullOrWhiteSpace(label)) 116 | { 117 | throw new ArgumentException("expected label argument", nameof(label)); 118 | } 119 | 120 | Name = name; 121 | Label = label; 122 | _counter = metricFactory.CreateCounter(label, description, new Prometheus.CounterConfiguration()); 123 | } 124 | 125 | public string Name { get; } 126 | 127 | public string Label { get; } 128 | 129 | public void Measure(DataSet dataSet) 130 | { 131 | var table = dataSet.Tables[0]; 132 | if (table.Rows.Count >= 0) 133 | { 134 | var row = table.Rows[0]; 135 | var valueIndex = QueryExtensions.GetColumnIndex(table, Name); 136 | if (double.TryParse(row.ItemArray[valueIndex].ToString(), out double result)) 137 | { 138 | _counter.Set(result); 139 | } 140 | } 141 | } 142 | } 143 | } 144 | #pragma warning restore CA1034 // Nested types should not be visible 145 | } 146 | -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.0", 4 | "rollForward": "latestMajor", 5 | "allowPrerelease": true 6 | } 7 | } -------------------------------------------------------------------------------- /src/mssql_exporter.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2019 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "core", "core\core.csproj", "{C65DEF09-4D8E-4393-9B0B-DFC9D63E9C2B}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "server", "server\server.csproj", "{5967A2AF-3277-40CC-BEA1-CAEC5E516EC1}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {C65DEF09-4D8E-4393-9B0B-DFC9D63E9C2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {C65DEF09-4D8E-4393-9B0B-DFC9D63E9C2B}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {C65DEF09-4D8E-4393-9B0B-DFC9D63E9C2B}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {C65DEF09-4D8E-4393-9B0B-DFC9D63E9C2B}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {5967A2AF-3277-40CC-BEA1-CAEC5E516EC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {5967A2AF-3277-40CC-BEA1-CAEC5E516EC1}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {5967A2AF-3277-40CC-BEA1-CAEC5E516EC1}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {5967A2AF-3277-40CC-BEA1-CAEC5E516EC1}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {58B84CF5-B34E-40BA-B388-13447112ABB7} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/server/ConfigurationOptions.cs: -------------------------------------------------------------------------------- 1 | using mssql_exporter.core; 2 | 3 | namespace mssql_exporter.server 4 | { 5 | public class ConfigurationOptions : IConfigure 6 | { 7 | public string DataSource { get; set; } 8 | 9 | public string ConfigFile { get; set; } = "metrics.json"; 10 | 11 | public string ConfigText { get; set; } 12 | 13 | public string ServerPath { get; set; } = "metrics"; 14 | 15 | public int ServerPort { get; set; } = 80; 16 | 17 | public bool AddExporterMetrics { get; set; } = false; 18 | 19 | public string LogLevel { get; set; } 20 | 21 | public string LogFilePath { get; set; } = "mssqlexporter-log.txt"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/server/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives must be placed correctly", Justification = "I don't like this.")] 7 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element must begin with upper-case letter", Justification = "Lot of places to fix this.")] 8 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "No")] 9 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File must have header", Justification = "No header file in this solution.")] 10 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1652:Enable XML documentation output", Justification = "Not enabling XML documentation.")] 11 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names must not begin with underscore", Justification = "I often use naming conventions with underscores.")] 12 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "My naming convention is purposeful.")] -------------------------------------------------------------------------------- /src/server/OnDemandCollector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using mssql_exporter.core; 6 | using mssql_exporter.core.config; 7 | using mssql_exporter.core.metrics; 8 | using Prometheus; 9 | using Serilog; 10 | 11 | namespace mssql_exporter.server 12 | { 13 | public class OnDemandCollector 14 | { 15 | private readonly string _sqlConnectionString; 16 | private readonly int _millisecondTimeout; 17 | private readonly ILogger _logger; 18 | private readonly CollectorRegistry _registry; 19 | private readonly Gauge _exceptionsGauge; 20 | private readonly Gauge _timeoutGauge; 21 | private readonly MetricFactory _metricFactory; 22 | private readonly IQuery[] _metrics; 23 | 24 | public OnDemandCollector(string sqlConnectionString, int millisecondTimeout, ILogger logger, CollectorRegistry registry, Func> configureAction) 25 | { 26 | _sqlConnectionString = sqlConnectionString; 27 | _millisecondTimeout = millisecondTimeout; 28 | _logger = logger; 29 | _registry = registry; 30 | _metricFactory = Metrics.WithCustomRegistry(registry); 31 | _registry.AddBeforeCollectCallback(UpdateMetrics); 32 | _metrics = 33 | configureAction(_metricFactory) 34 | .Append(new ConnectionUp(_metricFactory, logger)) 35 | .ToArray(); 36 | 37 | _exceptionsGauge = _metricFactory.CreateGauge("mssql_exceptions", "Number of queries throwing exceptions."); 38 | _timeoutGauge = _metricFactory.CreateGauge("mssql_timeouts", "Number of queries timing out."); 39 | } 40 | 41 | public void UpdateMetrics() 42 | { 43 | var results = Task.WhenAll(_metrics.Select(x => x.MeasureWithConnection(_logger, _sqlConnectionString, _millisecondTimeout)).ToArray()).ConfigureAwait(false).GetAwaiter().GetResult(); 44 | 45 | _exceptionsGauge?.Set(results.Count(x => x == MeasureResult.Exception)); 46 | 47 | _timeoutGauge?.Set(results.Count(x => x == MeasureResult.Timeout)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/server/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Hosting.WindowsServices; 9 | using mssql_exporter.core; 10 | using mssql_exporter.core.config; 11 | using Prometheus; 12 | using Serilog; 13 | 14 | namespace mssql_exporter.server 15 | { 16 | public static class Program 17 | { 18 | public static void Main(string[] args) 19 | { 20 | if ((args.Length >= 1 && args[0].Equals("serve", StringComparison.CurrentCulture)) || WindowsServiceHelpers.IsWindowsService()) 21 | { 22 | RunWebServer(args.Where(a => !string.Equals("serve", a, StringComparison.InvariantCultureIgnoreCase)).ToArray()); 23 | } 24 | else 25 | { 26 | Help(); 27 | } 28 | } 29 | 30 | /// 31 | /// dotnet run -- serve -ConfigFile "../../test.json" -DataSource "Server=tcp:{ YOUR DATABASE HERE },1433;Initial Catalog={ YOUR INITIAL CATALOG HERE };Persist Security Info=False;User ID={ USER ID HERE };Password={ PASSWORD HERE };MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" 32 | /// 33 | public static void Help() 34 | { 35 | Console.WriteLine("Commands"); 36 | Console.WriteLine(" help"); 37 | Console.WriteLine(" serve"); 38 | Console.WriteLine(" -DataSource (Connection String)"); 39 | Console.WriteLine(" -ConfigFile (metrics.json)"); 40 | Console.WriteLine(" -ServerPath (/metrics)"); 41 | Console.WriteLine(" -ServerPort (80)"); 42 | Console.WriteLine(" -AddExporterMetrics (false)"); 43 | Console.WriteLine(" -ConfigText ()"); 44 | Console.WriteLine(string.Empty); 45 | Console.WriteLine("Or environment variables:"); 46 | Console.WriteLine(" PROMETHEUS_MSSQL_DataSource"); 47 | Console.WriteLine(" PROMETHEUS_MSSQL_ConfigFile"); 48 | Console.WriteLine(" PROMETHEUS_MSSQL_ServerPath"); 49 | Console.WriteLine(" PROMETHEUS_MSSQL_ServerPort"); 50 | Console.WriteLine(" PROMETHEUS_MSSQL_AddExporterMetrics"); 51 | Console.WriteLine(" PROMETHEUS_MSSQL_ConfigText"); 52 | Console.WriteLine(" PROMETHEUS_MSSQL_Serilog__MinimumLevel"); 53 | } 54 | 55 | public static void RunWebServer(string[] args) 56 | { 57 | var switchMappings = new Dictionary 58 | { 59 | {"-DataSource", "DataSource"}, 60 | {"-ConfigFile", "ConfigFile"}, 61 | {"-ServerPath", "ServerPath"}, 62 | {"-ServerPort", "ServerPort"}, 63 | {"-AddExporterMetrics", "AddExporterMetrics"}, 64 | {"-ConfigText", "ConfigText"} 65 | }; 66 | 67 | var config = new ConfigurationBuilder() 68 | .AddJsonFile("config.json", true, false) 69 | .AddJsonFile("appsettings.json", true, false) 70 | .AddEnvironmentVariables("PROMETHEUS_MSSQL_") 71 | .AddCommandLine(args, switchMappings) 72 | .Build(); 73 | IConfigure configurationBinding = new ConfigurationOptions(); 74 | config.Bind(configurationBinding); 75 | 76 | Log.Logger = new LoggerConfiguration() 77 | .MinimumLevel.Warning() 78 | .Enrich.FromLogContext() 79 | //Logging setup above is a default in case load from configuration doesn't override. 80 | .ReadFrom.Configuration(config) 81 | .CreateLogger(); 82 | 83 | Log.Logger.Information("ServerPath {ServerPath}; ServerPort {ServerPort}; AddExporterMetrics {AddExporterMetrics}", configurationBinding.ServerPath, configurationBinding.ServerPort, configurationBinding.AddExporterMetrics); 84 | if (string.IsNullOrWhiteSpace(configurationBinding.DataSource)) 85 | { 86 | Log.Logger.Error("Expected DataSource: SQL Server connectionString"); 87 | return; 88 | } 89 | 90 | MetricFile metricFile; 91 | if (string.IsNullOrWhiteSpace(configurationBinding.ConfigText)) 92 | { 93 | var filePath = TryGetAbsolutePath(configurationBinding.ConfigFile); 94 | try 95 | { 96 | var fileText = System.IO.File.ReadAllText(filePath); 97 | Log.Logger.Information("Reading ConfigText {ConfigText} from {FileName}", fileText, filePath); 98 | metricFile = Parser.FromJson(fileText); 99 | } 100 | catch (Exception e) 101 | { 102 | Log.Logger.Error(e, "Failed to read and parse text from {FileName}", filePath); 103 | throw; 104 | } 105 | } 106 | else 107 | { 108 | try 109 | { 110 | Log.Logger.Information("Parsing ConfigText {ConfigText}", configurationBinding.ConfigText); 111 | metricFile = Parser.FromJson(configurationBinding.ConfigText); 112 | } 113 | catch (Exception e) 114 | { 115 | Log.Logger.Error(e, "Failed to parse text from ConfigText"); 116 | throw; 117 | } 118 | } 119 | 120 | var registry = (configurationBinding.AddExporterMetrics) 121 | ? Metrics.DefaultRegistry 122 | : new CollectorRegistry(); 123 | 124 | var collector = ConfigurePrometheus(configurationBinding, Log.Logger, metricFile, registry); 125 | 126 | CreateHostBuilder(args, configurationBinding, registry).Build().Run(); 127 | } 128 | 129 | public static IHostBuilder CreateHostBuilder(string[] args, IConfigure configurationBinding, 130 | CollectorRegistry registry) 131 | { 132 | var defaultPath = 133 | "/" + configurationBinding.ServerPath.Replace("/", string.Empty, 134 | StringComparison.CurrentCultureIgnoreCase); 135 | if (defaultPath.Equals("/", StringComparison.CurrentCultureIgnoreCase)) 136 | { 137 | defaultPath = string.Empty; 138 | } 139 | 140 | return Host.CreateDefaultBuilder(args) 141 | .ConfigureWebHostDefaults(builder => builder 142 | .Configure(applicationBuilder => applicationBuilder.UseMetricServer(defaultPath, registry)) 143 | .UseUrls($"http://*:{configurationBinding.ServerPort}")) 144 | .UseWindowsService() 145 | .UseSerilog(); 146 | } 147 | 148 | public static IEnumerable ConfigureMetrics(core.config.MetricFile metricFile, 149 | MetricFactory metricFactory, ILogger logger) 150 | { 151 | return metricFile.Queries.Select(x => MetricQueryFactory.GetSpecificQuery(metricFactory, x, logger)); 152 | } 153 | 154 | public static OnDemandCollector ConfigurePrometheus(IConfigure configure, ILogger logger, 155 | core.config.MetricFile metricFile, CollectorRegistry registry) 156 | { 157 | return new OnDemandCollector( 158 | configure.DataSource, 159 | metricFile.MillisecondTimeout, 160 | logger, 161 | registry, 162 | metricFactory => ConfigureMetrics(metricFile, metricFactory, logger)); 163 | } 164 | 165 | private static string TryGetAbsolutePath(string path) 166 | { 167 | if (string.IsNullOrEmpty(path)) 168 | { 169 | return path; 170 | } 171 | 172 | if (Path.IsPathFullyQualified(path)) 173 | { 174 | return path; 175 | } 176 | 177 | return Path.Combine(AppContext.BaseDirectory, path); 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /src/server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "Using": [ "Serilog.Sinks.Console" ], 4 | "MinimumLevel": "Information", 5 | "WriteTo": [ 6 | { "Name": "Console" } 7 | ], 8 | "Enrich": [ "FromLogContext" ], 9 | "Properties": { 10 | "Application": "MssqlExporter" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/server/metrics.json: -------------------------------------------------------------------------------- 1 | { 2 | "Queries": [ 3 | { 4 | "Name": "mssql_process_status", 5 | "Query": "SELECT status, COUNT(*) count FROM sys.sysprocesses GROUP BY status", 6 | "Description": "Counts the number of processes per status", 7 | "Usage": "GaugesWithLabels", 8 | "Columns": [ 9 | { 10 | "Name": "status", 11 | "Label": "status", 12 | "Usage": "GaugeLabel", 13 | "Order": 0 14 | }, 15 | { 16 | "Name": "count", 17 | "Label": "count", 18 | "Usage": "Gauge" 19 | } 20 | ] 21 | }, 22 | { 23 | "Name": "mssql_process_connections", 24 | "Query": "SELECT ISNULL(DB_NAME(dbid), 'other') as dbname, COUNT(dbid) as connections FROM sys.sysprocesses WHERE dbid > 0 GROUP BY dbid", 25 | "Description": "Counts the number of connections per db", 26 | "Usage": "GaugesWithLabels", 27 | "Columns": [ 28 | { 29 | "Name": "dbname", 30 | "Label": "dbname", 31 | "Usage": "GaugeLabel", 32 | "Order": 0 33 | }, 34 | { 35 | "Name": "connections", 36 | "Label": "count", 37 | "Usage": "Gauge" 38 | } 39 | ] 40 | }, 41 | { 42 | "Name": "mssql_deadlocks", 43 | "Query": "SELECT cntr_value FROM sys.dm_os_performance_counters where counter_name = 'Number of Deadlocks/sec' AND instance_name = '_Total'", 44 | "Description": "Number of lock requests per second that resulted in a deadlock since last restart", 45 | "Columns": [ 46 | { 47 | "Name": "cntr_value", 48 | "Label": "mssql_deadlocks", 49 | "Usage": "Gauge", 50 | "DefaultValue": 0 51 | } 52 | ] 53 | } 54 | ], 55 | "MillisecondTimeout": 4000 56 | } -------------------------------------------------------------------------------- /src/server/server.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | Exe 6 | mssql_exporter 7 | mssql_exporter.server 8 | win-x64;linux-x64 9 | 10 10 | Linux 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | docker-compose-pull.yml 36 | 37 | 38 | docker-compose.yml 39 | 40 | 41 | Dockerfile 42 | 43 | 44 | 45 | 46 | --------------------------------------------------------------------------------