我是微軟Dynamics 365 & Power Platform方面的工程師/顧問羅勇,也是2015年7月到2018年6月連續三年Dynamics CRM/Business Solutions方面的微軟最有價值專家(Microsoft MVP),歡迎關注我的微信公衆号 MSFTDynamics365erLuoYong ,回複464或者20220307可友善擷取本文,同時可以在第一間得到我釋出的最新博文資訊,follow me!
前面的文章 通過Custom Connector擷取flow的運作記錄 講述了通過flow擷取flow的運作記錄,這篇文章擴充下,我們做個配置表出來,配置監控哪些flow,以及實際将flow的運作記錄主要資訊放到Microsoft Dataverse中。
首先我履歷一個配置Flow監控的表,狀态為可用的就是要監控的,停用的記錄就不再監控,這樣利用标準的啟用/停用功能來配置是否監控。還做了個Flow運作記錄表,用來存儲flow運作記錄的主要資訊。
然後建立一個環境變量 (了解環境變量可以參考我的博文 在Power Apps中使用環境變量 )來存儲一些配置性的東西,如下:
儲存後打開這個環境變量,設定它的預設值(Default Value)如下:
{
"EnvironmentId":"6cd1dc1a-56f7-4090-ba7c-e06c4055c81a",
"DefaultSolutionId":"fd140aaf-4df4-11dd-bd17-0019b9312238",
"AlertEmails":"[email protected]",
"ViewFlowLink":"https://asia.flow.microsoft.com/manage/environments/{0}/solutions/{1}/flows/{2}/",
"ViewFlowRunHistoryLink":"https://asia.flow.microsoft.com/manage/environments/{0}/solutions/{1}/flows/{2}/runs/{3}"
}
然後我建立一個Custom API來批量建立和設定Flow設定記錄:
這個Custom API我使用的代碼如下:
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
namespace D365.Plugins
{
public class CustomAPIUpsertFlowMonitorSettings : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService adminOrgSvc = serviceFactory.CreateOrganizationService(null);
var flowSetting = GetEnvironmentVariableValue("ly_FlowMonitorSetting", adminOrgSvc, tracingService);
if (string.IsNullOrEmpty(flowSetting))
{
throw new InvalidPluginExecutionException("名稱為ly_FlowMonitorSetting的環境變量沒有設定,請聯系系統管理者!");
}
var flowSettingDic = DeserializeDictionary(flowSetting);
if (!flowSettingDic.ContainsKey("EnvironmentId") || !flowSettingDic.ContainsKey("DefaultSolutionId") || !flowSettingDic.ContainsKey("AlertEmails") || !flowSettingDic.ContainsKey("ViewFlowLink") || !flowSettingDic.ContainsKey("ViewFlowRunHistoryLink"))
{
throw new InvalidPluginExecutionException("名稱為ly_FlowMonitorSetting的環境變量設定有問題,部分元素沒有設定,請聯系系統管理者!");
}
var queryWorkflow = new QueryExpression("workflow");
queryWorkflow.NoLock = true;
queryWorkflow.ColumnSet = new ColumnSet("name", "workflowidunique", "statecode");
queryWorkflow.Criteria.AddCondition("category", ConditionOperator.Equal, 5);
queryWorkflow.Criteria.AddCondition("type", ConditionOperator.Equal, 1);
var workflowEc = adminOrgSvc.RetrieveMultiple(queryWorkflow);
QueryExpression qe;
EntityCollection ec;
foreach (var workflowEntity in workflowEc.Entities)
{
qe = new QueryExpression("ly_flowmonitorsetting");
qe.TopCount = 1;
qe.NoLock = true;
qe.ColumnSet.AddColumn("ly_flowmonitorsettingid");
qe.Criteria.AddCondition("ly_workflowidunique", ConditionOperator.Equal, workflowEntity.GetAttributeValue<Guid>("workflowidunique").ToString());
ec = adminOrgSvc.RetrieveMultiple(qe);
if (ec.Entities.Any())
{
var updateEntity = new Entity("ly_flowmonitorsetting", ec.Entities[0].Id);
updateEntity["ly_flowname"] = workflowEntity.GetAttributeValue<string>("name");
updateEntity["ly_isturnon"] = workflowEntity.GetAttributeValue<OptionSetValue>("statecode").Value == 0 ? false : true;
adminOrgSvc.Update(updateEntity);
}
else
{
var createEntity = new Entity("ly_flowmonitorsetting");
createEntity["ly_name"] = workflowEntity.GetAttributeValue<string>("name");
createEntity["ly_isturnon"] = workflowEntity.GetAttributeValue<OptionSetValue>("statecode").Value == 0 ? false : true;
createEntity["ly_workflowidunique"] = workflowEntity.GetAttributeValue<Guid>("workflowidunique").ToString();
createEntity["ly_flowname"] = workflowEntity.GetAttributeValue<string>("name");
createEntity["ly_viewflowlink"] = string.Format(flowSettingDic["ViewFlowLink"], flowSettingDic["EnvironmentId"], flowSettingDic["DefaultSolutionId"], workflowEntity.GetAttributeValue<Guid>("workflowidunique").ToString());
adminOrgSvc.Create(createEntity);
}
}
}
private string GetEnvironmentVariableValue(string variableSchemaName, IOrganizationService service, ITracingService tracingService)
{
if (string.IsNullOrEmpty(variableSchemaName))
{
throw new InvalidPluginExecutionException("環境變量名稱為空,中止查詢該變量的值!");
}
string returnVal = string.Empty;
var fetchXml = string.Format(@"<fetch versinotallow='1.0' mapping='logical' top='1' no-lock='true'>
<entity name='environmentvariabledefinition'>
<attribute name='defaultvalue'/>
<filter type='and'>
<condition attribute='schemaname' operator='eq' value='{0}'/>
<condition attribute='statecode' operator='eq' value='0'/>
</filter>
<link-entity name='environmentvariableval(new FetchExpression(fetchXml));
if (ec.Entities.Count >= 1)
{
if (ec.Entities[0].Contains("varvalue.value"))
{
returnVal = ec.Entities[0].GetAttributeValue<AliasedValue>("varvalue.value").Value.ToString();
}
else
{
returnVal = ec.Entities[0].GetAttributeValue<string>("defaultvalue");
}
}
tracingService.Trace($"名稱為{variableSchemaName}環境變量的值是{returnVal}");
return returnVal;
}
private Dictionary<string, string> DeserializeDictionary(string josnStr)
{
Dictionary<string, string> returnVal = null;
if (!string.IsNullOrEmpty(josnStr))
{
DataContractJsonSerializerSettings serializerSettings = new DataContractJsonSerializerSettings
{
UseSimpleDictionaryFormat = true
};
using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(josnStr)))
{
DataContractJsonSerializer deseralizer = new DataContractJsonSerializer(typeof(Dictionary<string, string>), serializerSettings);
returnVal = (Dictionary<string, string>)deseralizer.ReadObject(ms);
}
}
return returnVal;
}
}
}
然後在Flow監控配置的實體的清單界面我做了一個按鈕,使用的調用代碼如下:
"use strict";
var LY = window.LY || {};
LY.flowmonitorsettingRibbon = LY.flowmonitorsettingRibbon || {};
(function () {
this.bulkUpsertRecords = function (selectedControl) {
Xrm.Utility.showProgressIndicator("進行中,請稍候...");
var clientUrl = Xrm.Utility.getGlobalContext().getClientUrl();
var req = new XMLHttpRequest();
req.open("POST", `${clientUrl}/api/data/v9.2/ly_UpsertFlowMonitorSettings`, true);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.onreadystatechange = function () {
if (this.readyState == 4) {
req.onreadystatechange = null;
if (this.status == 204) {
Xrm.Utility.closeProgressIndicator();
selectedControl.refresh();
}
else {
Xrm.Utility.closeProgressIndicator();
selectedControl.refresh();
var error = JSON.parse(this.response).error;
Xrm.Navigation.openErrorDialog({ message: error.message });
}
}
}
req.send();
}
}).call(LY.flowmonitorsettingRibbon);
//# flowmonitorsetting_ribbon.js
這個按鈕涉及到的RibbonDiff内容如下:
<RibbonDiffXml>
<CustomActions>
<CustomAction Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.Button.CustomAction" Location="Mscrm.HomepageGrid.ly_flowmonitorsetting.MainTab.Management.Controls._children" Sequence="55">
<CommandUIDefinition>
<Button Command="ly.ly_flowmonitorsetting.bulkUpsertRecords.Command" Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.Button" LabelText="$LocLabels:ly.ly_flowmonitorsetting.bulkUpsertRecords.Button.LabelText" Sequence="55" TemplateAlias="o2" ToolTipDescription="$LocLabels:ly.ly_flowmonitorsetting.bulkUpsertRecords.Button.ToolTipDescription" ModernImage="New" />
</CommandUIDefinition>
</CustomAction>
</CustomActions>
<Templates>
<RibbonTemplates Id="Mscrm.Templates"></RibbonTemplates>
</Templates>
<CommandDefinitions>
<CommandDefinition Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.Command">
<EnableRules>
<EnableRule Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.EnableRule" />
</EnableRules>
<DisplayRules />
<Actions>
<JavaScriptFunction FunctionName="LY.flowmonitorsettingRibbon.bulkUpsertRecords" Library="$webresource:ly_/script/flowmonitorsetting/flowmonitorsetting_ribbon.js">
<CrmParameter Value="SelectedControl" />
</JavaScriptFunction>
</Actions>
</CommandDefinition>
</CommandDefinitions>
<RuleDefinitions>
<TabDisplayRules />
<DisplayRules />
<EnableRules>
<EnableRule Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.EnableRule">
<SelectionCountRule AppliesTo="PrimaryEntity" Minimum="0" Maximum="250" Default="false" InvertResult="false" />
</EnableRule>
</EnableRules>
</RuleDefinitions>
<LocLabels>
<LocLabel Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.Button.LabelText">
<Titles>
<Title description="建立/更新配置" languagecode="1033" />
</Titles>
</LocLabel>
<LocLabel Id="ly.ly_flowmonitorsetting.bulkUpsertRecords.Button.ToolTipDescription">
<Titles>
<Title description="自動擷取Flow資訊,建立/更新Flow監控配置資訊" languagecode="1033" />
</Titles>
</LocLabel>
</LocLabels>
</RibbonDiffXml>
然後我點選按鈕就将現有的Cloud Flow的資訊插入/更新到Flow監控配置實體中了。然後我将其他flow監控記錄停用,隻保留我自己的這個flow用于示範。
我再做個定期運作的flow用來重新整理flow的資訊,擷取flow的運作記錄。
為Flow起個名字,點選Create按鈕
根據需要設定運作頻率:
為了避免表達式太長,我聲明三個變量來存儲環境變量中的值:
使用的表達式類似 parameters('FlowMonitorSetting (ly_FlowMonitorSetting)')['DefaultSolutionId']
然後擷取需要監控flow的基本資訊,步驟如下:
使用的FetchXml是
<fetch mapping="logical" distinct="false" no-lock="true">
<entity name="workflow">
<attribute name="workflowid" />
<attribute name="statecode" />
<attribute name="workflowidunique" />
<filter type="and">
<condition attribute="category" operator="eq" value="5" />
<condition attribute="type" operator="eq" value="1" />
</filter>
<link-entity name="ly_flowmonitorsetting" from="ly_workflowidunique" to="workflowidunique" link-type="inner" alias="setting">
<filter type="and">
<condition attribute="statecode" operator="eq" value="0" />
</filter>
</link-entity>
</entity>
</fetch>
對于每個監控的flow,我先根據Workflow的唯一ID去找到Flow監控配置記錄,然後更新其開關狀态字段,用到的主要表達式是
ly_workflowidunique eq '@{items('Apply_to_each')?['workflowidunique']}' and statecode eq 0
if(equals(items('Apply_to_each')?['statecode'],0),false,true)
然後擷取監控的每個flow的運作記錄,并循環每個運作記錄,看是否已經存在運作記錄唯一ID相同的記錄,用到的主要表達式如下:
@{parameters('FlowMonitorSetting (ly_FlowMonitorSetting)')['EnvironmentId']}
@{items('Apply_to_each')?['workflowidunique']}
body('Get_Flow_Run_History')['value']
如果沒有就要新增flow運作記錄:
用到的主要表達式如下,有寫地方用了if,因為如果flow運作成功的話,有些元素是沒有的,如果不管有沒有都去讀取的話,flow運作會報錯。
length(outputs('查找相同運作id的flow運作記錄')?['body/value'])
/ly_flowmonitorsettings(@{items('Apply_to_each_2')?['ly_flowmonitorsettingid']})
@{replace(replace(replace(replace(variables('ViewFlowRunHistoryLink'),'{0}',variables('EnvironmentId')),'{1}',variables('DefaultSolutionId')),'{2}',items('Apply_to_each')?['workflowidunique']),'{3}',items('Apply_to_each_Flow_Run_History')['name'])}
@{items('Apply_to_each_Flow_Run_History')['properties/startTime']}
@{items('Apply_to_each_Flow_Run_History')['properties/status']}
@{items('Apply_to_each_Flow_Run_History')['properties/endTime']}
@{if(equals(items('Apply_to_each_Flow_Run_History')['properties/status'],'Succeeded'),'',items('Apply_to_each_Flow_Run_History')['properties/code'])}
@{items('Apply_to_each_Flow_Run_History')['name']}
@{if(equals(items('Apply_to_each_Flow_Run_History')['properties/status'],'Succeeded'),'',items('Apply_to_each_Flow_Run_History')['properties/error/code'])}
@{if(equals(items('Apply_to_each_Flow_Run_History')['properties/status'],'Succeeded'),'',items('Apply_to_each_Flow_Run_History')['properties/error/message'])}
效果如下:
擷取到的錯誤代碼、錯誤消息不一定是顯示詳細錯誤資訊,此處有待改進。
擷取的其他資訊我得測試結果表明是準确的。