【IRIS/Cache】続々・マクロの料理法! 

本記事は、マクロのちょっとした活用方法について解説します。

※この記事は下記の方向けになります。
  • メッセージの管理について悩んだ事がある方
  • アプリのログについて悩んだ事がある方

アプリのメッセージについて

アプリケーションから出力されるメッセージについて、どのように管理しているでしょうか?

  • 特定クラスの関数にメッセージ集を作成している。
  • マクロにメッセージを設定している。
  • グローバルで管理している。
  • excelで管理して、ソースにベタ書き

他にも色々な手段でメッセージを管理しているかと思います。

今回は、マクロの続きということなので、グローバル + マクロを利用した、メッセージ管理方法を解説致します。

メッセージの作成

今回メッセージを作成するのは、システムグローバル「^IRIS.Msg」になります。
 ※Cacheだと「^CacheMsg」になります。

作成方法は下記2通りです。

^IRIS.Msg作成方法
  1. xmlファイルからの読み込み
  2. グローバル直書き

xmlファイルからの読み込み

ベースとなるメッセージは、色々なシステムにも転用可能だったりします。
作業前に、どのようなメッセージが存在するか、精査する機会になればよいですね。

xmlファイルの作成

下記フォーマットのxmlファイルを作成します。
ファイル名は適当で構いません。

<?xml version="1.0" encoding="UTF-8" ?> 
<MsgFile Language="ja">
  <MsgDomain Domain="CustomMessage">
    <Message Id="-1000">あなたは嘘をついている</Message> 
    <Message Id="-1001">%1にも恋しい人がたくさんいるゆえ 何としても%2</Message> 
    <Message Id="-1002">いつのまにか%1になっておったのだな</Message>   
  </MsgDomain>
  <MsgDomain Domain="CustomMessage2">
    <Message Id="-1000">大いなる小競り合い</Message> 
    <Message Id="-1001">%1の腹</Message> 
    <Message Id="-1002">%1は慎重に</Message>   
  </MsgDomain>
</MsgFile>

日本語での対応であれば、エレメント「MsgFile」の要素「Language」に「ja」を設定します。

<MsgFile Language="ja">

エレメント「MsgDomain」の要素「Domain」にメッセージの分類を命名します。
頭文字に「%」が付かなければ、だいたい何でも良いようです。

<MsgDomain Domain="[メッセージの分類]">

エレメント「Message」の要素「ID」と値を設定します。
IDは「」の整数にします。
 ※正の整数だと、システムで使用する際に重複する可能性があります。

<Message Id="[メッセージID]">[メッセージ内容]</Message>
xmlファイルの読み込み

ターミナルを起動し、下記コマンドを実行します。

システムグローバル「^IRIS.Msg」は、ネームスペース毎に設定されています。
今回はネームスペース「sample」で使用したいので、znコマンドでネームスペースを変更しています。

zn "sample"
w ##class(%MessageDictionary).Import("[xml配置dir]\customMessage.xml")

ターミナルに「1」が表示されれば、メッセージの取り込み成功です。

メッセージの確認

管理ポータルより、「^IRIS.Msg」を確認します。

取り込んだメッセージが確認できれば完了です。

グローバル直書き

グローバルを直接SET文で構築可能です。

ターミナルを起動して、下記グローバルを直接作成します。

s ^IRIS.Msg([分類名],"ja",[メッセージID]) = [メッセージ内容]

試しにターミナルから下記を登録してみます。
 → メッセージの確認は「メッセージの取得」の項目で行います。

s ^IRIS.Msg("OriginalMesssage","ja",-1002) = "ありがた山の寒がらす"
s ^IRIS.Msg("OriginalMesssage","ja",-1001) = "お姫様方、貸本屋の%1が参りましたぞ"
s ^IRIS.Msg("OriginalMesssage","ja",-1000) = "%1や%2には勝てねえか"

将来、グローバル名が変わってしまった場合に備えて、xmlファイルからの作成方法がベストです。
 →グローバル名やノードの構成が変わっても、随時対応可能であればSET文でも問題ないです。

メッセージの取得

マクロについての記事なので、システムにより提供されるマクロ「$$$FormatMessage」を使用します。

$$$FormatMessageの構文

$$$FormatMessage([言語], [分類], [メッセージID], [default値], %1の値, %2の値...)

サンプル

ClassMethod editMess()
{
	w $$$FormatMessage("ja", "CustomMessage", -1000, "見つからない"),!
	w $$$FormatMessage("ja", "OriginalMesssage", -1000, "見つからない","岡場所","宿場"),!
	w $$$FormatMessage("ja", "hogehoge", -1000, "見つからない")
}

