2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

新卒から1.5年のぺーぺーがサーバー点検の半自動化ツール作ってみた。

Last updated at Posted at 2019-10-25

経歴

c#とPl/SQLで基幹システムをやって1.5年ほど経ちました。
その中で、サーバーの点検業務を行ってて、Windowsサーバーにはやれリモートで見に行く
やれDBに接続して直接DBの状態を確認すると全て私の現場では手作業で行っていました。

そんなん無駄じゃね??

はい、ということでルーチンワークでサーバーの点検を行っている中で自動化しないと時間が無駄に使われて行ってしまうので、私が現状持っている c#とPL/SQlの知識をフル動員して頑張って作ってみました。

※機密みたいな部分が全部省いて大まかな大きいロジックだけ紹介します(許して)

実装した機能

EventViewerとCPU使用率の監視
Oracleの表領域の状態取得
Oracle DataPumpのファイルやログの監視

おおきく私がやっていた点検はWindows

Logic

EntryPointの自作

エントリーポイントって既存のシステムを改修する中で既に完成していたりするので中々、開発できませんよね。私は無駄にこだわって多重起動の防止の処理とか書いてみました。

下記の処理で、多重起動を行ったらエラーが出てexeが動かない仕組みです。

ここで多重起動を発生させるサーバー点検でDBやサーバーに接続するときに負担がバリかかってしまうので阻止させてもらいました。

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using MultiTaskServerInspection.UI;
using System.Threading;

namespace MultiTaskServerInspection
{
    static class EntryPoint
    {
        public  static MainForm mf = null;
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            string MutexName = "ServerInspectionTool";
            Mutex  mutex = new Mutex(false, MutexName);

            bool hasHandle = false;
           
            try
            {
                try
                {
                    hasHandle = mutex.WaitOne(0, false);
                }
                catch (AbandonedMutexException)
                {
                    hasHandle = true;
                }

                if (hasHandle == false)
                {
                    MessageBox.Show("多重起動できません",
                            "エラー",
                            MessageBoxButtons.OK,
                            MessageBoxIcon.Error
                            );
                    return;
                }
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new MainForm());
            }
            finally
            {
                if (hasHandle)
                {
                    mutex.ReleaseMutex();
                }
                mutex.Close();
            }
        }
    }
}

Oracleの表領域の取得

ここはちょっと頑張りました。

前からDBの接続設定やDBの接続のクラスを自作してみたかったのでたっぷり自己満足してみました。

私が調査した時点ではC#でOracleDBへの接続を行うメソッドを作っている人が見当たらなかったので苦戦しました。
接続までは皆さんやっているようで、その先のトランザクション回りやSQLの実装までのメソッドは中々ありませんね。c#とOracleの組み合わせが珍しいのでしょうか??

DBController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Oracle.DataAccess.Client;
using System.Windows.Forms;
using System.Data;

namespace MultiTaskServerInspection
{
    class DBController
    {
        private OracleConnection cnn = new OracleConnection();
        private OracleCommand cmd = new OracleCommand();
        private OracleTransaction txn = null;
        //接続部分を担っているメソッドでDBのユーザーとパスワードとインスタンス名を引数に入れれば使えます。
        public void DBConnection(string DATABASE_USER, string DATABASE_PASSWD, string DATABASE_SOURCE)
        {
            try
            {
                string conString = "user id=" + DATABASE_USER
                    + ";password=" + DATABASE_PASSWD
                    + ";data source=" + DATABASE_SOURCE;

                cnn.ConnectionString = conString;
                cnn.Open();
            }
            catch (OracleException ex)
            {
                MessageBox.Show(ex.Message);
                MessageBox.Show((ex.Number).ToString());
            }
        }
        //DBの接続を終了するメソッド
        public void DBClose()
        {
            try
            {
                cnn.Close();
            }
            catch (OracleException ex)
            {
                MessageBox.Show(ex.Message);
                MessageBox.Show((ex.Number).ToString());
            }
        }
    //トランザクション生成メソッド
        public void BeginTran()
        {
            txn = cnn.BeginTransaction();
        }
        
