#はじめに
「PostgreSQLの統計情報を可視化」の3章です。この章では2章の「バックエンド編」で作成したAPIを使ってPostgreSQLの統計情報をグラフ化する、フロントエンドにフォーカスして話を進めます。
フロントエンドのモジュール構成
Nuxt.jsのフロントエンド側を大雑把に分けると、画面構成モジュールと、データ更新・保持モジュールがあります。まずはインターフェース仕様が決まっている「データ更新・保持モジュール」から作成します。
データ更新・保持モジュール
データを更新・保持するモジュールには、動的なデータの読み出し、書込み、保持を定義するstoreモジュール、静的なデータを保持するassetsモジュールがあります
バックエンドとの動的なデータ送受信にはaxiosを使います。
動的なデータの読み書き、保持を行いますので、storeモジュールに記述します。
###storeモジュール
storeモジュールは以下の4つの定義から構成されます。
名称 | 役割 |
---|---|
state | 保持するデータを定義 |
getters | データ読み出し処理 |
mutations | データ書込み処理 |
actions | データ取得・操作 |
バックエンドのAPIを呼び出して統計情報を取得・保持するsoteモジュールのプログラムは以下の様になります。
/**
* 統計情報取得Storeモジュール
*/
import Vue from 'vue'
import Vuex from 'vuex'
import * as api from './api'
Vue.use(Vuex)
export const state = () => ({
dbSize: [], // DBサイズ
slowQuery: [], // スロークエリ
sqlCall: null // SQL実行回数
})
// Getters
export const getters = {
getDbSize (state) {
return state.dbSize
},
getSlowQuery (state) {
return state.slowQuery
},
getSqlCall (state) {
return state.sqlCall
}
}
// Mutations
export const mutations = {
setDbSize (state, dbSize) {
state.dbSize = dbSize
},
setSlowQuery (state, slowQuery) {
state.slowQuery = slowQuery
},
setSqlCall (state, sqlCall) {
state.sqlCall = sqlCall
}
}
// Actions
export const actions = {
// DBサイズ取得
async doGetDbSize (params = null) {
const res = await api.getApi('/statistics/getDbSize')
if (res.status === 200) {
this.commit('statistics/setDbSize', res.data.value)
} else {
this.commit('statistics/setDbSize', [])
}
},
// SQL実行回数取得
async doGetSqlCall (params = null) {
const res = await api.getApi('/statistics/getSqlCalls')
if (res.status === 200) {
this.commit('statistics/setSqlCall', res.data)
} else {
this.commit('statistics/setSqlCall', null)
}
},
// スロークエリ取得
async doGetSlowQuery (params = null, data) {
const threshold = data.threshold || 1000
const limit = data.limit || 20
const arg = 'threshold=' + threshold + '&limit=' + limit
const res = await api.getApi('/statistics/getSlowQuery', arg)
if (res.status === 200) {
this.commit('statistics/setSlowQuery', res.data.value)
} else {
this.commit('statistics/setSlowQuery', [])
}
}
}
ここではaxsiosを使ってAPIの送受信処理を、共通モジュールのapi.jsとして定義しています。
/**
* API共通モジュール
* POSTとGETのインターフェース共通化
*/
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
// POST呼び出し共通処理
export const postApi = async function(uri, data = null) {
try {
var res = await axios.post(process.env.apiUrl + uri , data)
return res
} catch (e) {
return {status: 500}
}
}
//GET呼び出し共通処理
export const getApi = async function(uri, options = null) {
var param = (options != null) ? '?' + options : ''
try {
var res = await axios.get(process.env.apiUrl + uri + param)
return res
} catch (e) {
return {status: 500}
}
}
##画面を構成するモジュール
さて、いよいよ画面作成です。
Nuxtの画面を構成するモジュールには、画面の共通的な外観を定義するlayoutsモジュール、画面の構成を定義するpagesモジュール、画面の共通部品を定義するcomponentsモジュールがあります。
今回は可視化の為にグラフ作成を行いますので、グラフ描画コンポーネントを自動読込するためにpluginsモジュールも使います。
描画コンポーネントにはマテリアルコンポーネントフレームワークのvuetify.jsを使います。
・Vuetifyの公式サイト https://vuetifyjs.com/ja/
vuetifyのマテリアルデザインアイコンには種類が豊富なMaterial Design Iconsを使います。
・Material Design Iconsの公式サイト https://materialdesignicons.com/
グラフ作成のパッケージは色々ありますが、今回は直感的に使いやすいApexchartsとvue用のコンポーネントであるVue-Apexchartsを使います。
・Apexchartsの公式サイト https://apexcharts.com/
・Vue Apexchartsの公式サイト https://vuejsprojects.com/vue-apexcharts
###pluginsモジュール
画面描画用のvuetifyとvue-apexchartsのコンポーネント読み込み用のコードをプラグインに作成します。
import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import '@mdi/font/css/materialdesignicons.css'
Vue.use(Vuetify)
export default (ctx) => {
const vuetify = new Vuetify({
theme: {
dark: false // darkのテーマも格好いいですが、グラフの色が見づらくなるのでfalseにしました
},
icons: {
iconfont: 'mdi' // iconは種類が豊富なmdiを使います
}
})
ctx.app.vuetify = vuetify
ctx.$vuetify = vuetify.framework
}
import Vue from 'vue'
import VueApexCharts from 'vue-apexcharts'
Vue.use({
install (Vue, options) {
Vue.component('apexchart', VueApexCharts)
}
})
作成したプラグインをnuxt.config.jsに登録します。
(省略)
plugins: [
'@plugins/vuetify',
{
src: '@plugins/vue-apexcharts',
ssr: false
}
],
(省略)
###layoutsモジュール
全画面共通のレイアウトに以下のメニューを用意して画面が切り替わるようにしたいと思います。
メニュー | mdiアイコン | 表示ページ |
---|---|---|
データベース容量 | mdi-database | /db_size |
アクセス負荷 | mdi-align-vertical-bottom | /access_load |
スロークエリ | mdi-tortoise | /slow_query |
<template>
<v-app>
<v-navigation-drawer
v-model="drawer"
:mini-variant="miniVariant"
:clipped="clipped"
fixed
app
>
<v-list>
<v-list-item
v-for="(item, i) in items"
:key="i"
:to="item.to"
router
exact
>
<v-list-item-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title v-text="item.title" />
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-app-bar
:clipped-left="clipped"
fixed
app
>
<v-app-bar-nav-icon @click.stop="drawer = !drawer" />
<v-toolbar-title v-text="title" />
</v-app-bar>
<v-content>
<v-container>
<nuxt />
</v-container>
</v-content>
<v-footer
:fixed="fixed"
app
>
<span>© 2019</span>
</v-footer>
</v-app>
</template>
<script>
import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
Vue.use(Vuetify)
export default {
data () {
const $t = this.$t.bind(this)
return {
clipped: false,
drawer: false,
fixed: false,
miniVariant: false,
right: true,
rightDrawer: false,
title: $t('label.pgbmon'),
items: [
{
icon: 'mdi-database',
title: $t('label.dbSize'),
to: '/db_size'
},
{
icon: 'mdi-align-vertical-bottom',
title: $t('label.accessLoad'),
to: '/access_load'
},
{
icon: 'mdi-tortoise',
title: $t('label.slowQuery'),
to: '/slow_query'
}
]
}
}
}
</script>
国際化対応ファイルに日本語の文字列を登録します。
{
"label": {
"accessLoad": "アクセス負荷",
"db": "データベース",
"dbSize": "データベース容量",
"homePage": "トップページ",
"host": "ホスト",
"otherError": "エラー",
"pageNotFound": "ページはありません",
"pgbmon": "Pg バランスモニター",
"slowQuery": "スロークエリ"
},
"button": {},
"tooltip": {
"menu": "メニュー"
},
"message": {
"otherError": "エラーが発生しました",
"pageNotFound": "指定のページはありません"
}
}
pagesモジュール
とりあえず、空のindx.vueを用意します。
<template>
<v-layout column justify-center align-center>
<v-flex xs12 sm8 md6>
<v-card />
</v-flex>
</v-layout>
</template>
<script>
export default {
layout: 'monitor'
}
</script>
###DBサイズの可視化画面
ここでは、個々のDBサイズを見るのではなく、突出して肥大化したDBはないか確認のため、DB間のサイズのバランスを可視化したいと思います。
<template>
<v-container fluid fill-height>
<v-layout
align-center
row wrap
>
<v-flex xs12 md6>
<v-card class="ma-2 pa-3">
<apexchart type="donut" height="380" :options="chartOptions" :series="series" />
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
name: 'DbSizeChart',
layout: 'monitor',
data () {
const $t = this.$t.bind(this)
return {
series: [],
chartOptions: {
labels: this.getLabel(),
dataLabels: {
enable: false
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
value: {
formatter: function (val) {
return String(val).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,')
}
}
}
}
}
},
title: {
text: $t('label.dbSize')
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 200
},
legend: {
position: 'bottom'
}
}
}]
}
}
},
async created () {
await this.initialize()
},
methods: {
async initialize () {
await this.$store.dispatch('statistics/doGetDbSize')
this.series = this.getVal()
this.chartOptions.labels = this.getLabel()
},
getVal () {
const dbSize = this.$store.getters['statistics/getDbSize']
const val = []
for (let i = 0; i < dbSize.length; i++) {
val.push(Number(dbSize[i].result[0].pg_database_size))
}
return val
},
getLabel () {
const dbSize = this.$store.getters['statistics/getDbSize']
const label = []
for (let i = 0; i < dbSize.length; i++) {
label.push(dbSize[i].db + '@' + dbSize[i].host)
}
return label
}
}
}
</script>
こんな感じでDB間のDBサイズがドーナツグラフで表示されます。
多少、偏りが見られるようです。。。
###クエリの種類と数の可視化画面
次に、DB間でSELECT/INSERT/UPDATE等のクエリに偏りがないか、確認するためのグラフを作成したいと思います。
<template>
<v-container fluid fill-height>
<v-layout
align-center
row wrap
>
<v-flex v-for="(item, i) in series"
:key="i"
xs12 md4
>
<v-card class="item ma-2 pa-3">
<apexchart type="radar" height="350" :options="chartOptions[i]" :series="item" />
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
name: 'AccessLoadChart',
layout: 'monitor',
data () {
return {
// グラフの色を5種類用意します。
color: ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0'],
series: [],
chartOptions: []
}
},
async created () {
await this.initialize()
},
methods: {
async initialize () {
await this.$store.dispatch('statistics/doGetSqlCall')
const data = this.$store.getters['statistics/getSqlCall']
var series = []
const label = []
for (let i = 0; i < data.value.length; i++) {
label.push(data.value[i].db + '@' + data.value[i].host)
const val = data.value[i].result
for (let j = 0; j < val.length; j++) {
var call = []
if (Array.isArray(series[val[j].sql])) {
call = series[val[j].sql]
}
if (val[j].call !== null) {
call.push(Number(val[j].call))
} else {
call.push(0)
}
series[val[j].sql] = call
}
}
this.series = []
this.chartOptions = []
var i = 0
for (var key in series) {
var val = series[key]
var index = i % this.color.length
this.series.push([{ name: key, data: val }])
var option = {
title: { text: key },
labels: label,
fill: {
colors: [this.color[index]]
}
}
this.chartOptions.push(option)
i++
}
}
}
}
</script>
こんな感じでDB間のSELECT/INSERT/UPDATEクエリの実行数がレーダーチャート表示されます。
クエリはバランスよく分散されているようです。
###スロークエリの可視化
最後にDBによって処理負荷に偏りがないか、DB間のスロークエリを比較するグラフを作成したいと思います。
<template>
<v-container fluid fill-height>
<v-layout align-center row wrap>
<v-flex xs12 md12>
<v-card class="item ma-2 pa-3">
<p class="font-weight-black">{{ title }}</p>
<apexchart type="bar" height="400" :options="chartOptions" :series="series" />
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
layout: 'monitor',
data () {
return {
threshold: process.env.threshold,
limit: process.env.limit,
series: [],
chartOptions: {
plotOptions: {
bar: {
barHeight: '100%',
horizontal: false
}
},
dataLabels: {
enabled: false
},
xaxis: {
labels: { show: false }
}
},
title: 'slow query'
}
},
async created () {
await this.initialize()
},
methods: {
async initialize () {
this.series = []
await this.$store.dispatch('statistics/doGetSlowQuery', { threshold: this.threshold, limit: this.limit })
const data = this.$store.getters['statistics/getSlowQuery']
for (let i = 0; i < data.length; i++) {
var maxTime = []
var label = []
const val = data[i].result
for (let j = 0; j < val.length; j++) {
maxTime.push(Math.round(val[j].max_time))
label.push(String(val[j].calls))
}
var name = data[i].db + '@' + data[i].host
this.series.push({ name: name, data: maxTime })
}
}
}
}
</script>
こんな感じで登録したDBのスロークエリーの時間が棒グラフで表示されます。
全部バランスよく、遅いクエリーがあるようです。。。
##まとめ
ここまで簡単ですが、PostgreSQLの統計情報を可視化するコードを書いてみました。
統計情報の可視化は以前から興味がありましたので、軽い気持ちで始めてみましたが、思いのほか簡単に綺麗なグラフが出る事に驚きました。
今後、DBのデモなどに使えると良いなと思っています。
単体テストをやってみたい方は次の章にお進みください。
>>PostgreSQLの統計情報を可視化(単体でスト編)
なお、コード全体をご覧になりたい方はGitHubからダウンロードできるようにしてみましたので、ご利用ください。
GitHub https://github.com/at-mitani/pgbmon