【実行結果】
あなたは嘘をついている
岡場所や宿場には勝てねえか
見つからない

メッセージが表示されました!

グローバルなので管理しやすく、マクロがあるので取得が楽です。
また、各国へのローカライズも行えるので、海外を見据えて利用するのも有りかと思います。

便利な機能ですよね。

ロギングについて

開発者コミュニティに、面白い記事が掲載されていたのでご紹介します。
元の記事は、2017/3/24に投稿された記事で、所々少々古い記述方法等が見られますが、その発想・構築はブッチギリで素晴らしいです。

この仕組みを導入するだけで、マクロを1行書くとロギングが出来ちゃいます。

では、この記事を解説していきます。

先ずは全体像

先ずは、データクラス・インクルードファイル・実行結果を確認してみます。

ログクラス

Class dc.dev.logging.Log Extends %Persistent
{

/// Replacement for missing values
Parameter Null = "Null";

/// Type of event
Property EventType As %String(MAXLEN = 10, VALUELIST = ",NONE,FATAL,ERROR,WARN,INFO,STAT,DEBUG,RAW") [ InitialExpression = "INFO" ];

/// Name of class, where event happened
Property ClassName As %String(MAXLEN = 256);

/// Name of method, where event happened
Property MethodName As %String(MAXLEN = 128);

/// Line of int code
Property Source As %String(MAXLEN = 2000);

/// Line of cls code
Property SourceCLS As %String(MAXLEN = 2000);

/// Cache user
Property UserName As %String(MAXLEN = 128) [ InitialExpression = {$username} ];

/// Arguments' values passed to method
Property Arguments As %String(MAXLEN = 32000, TRUNCATE = 1);

/// Date and time
Property TimeStamp As %TimeStamp [ InitialExpression = {$zdt($h, 3, 1)} ];

/// User message
Property Message As %String(MAXLEN = 32000, TRUNCATE = 1);

/// User IP address
Property ClientIPAddress As %String(MAXLEN = 32) [ InitialExpression = {..GetClientAddress()} ];

Index idxEventType On EventType [ Type = bitmap ];

Index idxUserName On UserName [ Type = bitmap ];

Index idxClassName On ClassName [ Type = bitmap ];

Index idxTimeStamp On TimeStamp [ Type = bitslice ];

Index idxClientIPAddress On ClientIPAddress;

/// Determine user IP address
ClassMethod GetClientAddress()
{
	// %CSP.Session source is preferable
	#dim %request As %CSP.Request
	If ($d(%request)) {
		Return %request.CgiEnvs("REMOTE_ADDR")
	}
	Return $system.Process.ClientIPAddress()
}

/// Add new log event
/// Use via $$$LogEventTYPE().
ClassMethod AddRecord(ClassName As %String = "", MethodName As %String = "", Source As %String = "", EventType As %String = "", Arguments As %String = "", Message As %String = "")
{
	Set record = ..%New()
	Set record.Arguments = Arguments
	Set record.ClassName = ClassName
	Set record.EventType = EventType
	Set record.Message = Message
	Set record.MethodName = MethodName
	Set record.Source = Source
	do ..GetClassSourceLine($Piece(Source, " ", 1, *-1), .SourceCLS)
	Set record.SourceCLS = SourceCLS
	Do record.%Save()
}

/// Entry point to get method arguments string 
ClassMethod GetMethodArguments(ClassName As %String, MethodName As %String) As %String
{
	Set list = ..GetMethodArgumentsList(ClassName, MethodName)
	Set string = ..ArgumentsListToString(list)
	Return string
}

/// Get a list of method arguments
ClassMethod GetMethodArgumentsList(ClassName As %String, MethodName As %String) As %List
{
	Set result = ""
	Set def = ##class(%Dictionary.CompiledMethod).%OpenId(ClassName _ "||" _ MethodName)
	If ($IsObject(def)) {
		Set result = def.FormalSpecParsed
	}
	Return result
}

/// Convert list of method arguments to string
ClassMethod ArgumentsListToString(List As %List) As %String
{
	Set result = ""
	For i=1:1:$ll(List) {
		Set result = result _ $$$quote($s(i>1=0:"",1:"; ") _ $lg($lg(List,i))_"=") 
		_ ..GetArgumentValue($lg($lg(List,i)),$lg($lg(List,i),2))
		_$S(i=$ll(List)=0:"",1:$$$quote(";"))
	}
	Return result
}

ClassMethod GetArgumentValue(Name As %String, ClassName As %Dictionary.CacheClassname) As %String
{
	If $ClassMethod(ClassName, "%Extends", "%RegisteredObject") {
		// it's an object
		Return "_##class(dc.dev.logging.Log).SerializeObject("_Name _ ")_"
	} Else {
		// it's a datatype
		Return "_$g(" _ Name _ ","_$$$quote(..#Null)_")_"
	}
}

ClassMethod SerializeObject(Object) As %String
{
	Return:'$IsObject(Object) Object
	Return ..WriteJSONFromObject(Object)
}

ClassMethod WriteJSONFromObject(Object) As %String [ ProcedureBlock = 0 ]
{
	New OldIORedirected, OldMnemonic, OldIO, Str
	Set OldIORedirected = ##class(%Device).ReDirectIO()
	Set OldMnemonic = ##class(%Device).GetMnemonicRoutine()
	Set OldIO = $io
	Try {
		Set Str=""

		//Redirect IO to the current routine - makes use of the labels defined below
		Use $io::("^"_$ZNAME)

		//Enable redirection
		Do ##class(%Device).ReDirectIO(1)

		Do ##class(%ZEN.Auxiliary.jsonProvider).%ObjectToJSON(Object)
	} Catch Ex {
		Set Str = ""
	}

	//Return to original redirection/mnemonic routine settings
	If (OldMnemonic '= "") {
		Use OldIO::("^"_OldMnemonic)
	} Else {
		Use OldIO
	}
	Do ##class(%Device).ReDirectIO(OldIORedirected)

	Quit Str
 
    // Labels that allow for IO redirection
    // Read Character - we don't care about reading
rchr(c)      Quit
    // Read a string - we don't care about reading
rstr(sz,to)  Quit
    // Write a character - call the output label
wchr(s)      Do output($char(s))  Quit
    // Write a form feed - call the output label
wff()        Do output($char(12))  Quit
    // Write a newline - call the output label
wnl()        Do output($char(13,10))  Quit
    // Write a string - call the output label
wstr(s)      Do output(s)  Quit
    // Write a tab - call the output label
wtab(s)      Do output($char(9))  Quit
    // Output label - this is where you would handle what you actually want to do.
    // in our case, we want to write to Str
output(s)    Set Str = Str_s Quit
}

ClassMethod LoadContext(zzId) As %Status [ ProcedureBlock = 0 ]
{
	New zzObj, zzArguments, zzArgument, zzi, zzList
	Return:'..%ExistsId(zzId) $$$OK
	Set zzObj = ..%OpenId(zzId)
	Set zzArguments = zzObj.Arguments
	Set zzList = ..GetMethodArgumentsList(zzObj.ClassName,zzObj.MethodName)
	For zzi=1:1:$Length(zzArguments, ";")-1 {
		Set zzArgument = $Piece(zzArguments,";",zzi)
		Set @$lg($lg(zzList,zzi)) = ..DeserializeObject($Piece(zzArgument,"=",2), $lg($lg(zzList,zzi),2))
	}
}

ClassMethod DeserializeObject(String, ClassName) As %String
{
	If $ClassMethod(ClassName, "%Extends", "%RegisteredObject") {
		// it's an object
		Set st = ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(String,,.obj)
		Return:$$$ISOK(st) obj
	}
	Return String
}

ClassMethod GetClassSourceLine(IntLocation As %String, Output ClsLocation As %String) As %Status
{
    Set Status = $$$OK
    Set ClsLocation = ""
    Try {
        Set MethodAndLine = $Piece(IntLocation,"^",1)
        Set IntName = $Piece(IntLocation,"^",2)
        Set Tag = $Piece(MethodAndLine,"+")
        Set RelativeOffset = $Piece(MethodAndLine,"+",2)
        
        // Get routine text to find the absolute offset of tTag
        Set TagOffset = 0
        Set EndPos = 0
        Set TextLines = 0
        For {
            Set Line = $Text(@("+"_$Increment(TextLines)_ "^" _ IntName))
            Quit:Line=""
            
            // Example:
            // zRun() public {
            // This relies on an assumption that methods will be sorted alphabetically and won't contain labels.
            If $Extract(Line, 1, $Length(Tag)) = Tag {
                Set TagOffset = TextLines //tTextLines is the counter.
                Set EndPos = $Length(Line)
                Quit
            }
        }
        
        // The absolute offset of the line in the .int file is the tag's offset plus the offset within it.
        Set Offset = TagOffset + RelativeOffset
        Set Status = ##class(%Studio.Debugger).SourceLine(IntName, Offset, 0, Offset, EndPos,, .Map)
        If $$$ISERR(Status) {
            Quit
        }
        If $Data(Map("CLS", 1)) {
            Set $ListBuild(Class, Method, Line, EndPos, Namespace) = Map("CLS", 1)
            Set Class = $$$comMemberKeyGet(Class, $$$cCLASSmethod, Method, $$$cMETHorigin)
            Set ClsLocation = Class _ ":" _ Method _ "+" _ Line
        }
    } Catch Ex {
        Set Status = Ex.AsStatus()
    }
    Quit Status
}
}

インクルードファイル

#define StackPlace 		$st($st(-1),"PLACE")
#define CurrentClass 	##Expression($$$quote(%classname))
#define CurrentMethod 	##Expression($$$quote(%methodname))

#define MethodArguments ##Expression(##class(dc.dev.logging.Log).GetMethodArguments(%classname,%methodname))

#define LogEvent(%type, %message) Do ##class(dc.dev.logging.Log).AddRecord($$$CurrentClass,$$$CurrentMethod,$$$StackPlace,%type,$$$MethodArguments,%message)
#define LogNone(%message) 		$$$LogEvent("NONE", %message)
#define LogError(%message) 		$$$LogEvent("ERROR", %message)
#define LogFatal(%message) 		$$$LogEvent("FATAL", %message)
#define LogWarn(%message) 		$$$LogEvent("WARN", %message)
#define LogInfo(%message) 		$$$LogEvent("INFO", %message)
#define LogStat(%message) 		$$$LogEvent("STAT", %message)
#define LogDebug(%message) 		$$$LogEvent("DEBUG", %message)
#define LogRaw(%message) 		$$$LogEvent("RAW", %message)

実行結果の確認

ロギングの使い方を含めて、実行関数を用意していただいています。
この関数を使用して、ログの確認を行います。

Include dc.dev.logging.LogMacro
Class dc.dev.logging.Use [ CompileAfter = dc.dev.logging.Log ]
{
/// do ##class(dc.dev.logging.Use).Test()
ClassMethod Test(a As %Integer = 1, ByRef b = 2)
{
	$$$LogWarn("User message") // just place this macro in user code you wish to log
}
/// do ##class(dc.dev.Use).TestWithObjects()
ClassMethod TestWithObjects(a As %Integer = 1, b As %ZEN.proxyObject)
{
	$$$LogWarn("User message") // just place this macro in user code you wish to log for Objects
}
}
関数1の実行

ターミナルを起動し、下記コマンドを実行します。

do ##class(dc.dev.logging.Use).Test()

ロギングデータが下記になります。

着目するのは、dc.dev.logging.Log.clsの「Arguments」「Source」「SourceCLS」のフィールドになります。

列名説明
Arguments関数の「引数名」と「引数の値」が保存される
SourceINTファイル内での位置
SourceCLSクラスファイル内での位置

この3フィールドが、自動でロギングされるだけでもお釣りがくる勢いです。

関数2の実行

2つ目の関数は、JSON関連で「%Dynamicシリーズ」が実装される前に使われていた、「%ZEN.proxyObject」を引数に使用したデモ関数です。

s json=##class(%ZEN.proxyObject).%New()
s json.sample = "サンプル"
s json.message = "昔のJSON関数"
do ##class(dc.dev.logging.Use).TestWithObjects(100, json)

ロギングデータが下記になります。
 →右端のTimeStampとUserNameは割愛

JSONの中身までロギングされているのはイイですね。

引数の復元

関数「LoadContext」にログのレコードIDを引数にする事で、実行環境(ターミナル等)上で、引数の復元が可能になります。

では、動作を確認してみましょう。

d ##class(dc.dev.logging.Log).LoadContext(2)

ターミナルで実行

関数「LoadContext」の「ProcedureBlock」が「0」となっているため、プロシージャの外でも変数がクリアされず使用可能です。
 →デフォルト設定は、ProcedureBlock = 1です。

すごいですねーーー
としか出てこないです。

関数解説

引数の取得「Arguments」

引数の情報を取得する方法は、下記コマンドがマクロ内で実行されているためです。

例)関数「Test」の場合

