diff options
author | Teravus Ovares | 2009-01-05 04:09:04 +0000 |
---|---|---|
committer | Teravus Ovares | 2009-01-05 04:09:04 +0000 |
commit | e4a8cc192dd16930718ff18838aa82e6187741bf (patch) | |
tree | a41492dc43a435fcd8a9395fb1f6aff8ae03b702 | |
parent | * Another minor GenericMessage fix - If we assume the method names are case-i... (diff) | |
download | opensim-SC-e4a8cc192dd16930718ff18838aa82e6187741bf.zip opensim-SC-e4a8cc192dd16930718ff18838aa82e6187741bf.tar.gz opensim-SC-e4a8cc192dd16930718ff18838aa82e6187741bf.tar.bz2 opensim-SC-e4a8cc192dd16930718ff18838aa82e6187741bf.tar.xz |
* Adds an active log to the WebStats console. for an example of it in use as it is right now see http://wmcv.com:9000/SStats/
* It still isn't quite ready to be used mainstream.
* A couple of things to note, it doesn't keep track of the logs if nobody is looking at the stats.
* It doesn't read the whole log file. Just the last 10 lines of the stream. Tested to 1GB+ logfiles with no noticeable performance issues.
Diffstat (limited to '')
-rw-r--r-- | OpenSim/Region/UserStatistics/ActiveConnectionsAJAX.cs | 13 | ||||
-rw-r--r-- | OpenSim/Region/UserStatistics/Default_Report.cs | 16 | ||||
-rw-r--r-- | OpenSim/Region/UserStatistics/LogLinesAJAX.cs | 98 | ||||
-rw-r--r-- | OpenSim/Region/UserStatistics/SimStatsAJAX.cs | 3 | ||||
-rw-r--r-- | OpenSim/Region/UserStatistics/WebStatsModule.cs | 66 |
5 files changed, 185 insertions, 11 deletions
diff --git a/OpenSim/Region/UserStatistics/ActiveConnectionsAJAX.cs b/OpenSim/Region/UserStatistics/ActiveConnectionsAJAX.cs index 2dcd2b8..ed8fc40 100644 --- a/OpenSim/Region/UserStatistics/ActiveConnectionsAJAX.cs +++ b/OpenSim/Region/UserStatistics/ActiveConnectionsAJAX.cs | |||
@@ -14,6 +14,7 @@ namespace OpenSim.Region.UserStatistics | |||
14 | { | 14 | { |
15 | #region IStatsController Members | 15 | #region IStatsController Members |
16 | 16 | ||
17 | private Vector3 DefaultNeighborPosition = new Vector3(128, 128, 70); | ||
17 | public Hashtable ProcessModel(Hashtable pParams) | 18 | public Hashtable ProcessModel(Hashtable pParams) |
18 | { | 19 | { |
19 | 20 | ||
@@ -50,10 +51,20 @@ namespace OpenSim.Region.UserStatistics | |||
50 | output.Append(av.Name); | 51 | output.Append(av.Name); |
51 | output.Append(" "); | 52 | output.Append(" "); |
52 | output.Append((av.IsChildAgent ? "Child" : "Root")); | 53 | output.Append((av.IsChildAgent ? "Child" : "Root")); |
53 | 54 | if (av.AbsolutePosition == DefaultNeighborPosition) | |
55 | { | ||
56 | output.Append("<br />Position: ?"); | ||
57 | } | ||
58 | else | ||
59 | { | ||
60 | output.Append(string.Format("<br /><NOBR>Position: <{0},{1},{2}></NOBR>", (int)av.AbsolutePosition.X, | ||
61 | (int) av.AbsolutePosition.Y, | ||
62 | (int) av.AbsolutePosition.Z)); | ||
63 | } | ||
54 | Dictionary<string, int> throttles = DecodeClientThrottles(av.ControllingClient.GetThrottlesPacked(1)); | 64 | Dictionary<string, int> throttles = DecodeClientThrottles(av.ControllingClient.GetThrottlesPacked(1)); |
55 | 65 | ||
56 | HTMLUtil.UL_O(ref output, ""); | 66 | HTMLUtil.UL_O(ref output, ""); |
67 | |||
57 | foreach (string throttlename in throttles.Keys) | 68 | foreach (string throttlename in throttles.Keys) |
58 | { | 69 | { |
59 | HTMLUtil.LI_O(ref output, ""); | 70 | HTMLUtil.LI_O(ref output, ""); |
diff --git a/OpenSim/Region/UserStatistics/Default_Report.cs b/OpenSim/Region/UserStatistics/Default_Report.cs index 02b15ad..eac4a7e 100644 --- a/OpenSim/Region/UserStatistics/Default_Report.cs +++ b/OpenSim/Region/UserStatistics/Default_Report.cs | |||
@@ -76,9 +76,9 @@ TD.align_top { vertical-align: top; } | |||
76 | HTMLUtil.HtmlHeaders_O(ref output); | 76 | HTMLUtil.HtmlHeaders_O(ref output); |
77 | 77 | ||
78 | HTMLUtil.InsertProtoTypeAJAX(ref output); | 78 | HTMLUtil.InsertProtoTypeAJAX(ref output); |
79 | string[] ajaxUpdaterDivs = new string[2]; | 79 | string[] ajaxUpdaterDivs = new string[3]; |
80 | int[] ajaxUpdaterSeconds = new int[2]; | 80 | int[] ajaxUpdaterSeconds = new int[3]; |
81 | string[] ajaxUpdaterReportFragments = new string[2]; | 81 | string[] ajaxUpdaterReportFragments = new string[3]; |
82 | 82 | ||
83 | ajaxUpdaterDivs[0] = "activeconnections"; | 83 | ajaxUpdaterDivs[0] = "activeconnections"; |
84 | ajaxUpdaterSeconds[0] = 10; | 84 | ajaxUpdaterSeconds[0] = 10; |
@@ -88,6 +88,10 @@ TD.align_top { vertical-align: top; } | |||
88 | ajaxUpdaterSeconds[1] = 20; | 88 | ajaxUpdaterSeconds[1] = 20; |
89 | ajaxUpdaterReportFragments[1] = "simstatsajax.ajax"; | 89 | ajaxUpdaterReportFragments[1] = "simstatsajax.ajax"; |
90 | 90 | ||
91 | ajaxUpdaterDivs[2] = "activelog"; | ||
92 | ajaxUpdaterSeconds[2] = 5; | ||
93 | ajaxUpdaterReportFragments[2] = "activelogajax.ajax"; | ||
94 | |||
91 | HTMLUtil.InsertPeriodicUpdaters(ref output, ajaxUpdaterDivs, ajaxUpdaterSeconds, ajaxUpdaterReportFragments); | 95 | HTMLUtil.InsertPeriodicUpdaters(ref output, ajaxUpdaterDivs, ajaxUpdaterSeconds, ajaxUpdaterReportFragments); |
92 | 96 | ||
93 | output.Append(STYLESHEET); | 97 | output.Append(STYLESHEET); |
@@ -152,11 +156,11 @@ TD.align_top { vertical-align: top; } | |||
152 | HTMLUtil.TABLE_O(ref output, ""); | 156 | HTMLUtil.TABLE_O(ref output, ""); |
153 | HTMLUtil.TR_O(ref output, ""); | 157 | HTMLUtil.TR_O(ref output, ""); |
154 | HTMLUtil.TD_O(ref output, "align_top"); | 158 | HTMLUtil.TD_O(ref output, "align_top"); |
155 | output.Append("<DIV id=\"activeconnections\">loading...</DIV>"); | 159 | output.Append("<DIV id=\"activeconnections\">Active Connections loading...</DIV>"); |
156 | HTMLUtil.TD_C(ref output); | 160 | HTMLUtil.TD_C(ref output); |
157 | HTMLUtil.TD_O(ref output, "align_top"); | 161 | HTMLUtil.TD_O(ref output, "align_top"); |
158 | output.Append("<DIV id=\"activesimstats\">loading...</DIV>"); | 162 | output.Append("<DIV id=\"activesimstats\">SimStats loading...</DIV>"); |
159 | 163 | output.Append("<DIV id=\"activelog\">ActiveLog loading...</DIV>"); | |
160 | HTMLUtil.TD_C(ref output); | 164 | HTMLUtil.TD_C(ref output); |
161 | HTMLUtil.TR_C(ref output); | 165 | HTMLUtil.TR_C(ref output); |
162 | HTMLUtil.TABLE_C(ref output); | 166 | HTMLUtil.TABLE_C(ref output); |
diff --git a/OpenSim/Region/UserStatistics/LogLinesAJAX.cs b/OpenSim/Region/UserStatistics/LogLinesAJAX.cs new file mode 100644 index 0000000..9626c12 --- /dev/null +++ b/OpenSim/Region/UserStatistics/LogLinesAJAX.cs | |||
@@ -0,0 +1,98 @@ | |||
1 | using System; | ||
2 | using System.Collections; | ||
3 | using System.Collections.Generic; | ||
4 | using System.Reflection; | ||
5 | using System.Text; | ||
6 | using System.Text.RegularExpressions; | ||
7 | using Mono.Data.SqliteClient; | ||
8 | using OpenMetaverse; | ||
9 | using OpenSim.Region.Environment.Scenes; | ||
10 | using OpenSim.Framework.Statistics; | ||
11 | |||
12 | namespace OpenSim.Region.UserStatistics | ||
13 | { | ||
14 | public class LogLinesAJAX : IStatsController | ||
15 | { | ||
16 | private Regex normalizeEndLines = new Regex(@"\r\n", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline); | ||
17 | |||
18 | private Regex webFormat = new Regex(@"[^\s]*\s([^,]*),[^\s]*\s([A-Z]*)[^\s-][^\[]*\[([^\]]*)\]([^\n]*)", | ||
19 | RegexOptions.Singleline | RegexOptions.Compiled); | ||
20 | private Regex TitleColor = new Regex(@"[^\s]*\s(?:[^,]*),[^\s]*\s(?:[A-Z]*)[^\s-][^\[]*\[([^\]]*)\](?:[^\n]*)", | ||
21 | RegexOptions.Singleline | RegexOptions.Compiled); | ||
22 | |||
23 | |||
24 | #region IStatsController Members | ||
25 | |||
26 | public Hashtable ProcessModel(Hashtable pParams) | ||
27 | { | ||
28 | Hashtable nh = new Hashtable(); | ||
29 | nh.Add("loglines", pParams["LogLines"]); | ||
30 | return nh; | ||
31 | } | ||
32 | |||
33 | public string RenderView(Hashtable pModelResult) | ||
34 | { | ||
35 | StringBuilder output = new StringBuilder(); | ||
36 | |||
37 | HTMLUtil.HR(ref output, ""); | ||
38 | output.Append("<H3>ActiveLog</H3>\n"); | ||
39 | |||
40 | string tmp = normalizeEndLines.Replace(pModelResult["loglines"].ToString(), "\n"); | ||
41 | |||
42 | string[] result = Regex.Split(tmp, "\n"); | ||
43 | |||
44 | string formatopen = ""; | ||
45 | string formatclose = ""; | ||
46 | |||
47 | for (int i = 0; i < result.Length;i++ ) | ||
48 | { | ||
49 | if (result[i].Length >= 30) | ||
50 | { | ||
51 | string logtype = result[i].Substring(24, 6); | ||
52 | switch (logtype) | ||
53 | { | ||
54 | case "WARN ": | ||
55 | formatopen = "<font color=\"#7D7C00\">"; | ||
56 | formatclose = "</font>"; | ||
57 | break; | ||
58 | |||
59 | case "ERROR ": | ||
60 | formatopen = "<font color=\"#FF0000\">"; | ||
61 | formatclose = "</font>"; | ||
62 | break; | ||
63 | |||
64 | default: | ||
65 | formatopen = ""; | ||
66 | formatclose = ""; | ||
67 | break; | ||
68 | |||
69 | } | ||
70 | } | ||
71 | StringBuilder replaceStr = new StringBuilder(); | ||
72 | //string titlecolorresults = | ||
73 | |||
74 | string formatresult = Regex.Replace(TitleColor.Replace(result[i], "$1"), "[^ABCDEFabcdef0-9]", ""); | ||
75 | if (formatresult.Length > 6) | ||
76 | { | ||
77 | formatresult = formatresult.Substring(0, 6); | ||
78 | |||
79 | } | ||
80 | for (int j = formatresult.Length; j <= 5; j++) | ||
81 | formatresult += "0"; | ||
82 | replaceStr.Append("$1 - [<font color=\"#"); | ||
83 | replaceStr.Append(formatresult); | ||
84 | replaceStr.Append("\">$3</font>] $4<br />"); | ||
85 | string repstr = replaceStr.ToString(); | ||
86 | |||
87 | output.Append(formatopen); | ||
88 | output.Append(webFormat.Replace(result[i], repstr)); | ||
89 | output.Append(formatclose); | ||
90 | } | ||
91 | |||
92 | |||
93 | return output.ToString(); | ||
94 | } | ||
95 | |||
96 | #endregion | ||
97 | } | ||
98 | } | ||
diff --git a/OpenSim/Region/UserStatistics/SimStatsAJAX.cs b/OpenSim/Region/UserStatistics/SimStatsAJAX.cs index f6dd1d6..26fe3d4 100644 --- a/OpenSim/Region/UserStatistics/SimStatsAJAX.cs +++ b/OpenSim/Region/UserStatistics/SimStatsAJAX.cs | |||
@@ -21,7 +21,6 @@ namespace OpenSim.Region.UserStatistics | |||
21 | Hashtable nh = new Hashtable(); | 21 | Hashtable nh = new Hashtable(); |
22 | nh.Add("hdata", m_scene); | 22 | nh.Add("hdata", m_scene); |
23 | nh.Add("simstats", pParams["SimStats"]); | 23 | nh.Add("simstats", pParams["SimStats"]); |
24 | |||
25 | return nh; | 24 | return nh; |
26 | } | 25 | } |
27 | 26 | ||
@@ -181,7 +180,9 @@ namespace OpenSim.Region.UserStatistics | |||
181 | HTMLUtil.TD_C(ref output); | 180 | HTMLUtil.TD_C(ref output); |
182 | HTMLUtil.TR_C(ref output); | 181 | HTMLUtil.TR_C(ref output); |
183 | HTMLUtil.TABLE_C(ref output); | 182 | HTMLUtil.TABLE_C(ref output); |
183 | |||
184 | } | 184 | } |
185 | |||
185 | return output.ToString(); | 186 | return output.ToString(); |
186 | } | 187 | } |
187 | 188 | ||
diff --git a/OpenSim/Region/UserStatistics/WebStatsModule.cs b/OpenSim/Region/UserStatistics/WebStatsModule.cs index 4eb8cb7..832c763 100644 --- a/OpenSim/Region/UserStatistics/WebStatsModule.cs +++ b/OpenSim/Region/UserStatistics/WebStatsModule.cs | |||
@@ -1,8 +1,11 @@ | |||
1 | using System; | 1 | using System; |
2 | using System.Collections; | 2 | using System.Collections; |
3 | using System.Collections.Generic; | 3 | using System.Collections.Generic; |
4 | using System.IO; | ||
4 | using System.Net; // to be used for REST-->Grid shortly | 5 | using System.Net; // to be used for REST-->Grid shortly |
5 | using System.Reflection; | 6 | using System.Reflection; |
7 | using System.Text; | ||
8 | using System.Threading; | ||
6 | using log4net; | 9 | using log4net; |
7 | using Nini.Config; | 10 | using Nini.Config; |
8 | using OpenMetaverse; | 11 | using OpenMetaverse; |
@@ -25,14 +28,19 @@ namespace OpenSim.Region.UserStatistics | |||
25 | { | 28 | { |
26 | private static readonly ILog m_log = | 29 | private static readonly ILog m_log = |
27 | LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); | 30 | LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); |
31 | |||
28 | private static SqliteConnection dbConn; | 32 | private static SqliteConnection dbConn; |
29 | private Dictionary<UUID, UserSessionID> m_sessions = new Dictionary<UUID, UserSessionID>(); | 33 | private Dictionary<UUID, UserSessionID> m_sessions = new Dictionary<UUID, UserSessionID>(); |
30 | private List<Scene> m_scene = new List<Scene>(); | 34 | private List<Scene> m_scene = new List<Scene>(); |
31 | private Dictionary<string, IStatsController> reports = new Dictionary<string, IStatsController>(); | 35 | private Dictionary<string, IStatsController> reports = new Dictionary<string, IStatsController>(); |
32 | private Dictionary<UUID, USimStatsData> m_simstatsCounters = new Dictionary<UUID, USimStatsData>(); | 36 | private Dictionary<UUID, USimStatsData> m_simstatsCounters = new Dictionary<UUID, USimStatsData>(); |
33 | private const int updateStatsMod = 6; | 37 | private const int updateStatsMod = 6; |
38 | private int updateLogMod = 1; | ||
39 | private volatile int updateLogCounter = 0; | ||
34 | private volatile int concurrencyCounter = 0; | 40 | private volatile int concurrencyCounter = 0; |
35 | private bool enabled = false; | 41 | private bool enabled = false; |
42 | private string m_loglines = String.Empty; | ||
43 | private volatile int lastHit = 12000; | ||
36 | 44 | ||
37 | 45 | ||
38 | public virtual void Initialise(Scene scene, IConfigSource config) | 46 | public virtual void Initialise(Scene scene, IConfigSource config) |
@@ -42,7 +50,8 @@ namespace OpenSim.Region.UserStatistics | |||
42 | { | 50 | { |
43 | cnfg = config.Configs["WebStats"]; | 51 | cnfg = config.Configs["WebStats"]; |
44 | enabled = cnfg.GetBoolean("enabled", false); | 52 | enabled = cnfg.GetBoolean("enabled", false); |
45 | 53 | ||
54 | |||
46 | } | 55 | } |
47 | catch (Exception) | 56 | catch (Exception) |
48 | { | 57 | { |
@@ -68,12 +77,14 @@ namespace OpenSim.Region.UserStatistics | |||
68 | Updater_distributor updatedep = new Updater_distributor(); | 77 | Updater_distributor updatedep = new Updater_distributor(); |
69 | ActiveConnectionsAJAX ajConnections = new ActiveConnectionsAJAX(); | 78 | ActiveConnectionsAJAX ajConnections = new ActiveConnectionsAJAX(); |
70 | SimStatsAJAX ajSimStats = new SimStatsAJAX(); | 79 | SimStatsAJAX ajSimStats = new SimStatsAJAX(); |
80 | LogLinesAJAX ajLogLines = new LogLinesAJAX(); | ||
71 | reports.Add("", rep); | 81 | reports.Add("", rep); |
72 | reports.Add("index.aspx", rep); | 82 | reports.Add("index.aspx", rep); |
73 | reports.Add("prototype.js", protodep); | 83 | reports.Add("prototype.js", protodep); |
74 | reports.Add("updater.js", updatedep); | 84 | reports.Add("updater.js", updatedep); |
75 | reports.Add("activeconnectionsajax.ajax", ajConnections); | 85 | reports.Add("activeconnectionsajax.ajax", ajConnections); |
76 | reports.Add("simstatsajax.ajax", ajSimStats); | 86 | reports.Add("simstatsajax.ajax", ajSimStats); |
87 | reports.Add("activelogajax.ajax", ajLogLines); | ||
77 | 88 | ||
78 | scene.CommsManager.HttpServer.AddHTTPHandler("/SStats/", HandleStatsRequest); | 89 | scene.CommsManager.HttpServer.AddHTTPHandler("/SStats/", HandleStatsRequest); |
79 | 90 | ||
@@ -95,10 +106,17 @@ namespace OpenSim.Region.UserStatistics | |||
95 | 106 | ||
96 | try | 107 | try |
97 | { | 108 | { |
98 | 109 | // Ignore the update if there's a report running right now | |
99 | if (concurrencyCounter > 0) | 110 | // ignore the update if there hasn't been a hit in 30 seconds. |
111 | if (concurrencyCounter > 0 && System.Environment.TickCount - lastHit < 30000) | ||
100 | return; | 112 | return; |
101 | 113 | ||
114 | if ((updateLogCounter++ % updateLogMod) == 0) | ||
115 | { | ||
116 | m_loglines = readLogLines(10); | ||
117 | if (updateLogCounter > 10000) updateLogCounter = 1; | ||
118 | } | ||
119 | |||
102 | USimStatsData ss = m_simstatsCounters[stats.RegionUUID]; | 120 | USimStatsData ss = m_simstatsCounters[stats.RegionUUID]; |
103 | 121 | ||
104 | if ((++ss.StatsCounter % updateStatsMod) == 0) | 122 | if ((++ss.StatsCounter % updateStatsMod) == 0) |
@@ -114,6 +132,7 @@ namespace OpenSim.Region.UserStatistics | |||
114 | 132 | ||
115 | public Hashtable HandleStatsRequest(Hashtable request) | 133 | public Hashtable HandleStatsRequest(Hashtable request) |
116 | { | 134 | { |
135 | lastHit = System.Environment.TickCount; | ||
117 | Hashtable responsedata = new Hashtable(); | 136 | Hashtable responsedata = new Hashtable(); |
118 | string regpath = request["uri"].ToString(); | 137 | string regpath = request["uri"].ToString(); |
119 | int response_code = 404; | 138 | int response_code = 404; |
@@ -130,6 +149,7 @@ namespace OpenSim.Region.UserStatistics | |||
130 | repParams["DatabaseConnection"] = dbConn; | 149 | repParams["DatabaseConnection"] = dbConn; |
131 | repParams["Scenes"] = m_scene; | 150 | repParams["Scenes"] = m_scene; |
132 | repParams["SimStats"] = m_simstatsCounters; | 151 | repParams["SimStats"] = m_simstatsCounters; |
152 | repParams["LogLines"] = m_loglines; | ||
133 | 153 | ||
134 | concurrencyCounter++; | 154 | concurrencyCounter++; |
135 | 155 | ||
@@ -245,6 +265,7 @@ namespace OpenSim.Region.UserStatistics | |||
245 | { | 265 | { |
246 | lock (m_scene) | 266 | lock (m_scene) |
247 | { | 267 | { |
268 | updateLogMod = m_scene.Count * 2; | ||
248 | foreach (Scene scene in m_scene) | 269 | foreach (Scene scene in m_scene) |
249 | { | 270 | { |
250 | scene.EventManager.OnRegisterCaps += OnRegisterCaps; | 271 | scene.EventManager.OnRegisterCaps += OnRegisterCaps; |
@@ -315,6 +336,45 @@ namespace OpenSim.Region.UserStatistics | |||
315 | 336 | ||
316 | } | 337 | } |
317 | 338 | ||
339 | public string readLogLines( int amount) | ||
340 | { | ||
341 | Encoding encoding = Encoding.ASCII; | ||
342 | int sizeOfChar = encoding.GetByteCount("\n"); | ||
343 | byte[] buffer = encoding.GetBytes("\n"); | ||
344 | string logfile = Util.logDir() + "/" + "OpenSim.log"; | ||
345 | FileStream fs = new FileStream(logfile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); | ||
346 | Int64 tokenCount = 0; | ||
347 | Int64 endPosition = fs.Length / sizeOfChar; | ||
348 | |||
349 | for (Int64 position = sizeOfChar; position < endPosition; position += sizeOfChar) | ||
350 | { | ||
351 | fs.Seek(-position, SeekOrigin.End); | ||
352 | fs.Read(buffer, 0, buffer.Length); | ||
353 | |||
354 | if (encoding.GetString(buffer) == "\n") | ||
355 | { | ||
356 | tokenCount++; | ||
357 | if (tokenCount == amount) | ||
358 | { | ||
359 | byte[] returnBuffer = new byte[fs.Length - fs.Position]; | ||
360 | fs.Read(returnBuffer, 0, returnBuffer.Length); | ||
361 | fs.Close(); | ||
362 | fs.Dispose(); | ||
363 | return encoding.GetString(returnBuffer); | ||
364 | } | ||
365 | } | ||
366 | } | ||
367 | |||
368 | // handle case where number of tokens in file is less than numberOfTokens | ||
369 | fs.Seek(0, SeekOrigin.Begin); | ||
370 | buffer = new byte[fs.Length]; | ||
371 | fs.Read(buffer, 0, buffer.Length); | ||
372 | fs.Close(); | ||
373 | fs.Dispose(); | ||
374 | return encoding.GetString(buffer); | ||
375 | |||
376 | } | ||
377 | |||
318 | public UUID GetRegionUUIDFromHandle(ulong regionhandle) | 378 | public UUID GetRegionUUIDFromHandle(ulong regionhandle) |
319 | { | 379 | { |
320 | lock (m_scene) | 380 | lock (m_scene) |