        //コミットメソッド
        public void CommitTran()
        {
            txn.Commit();
        }
        
        //ロールバックメソッド
        public void Rollback()
        {
            txn.Rollback();
        }

        //SQL実行メソッド
        //近いうちストアド用のメソッドも実装します
        public DataTable ExecuteQuery(string query)
        {
            OracleCommand cmd = new OracleCommand(query, cnn);
            OracleDataAdapter oda = new OracleDataAdapter(cmd);
            DataTable dt = new DataTable();
            cmd.ExecuteNonQuery();
            oda.Fill(dt);
            return dt;
        }
    }
}

DBの設定はXML形式で記述しました。
ソースコードにそのまま実装されていると読む側が設定報を見に行きにくいと考えました。
設定画面の読み込みはXML形式で行われ、DBの追加はXMLの中のタグを増やすことで対応できます。

CommonMethod.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Linq;

namespace MultiTaskServerInspection.Common
{
    class CommonMethod
    {
        private string XML_FILE_NAME = @"\ServerInspec.xml";
        public bool IsDmpCheck(string DB, int NodeType)
        {
            try
            {
                string Node = string.Empty;
                int i = 0;
                XElement xml = XElement.Load(Application.StartupPath + XML_FILE_NAME);

                if (NodeType == 1)
                {
                    
                    Node = "ExpLog";
                }
                else if (NodeType == 2)
                {
                    Node = "Exp";
                }
                else if (NodeType == 3)
                {
                    Node = "Imp";
                }
                else{
                    Node = "ImpLog";
                }
                IEnumerable<String> infos = from item in xml.Elements("dmpConfig").Elements(DB).Elements(Node) select item.Value;
                if (infos.All(info => info == "1"))
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
}

設定のXMLファイルです。

のタグはDBの接続設定の情報を記載します。

はのちにお話ししますが、Oracleの機能であるDataDumpの機能のONとOFFを記録する部分です。

ServerInspec.xml
<ServerInspec>
<dbConfig>
 <MainDB>
  <DataBase>本番サーバーインスタンス名</DataBase>
  <User>本番サーバーユーザー名</User>
  <PassWord>本番サーバーパスワード</PassWord>
 </MainDB>
 <SubDB>
  <DataBase>ミラーサーバーインスタンス名</DataBase>
  <User>ミラーサーバーユーザー名</User>
  <PassWord>ミラーサーバーパスワード</PassWord>
 </SubDB>
 <DevDB>
  <DataBase>開発版サーバーインスタンス名</DataBase>
  <User>開発版サーバーユーザー名</User>
  <PassWord>開発版サーバーパスワード</PassWord>
 </DevDB>
</dbConfig>
<dmpConfig>
 <MainDB>
   <Exp>1</Exp>
   <Imp>0<Imp>
   <ExpLog>1</ExpLog>
   <ImpLog>0</ImpLog>
 </MainDB>
 <SubDB>
   <Exp>1</Exp>
   <Imp>1<Imp>
   <ExpLog>1</ExpLog>
   <ImpLog>1</ImpLog>
 </SubDB>
 <DevDB>
   <Exp>1</Exp>
   <Imp>0<Imp>
   <ExpLog>1</ExpLog>
   <ImpLog>0</ImpLog>
 </DevDB>
</dmpConfig>
</ServerInspec>

最後にこのメソッドの使用方法の例です。

ストアドプロシージャにしなかった理由は、DBは常に変化するので勝手に削除されないようにローカルでSQLを持っておこうということになって
StringBuilderクラスを利用してDBにSQLを投げています。

SQLの内容はOracleDb内で使用しているユーザーが作成したスキーマとSYSTEMとSYSAUXの表領域をGB単位で取得するというメソッドです。

このメソッドでDataTableに格納してC#内で使用します。

DBRS.cs

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Oracle.DataAccess.Client;

namespace MultiTaskServerInspection
{
    class DBRS
    {
        public DataTable TablespaceStatement(string UserName, string Passwd, string InstanceName)
        {
            DBController controller = new DBController();
            StringBuilder query = new StringBuilder(128);
            DataTable dt = null;
            try
            {
                controller.DBConnection(UserName, Passwd, InstanceName);
                query.Append("select ");
                query.Append("x.TABLESPACE_NAME,");
                query.Append("x.free_GByte,");
                query.Append("trunc(sum(u.bytes / power(1024, 3)), 3) as Total_Gbyte ");
                query.Append("from ");
                query.Append("(select ");
                query.Append("t.TABLESPACE_NAME,");
                query.Append("trunc(sum(t.bytes / power(1024, 3)), 2) as free_GByte ");
                query.Append("from dba_free_space t ");
                query.Append("group by t.TABLESPACE_NAME");
                query.Append(")x ");
                query.Append("left outer join dba_data_files u on x.TABLESPACE_NAME = u.TABLESPACE_NAME ");
                query.Append("where x.TABLESPACE_NAME in ('データ格納スキーマ', 'SYSTEM', 'SYSAUX') ");
                query.Append("group by ");
                query.Append("x.TABLESPACE_NAME,");
                query.Append("x.free_GByte");
                dt = controller.ExecuteQuery(query.ToString());
                return dt;
            }
            catch (OracleException ex)
            {
                throw ex;
            }
            finally
            {
                dt = null;
                if(controller != null)
                {
                    controller.DBClose();
                }
                controller = null;
            }
        }
    }
}

ここは非常に悩んだ部分でどうやって同じネットーワーク内にあるサーバーからEventView情報を持ってくるか
結局最適解かは分かりませんが 私が最終的にたどり着いた回答s\としてはPowerShellのGet-WinEvent コマンドで取得する方法でした。

最後は、txt形式とcsv形式 二種類のログを作成し txtは可読性に特化したもの csvはアプリケーションに読み込ませてGridに表示することも可能になっています。

FetchEventLog.ps1

# 抽出するログを指定する
$LogName = "System", "Application"

# 抽出を行う期間を取得する
$StartTime = (Get-Date).AddHours(-24)
$EndTime = Get-Date

$SerVerList = @("MainDB", "SubDB", "DevDB", "DC", "FileServer1", "FileServer2", "FileServer3")

foreach($Name in $SerVerList){
    #コンピュータを指定する
    $ComName = $Name
    #[Net.Dns]::GetHostName()

    #抽出する種類及び期間のイベントログを取得する
    $EventsList = Get-WinEvent -ComputerName $ComName -FilterHashTable @{LogName=$LogName ; StartTime=$StartTime ; EndTime = $EndTime }

    #重大、エラー、警告のみのイベントログを取得
    $EventsList = $EventsList | Where-Object {$_.Level -le 2}

    #出力パス及びファイル名の指定
    $EventText = (Convert-Path .)+ '\' + $ComName + ".txt"
    $EventCSV = (Convert-Path .) + '\' + $ComName + ".csv"
    echo $EventText
    echo $EventCSV
    #イベントリスト出力
    $EventsList | Format-List -Property LogName,MachineName,LevelDisplayName,TimeCreated,ProviderName,Id,UserId,Message > $EventText

    #csv形式で出力
    $EventsList | Select LogName,MachineName,LevelDisplayName,TimeCreated,ProviderName,Id,UserId, @{name="message";expression={$_.message.Replace("`n", ";")}} | Export-CSV $EventCSV -Encoding Default
}

終わりに

本日はここまでといたします。 Mainロジックは結構長いのでいったん示させていただきます。

本日の感想

初めてQiitaに投稿しました。初投稿で緊張していますが、技術発信の名目の元頑張っていくのでよろしくお願いいます。

こうやってコードに起こして投稿の文章を書いてみると自分自身のコードの良くなかった部分 いい部分と悪い部分が浮き彫りになってきてとても楽しいです。

また 一つのクラスに長いロジックを書きすぎるので反省したいとことろです。

ps:

最近はDockerとgoとVue.jsでゲームの計算アプリも作り始めました。
初めてのWeb開発で主いろいこと反面 難しくて理解しがたいことばかり もうちょっと頑張ってみて先人の知恵を借りようと思います。orz

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?