“a=”_$g(a,“Null”)_“; b=”_$g(b,“Null”)_;

確かにこれなら実現可能ですね。
こんなやり方があったかー、って感じです。

このコマンドを構築する工程は、下記にあります。

  1. 関数より、引数情報の取得
  2. 引数の型より、データ取得コマンドの構築
引数情報の取得

関数「GetMethodArgumentsList」内で「%Dictionary.CompiledMethod.cls」の「FormalSpecParsed」より、関数の引数情報を取得します。

ClassMethod GetMethodArgumentsList(ClassName As %String, MethodName As %String) As %List
{
	Set result = ""
	Set def = ##class(%Dictionary.CompiledMethod).%OpenId(ClassName _ "||" _ MethodName)
	If ($IsObject(def)) {
		Set result = def.FormalSpecParsed
	}
	Return result
}

「FormalSpecParsed」のデータ構造
$lb($lb([引数名], [引数の型], [ByRef=”&”, Output=”*”], [初期値]),…)

なので、最初の関数「Text」の引数は下記になります。
 → Test(a As %Integer = 1, ByRef b = 2)

$lb($lb(“a”, “%Library.Integer”, “”, “1”), $lb(“b”, “%Library.String”, “&”, “2”))

データ取得コマンドの構築

引数と引数の型が手に入ったら、後は引数の値を取得できるよう構築するだけです。

