-
Notifications
You must be signed in to change notification settings - Fork 357
/
FunctionInstanceLogItem.cs
233 lines (205 loc) · 9.27 KB
/
FunctionInstanceLogItem.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Azure.WebJobs.Logging
{
/// <summary>
/// Publically visible.
/// Represent a function invocation.
/// </summary>
public class FunctionInstanceLogItem : IFunctionInstanceBaseEntry
{
// Max lengths of various fields.
// Except for output logging, these shouldn't get hit in practice.
// Truncate if they're exceeded.
private const int MaxTriggerReasonLength = 200;
private const int MaxErrorLength = 500;
private const int MaxLogOutputLength = 2000 + 3;
private const int MaxParameterPayloadLength = 1000;
private const int MaxParameterTotalPayloadLength = 2000;
/// <summary>Gets or sets the function instance ID.</summary>
public Guid FunctionInstanceId { get; set; }
/// <summary>Gets or sets the Function ID of the ancestor function instance.</summary>
public Guid? ParentId { get; set; }
/// <summary>Short Name of this function.</summary>
public string FunctionName { get; set; }
/// <summary>Globally unique name of this function. This is unique across hosts.
/// This can be used in querying other instances of this function. </summary>
public FunctionId FunctionId { get; set; }
/// <summary>
/// An optional hint about why this function was invoked. It may have been triggered, replayed, manually invoked, etc.
/// </summary>
public string TriggerReason { get; set; }
/// <summary>UTC time that the function started executing.</summary>
public DateTime StartTime { get; set; }
/// <summary>If set, the time the function completed (either successfully or failure). </summary>
public DateTime? EndTime { get; set; }
/// <summary>A function outputs a heartbeat while it's running.
/// If EndTime = null and heartbeat is "old", then the function is likley abandoned (machine killed).
/// It's always possible a node loses network connectity, and so we appear abandoned, but
/// the function successfully completes</summary>
/// <inheritdoc/>
public DateTime? FunctionInstanceHeartbeatExpiry { get; set; }
/// <summary>
/// Given log item a chance to refresh.
/// </summary>
/// <param name="pollingFrequency">Approximate frequency at which refresh is called. This can be used to determine the heartbeat expiration time. </param>
public virtual void Refresh(TimeSpan pollingFrequency)
{
var gracePeriod = pollingFrequency.TotalMilliseconds * 3;
this.FunctionInstanceHeartbeatExpiry = DateTime.UtcNow.AddMilliseconds(gracePeriod);
}
/// <summary>
/// Null on success.
/// Else, set to some string with error details.
/// </summary>
public string ErrorDetails { get; set; }
/// <summary>
/// True if we have an error. Implementation of <see cref="IFunctionInstanceBaseEntry"/>
/// </summary>
public bool HasError
{
get
{
return this.ErrorDetails != null;
}
}
/// <summary>Gets or sets the function's argument values and help strings.</summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public IDictionary<string, string> Arguments { get; set; }
/// <summary>Direct inline capture for small log outputs. For large log outputs, this is faulted over to a blob. </summary>
public string LogOutput { get; set; }
/// <summary>
/// Get a summary for this instance.
/// </summary>
/// <returns></returns>
public string GetDisplayTitle()
{
IEnumerable<string> argumentValues = null;
if (this.Arguments != null)
{
argumentValues = this.Arguments.Values;
}
return BuildFunctionDisplayTitle(this.FunctionName, argumentValues);
}
/// <summary>
/// Helper to build an instance summary display name given the arguments.
/// </summary>
/// <param name="functionName">name of the function</param>
/// <param name="argumentValues">argument values for this instance</param>
/// <returns></returns>
public static string BuildFunctionDisplayTitle(string functionName, IEnumerable<string> argumentValues)
{
var name = new StringBuilder(functionName);
if (argumentValues != null)
{
string parametersDisplayText = String.Join(", ", argumentValues);
if (parametersDisplayText != null)
{
// Remove newlines to avoid 403/forbidden storage exceptions when saving display title to blob metadata
// for function indexes. Newlines may be present in JSON-formatted arguments.
parametersDisplayText = parametersDisplayText.Replace("\r\n", String.Empty);
name.Append(" (");
if (parametersDisplayText.Length > 20)
{
name.Append(parametersDisplayText.Substring(0, 18))
.Append(" ...");
}
else
{
name.Append(parametersDisplayText);
}
name.Append(")");
}
}
return name.ToString();
}
/// <summary>
/// Validate the fields are throw if they are inconsistent.
/// </summary>
public void Validate()
{
if (this.FunctionInstanceId == Guid.Empty)
{
throw new InvalidOperationException("Function Instance Id must be set.");
}
if (string.IsNullOrWhiteSpace(this.FunctionName))
{
throw new InvalidOperationException("Function Name must be set.");
}
if (this.EndTime.HasValue)
{
if (this.StartTime > this.EndTime)
{
throw new InvalidOperationException("End Time must be greater than start time");
}
}
}
/// <summary>
/// Truncate various fields to fit in logging sizes.
/// </summary>
public void Truncate()
{
// This is fundamentally driven by performance. We need to fit log entries into table rows.
// Truncate to ensure that we're under table's maximum request payload size (4mb).
// None of these limits (except for output log) should actually get hit in normal scenarios.
this.TriggerReason = Truncate(this.TriggerReason, MaxTriggerReasonLength);
this.ErrorDetails = Truncate(this.ErrorDetails, MaxErrorLength);
// Logger should already have truncated this, but just in case.
this.LogOutput = Truncate(this.LogOutput, MaxLogOutputLength);
// Arguments may have 1 larger argument for the trigger.
// The other arguments should all be small.
if (this.Arguments != null)
{
bool truncate = false;
int argSize = 0;
foreach (var kv in this.Arguments)
{
argSize += kv.Key.Length;
if (!string.IsNullOrEmpty(kv.Value))
{
if (kv.Value.Length > MaxParameterPayloadLength)
{
truncate = true;
argSize += MaxParameterPayloadLength;
}
else
{
argSize += kv.Value.Length;
}
}
}
if (argSize > MaxParameterTotalPayloadLength)
{
// This shouldn't happen in any normal case.
// We'd need either a) a very large number of individual parameters;
// b) multiple parameters (not just the trigger) with large payloads.
// At this point, we can't log all that, so just truncate.
this.Arguments = null;
}
else if (truncate)
{
Dictionary<string, string> args2 = new Dictionary<string, string>();
foreach (var kv in this.Arguments)
{
args2[kv.Key] = Truncate(kv.Value, MaxParameterPayloadLength);
}
this.Arguments = args2;
}
}
}
private static string Truncate(string value, int maxLength)
{
if (value != null)
{
if (value.Length > maxLength)
{
return value.Substring(0, maxLength) + "...";
}
}
return value;
}
}
}