
本記事は、マクロのちょっとした活用方法について解説します。
アプリのメッセージについて
アプリケーションから出力されるメッセージについて、どのように管理しているでしょうか?
他にも色々な手段でメッセージを管理しているかと思います。
今回は、マクロの続きということなので、グローバル + マクロを利用した、メッセージ管理方法を解説致します。
メッセージの作成
今回メッセージを作成するのは、システムグローバル「^IRIS.Msg」になります。
※Cacheだと「^CacheMsg」になります。
作成方法は下記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 | 関数の「引数名」と「引数の値」が保存される |
Source | INTファイル内での位置 |
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”)_“;“
確かにこれなら実現可能ですね。
こんなやり方があったかー、って感じです。
このコマンドを構築する工程は、下記にあります。
引数情報の取得
関数「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()」で、ソースコードのロケーションを取得しています。
勉強になります。
ありがとうございます!
他にも見どころはあります。
皆さまも、お手すきの時に確認してみて下さい。
今後の機能拡張
現状でも十分要件を満たしているロギング機能ですが、下記機能が含まれるとさらに便利になっていくかと思います。
システムによって必用な機能は変わってくると思います。
他に欲しい機能があったら、そっと教えてください。