引数の型を「データ型」「オブジェクト型」の二択で切り分けて処理しています。

ClassMethod ArgumentsListToString(List As %List) As %String
{
	Set result = ""
	For i=1:1:$ll(List) {
		Set result = result _ $$$quote($s(i>1=0:"",1:"; ") _ $lg($lg(List,i))_"=") 
		_ ..GetArgumentValue($lg($lg(List,i)),$lg($lg(List,i),2))
		_$S(i=$ll(List)=0:"",1:$$$quote(";"))
	}
	Return result
}
ClassMethod GetArgumentValue(Name As %String, ClassName As %Dictionary.CacheClassname) As %String
{
	If $ClassMethod(ClassName, "%Extends", "%RegisteredObject") {
		// it's an object
		Return "_##class(dc.dev.logging.Log).SerializeObject("_Name _ ")_"
	} Else {
		// it's a datatype
		Return "_$g(" _ Name _ ","_$$$quote(..#Null)_")_"
	}
}

引数の取得「Source」「SourceCLS」

intファイルの位置を格納します。

とは言え、やっている事は「$st($st(-1), “PLACE”)」なので、スタック情報の最後のデータを取得しているだけです。

クラスの位置を取得する関数が下記になります。

ClassMethod GetClassSourceLine(IntLocation As %String, Output ClsLocation As %String) As %Status
{
    Set Status = $$$OK
    Set ClsLocation = ""
    Try {
        Set MethodAndLine = $Piece(IntLocation,"^",1)
        Set IntName = $Piece(IntLocation,"^",2)
        Set Tag = $Piece(MethodAndLine,"+")
        Set RelativeOffset = $Piece(MethodAndLine,"+",2)
        
        // Get routine text to find the absolute offset of tTag
        Set TagOffset = 0
        Set EndPos = 0
        Set TextLines = 0
        For {
            Set Line = $Text(@("+"_$Increment(TextLines)_ "^" _ IntName))
            Quit:Line=""
            
            // Example:
            // zRun() public {
            // This relies on an assumption that methods will be sorted alphabetically and won't contain labels.
            If $Extract(Line, 1, $Length(Tag)) = Tag {
                Set TagOffset = TextLines //tTextLines is the counter.
                Set EndPos = $Length(Line)
                Quit
            }
        }
        
        // The absolute offset of the line in the .int file is the tag's offset plus the offset within it.
        Set Offset = TagOffset + RelativeOffset
        Set Status = ##class(%Studio.Debugger).SourceLine(IntName, Offset, 0, Offset, EndPos,, .Map)
        If $$$ISERR(Status) {
            Quit
        }
        If $Data(Map("CLS", 1)) {
            Set $ListBuild(Class, Method, Line, EndPos, Namespace) = Map("CLS", 1)
            Set Class = $$$comMemberKeyGet(Class, $$$cCLASSmethod, Method, $$$cMETHorigin)
            Set ClsLocation = Class _ ":" _ Method _ "+" _ Line
        }
    } Catch Ex {
        Set Status = Ex.AsStatus()
    }
    Quit Status
}

$Text()」で、intファイルの全ラインから、クラスメソッドの位置を検索し、「##class(%Studio.Debugger).SourceLine()」で、ソースコードのロケーションを取得しています。

勉強になります。
ありがとうございます!

他にも見どころはあります。
皆さまも、お手すきの時に確認してみて下さい。

今後の機能拡張

現状でも十分要件を満たしているロギング機能ですが、下記機能が含まれるとさらに便利になっていくかと思います。

追加したい機能
  • %List型の引数や配列なども保存したい
  • ログ毎にレベルを設定し、レベルの上下によってロギングを制限したい
  • 関数内の変数もロギングしたい

システムによって必用な機能は変わってくると思います。
他に欲しい機能があったら、そっと教えてください。