├── .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 | | [](https://github.com/DanielOliver/mssql_exporter/actions/workflows/dockerimage.yaml) | [](https://github.com/DanielOliver/mssql_exporter/releases/latest) | [](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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/.run/server.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
--------------------------------------------------------------------------------